import { createTradeTicket, listPersonalCapital, listRealPortfolio, listTradeTickets, recordAuditEvent, recordPersonalCapitalAction, recordRealFill, updateRealPositionMark, updateTradeTicketStatus, } from "./_db.js"; const REQUIRED_ENV = [ ["PRODUCTION_APP_URL", "Production app URL"], ["AUTH_PROVIDER", "Real account authentication provider"], ["DATABASE_URL", "Production account database"], ["SESSION_SECRET", "Session signing secret"], ["ENCRYPTION_KEY", "Encryption key for sensitive user settings"], ["KYC_PROVIDER", "KYC / eligibility provider"], ["KYC_API_KEY", "KYC provider API key"], ["KYC_WEBHOOK_SECRET", "KYC webhook secret"], ["SANCTIONS_PROVIDER", "Sanctions / watchlist screening provider"], ["SANCTIONS_API_KEY", "Sanctions provider API key"], ["GEOIP_PROVIDER", "Geo-IP / geofencing provider"], ["GEOIP_API_KEY", "Geo-IP provider API key"], ["RESTRICTED_JURISDICTIONS", "Restricted jurisdiction rules"], ["TERMS_VERSION", "Approved terms version"], ["PRIVACY_VERSION", "Approved privacy policy version"], ["RISK_DISCLOSURE_VERSION", "Approved risk disclosure version"], ["MARKET_POLICY_STORE", "Market eligibility and category policy store"], ["CONSENT_STORE", "Versioned user consent store"], ["PAYMENTS_PROVIDER", "Deposit and withdrawal provider"], ["PAYMENTS_API_KEY", "Payments provider API key"], ["PAYMENTS_WEBHOOK_SECRET", "Payments webhook secret"], ["WEBHOOK_BASE_URL", "Public webhook base URL"], ["WALLET_PROVIDER", "Wallet or deposit-wallet provider"], ["WALLET_PROJECT_ID", "Wallet provider project ID"], ["WALLET_API_KEY", "Wallet provider API key"], ["DEPOSIT_WALLET_ADDRESS", "Polymarket deposit-wallet address"], ["POLYMARKET_SIGNATURE_TYPE", "Polymarket signature type"], ["POLYMARKET_CLOB_API_KEY", "Polymarket CLOB API key"], ["POLYMARKET_CLOB_SECRET", "Polymarket CLOB API secret"], ["POLYMARKET_CLOB_PASSPHRASE", "Polymarket CLOB passphrase"], ["SETTLEMENT_ASSET", "Settlement asset"], ["SETTLEMENT_CHAIN", "Settlement chain"], ["RECONCILIATION_STORE", "Balance, position, and fill reconciliation store"], ["ACCOUNTING_EXPORT_STORE", "Statements, tax, and accounting export store"], ["RATE_LIMIT_STORE", "Rate limit and abuse prevention store"], ["AUDIT_LOG_STORE", "Durable audit log store"], ["MONITORING_DSN", "Error and performance monitoring"], ["INCIDENT_WEBHOOK_URL", "Incident alert webhook"], ["RISK_ADMIN_TOKEN", "Risk admin incident-control token"], ["ADMIN_ALERT_EMAIL", "Admin alert destination"], ["CUSTOMER_SUPPORT_EMAIL", "Customer support contact"], ]; const PERSONAL_REQUIRED_ENV = [ ["DATABASE_URL", "Audit database"], ["DEPOSIT_WALLET_ADDRESS", "Personal Polymarket deposit wallet address"], ["POLYMARKET_SIGNATURE_TYPE", "Polymarket signature type"], ["POLYMARKET_CLOB_API_KEY", "Personal Polymarket CLOB API key"], ["POLYMARKET_CLOB_SECRET", "Personal Polymarket CLOB API secret"], ["POLYMARKET_CLOB_PASSPHRASE", "Personal Polymarket CLOB passphrase"], ["RISK_ADMIN_TOKEN", "Personal admin token"], ]; const ENV_ALIASES = { DATABASE_URL: ["NEON_DATABASE_URL"], KYC_API_KEY: ["VERIFF_API_KEY"], KYC_WEBHOOK_SECRET: ["VERIFF_WEBHOOK_SECRET", "VERIFF_SECRET_KEY"], SANCTIONS_API_KEY: ["VERIFF_API_KEY"], GEOIP_API_KEY: ["VERIFF_API_KEY"], PAYMENTS_API_KEY: ["CIRCLE_API_KEY"], PAYMENTS_WEBHOOK_SECRET: ["CIRCLE_WEBHOOK_SECRET"], WALLET_PROJECT_ID: ["CIRCLE_WALLET_SET_ID"], WALLET_API_KEY: ["CIRCLE_API_KEY"], RECONCILIATION_STORE: ["DATABASE_URL", "NEON_DATABASE_URL"], ACCOUNTING_EXPORT_STORE: ["DATABASE_URL", "NEON_DATABASE_URL"], RATE_LIMIT_STORE: ["DATABASE_URL", "NEON_DATABASE_URL"], AUDIT_LOG_STORE: ["DATABASE_URL", "NEON_DATABASE_URL"], CONSENT_STORE: ["DATABASE_URL", "NEON_DATABASE_URL"], MONITORING_DSN: ["SENTRY_DSN"], INCIDENT_WEBHOOK_URL: ["SENTRY_WEBHOOK_URL"], }; const PROVIDER_STACK = [ { provider: "Clerk", role: "User auth, sessions, sign-in/sign-out, account identity", env: ["AUTH_PROVIDER", "CLERK_PUBLISHABLE_KEY", "CLERK_SECRET_KEY", "CLERK_WEBHOOK_SIGNING_SECRET"], dashboard: "https://dashboard.clerk.com/", }, { provider: "Neon", role: "Postgres database for users, portfolios, audit metadata, reconciliation, and history", env: ["DATABASE_URL"], dashboard: "https://console.neon.tech/", }, { provider: "Veriff", role: "KYC/KYB, identity verification, age checks, AML/sanctions workflow", env: ["KYC_PROVIDER", "VERIFF_API_KEY", "VERIFF_SECRET_KEY", "VERIFF_WEBHOOK_SECRET"], dashboard: "https://station.veriff.com/", }, { provider: "Circle", role: "USDC, embedded wallets, payments/deposits/withdrawals, webhooks", env: ["PAYMENTS_PROVIDER", "WALLET_PROVIDER", "CIRCLE_API_KEY", "CIRCLE_WEBHOOK_SECRET", "CIRCLE_WALLET_SET_ID"], dashboard: "https://console.circle.com/", }, { provider: "Sentry", role: "Error monitoring, browser/backend issue tracking, incident visibility", env: ["MONITORING_DSN", "SENTRY_DSN"], dashboard: "https://sentry.io/", }, ]; const LAUNCH_REQUIREMENTS = [ ["legal", "Terms, privacy policy, risk disclosures, and jurisdiction policy approved"], ["eligibility", "KYC/KYB, age, sanctions, watchlist, and location screening active"], ["market_policy", "Allowed market categories, restricted events, and manipulation rules defined"], ["auth", "Production accounts, sessions, password reset, MFA path, and sign-out implemented"], ["wallets", "User-controlled wallet/deposit-wallet signing and permission revocation implemented"], ["payments", "Deposits, withdrawals, webhooks, failed-payment handling, and support flows implemented"], ["execution", "Signed order creation, CLOB submission, cancellation, retry, and fill tracking implemented"], ["reconciliation", "Cash, open orders, positions, fills, fees, and withdrawals reconciled continuously"], ["risk", "Agent allocation caps, market caps, stop rules, manual approval, and emergency pause enforced server-side"], ["security", "Secret management, encryption, rate limits, abuse controls, and security review completed"], ["records", "Append-only audit logs, customer statements, exports, and retention policy active"], ["operations", "Monitoring, incident response, customer support, and rollback plan ready"], ["testing", "Paper-to-live parity, dry-runs, test wallets, webhook tests, and edge-case QA completed"], ]; const WEBHOOK_ROUTES = [ ["clerk", "Clerk user and session events", "/api/webhooks/clerk"], ["veriff", "Veriff verification and review events", "/api/webhooks/veriff"], ["circle", "Circle payment, wallet, and transfer events", "/api/webhooks/circle"], ]; function envValue(key) { if (process.env[key]) return process.env[key]; return (ENV_ALIASES[key] || []).map((alias) => process.env[alias]).find(Boolean); } function providerStatus() { return REQUIRED_ENV.map(([key, label]) => ({ key, label, configured: Boolean(envValue(key)), aliases: ENV_ALIASES[key] || [], expected: key === "POLYMARKET_SIGNATURE_TYPE" ? "3 for new deposit-wallet API users" : undefined, })); } function personalStatus() { return PERSONAL_REQUIRED_ENV.map(([key, label]) => ({ key, label, configured: Boolean(envValue(key)), expected: key === "POLYMARKET_SIGNATURE_TYPE" ? "3 for deposit-wallet users" : undefined, })); } function stackStatus() { return PROVIDER_STACK.map((provider) => { const checks = provider.env.map((key) => ({ key, configured: Boolean(envValue(key)) })); return { ...provider, configured: checks.every((x) => x.configured), checks, }; }); } function liveTradingReady() { const requiredConfigured = providerStatus().every((x) => x.configured); const liveFlagEnabled = process.env.LIVE_TRADING_ENABLED === "true"; const signatureTypeOk = String(process.env.POLYMARKET_SIGNATURE_TYPE || "") === "3"; const chainOk = String(process.env.POLYMARKET_CHAIN_ID || "137") === "137"; return requiredConfigured && liveFlagEnabled && signatureTypeOk && chainOk; } function personalTradingReady() { return personalStatus().every((x) => x.configured) && process.env.PERSONAL_TRADING_ENABLED === "true" && String(process.env.POLYMARKET_SIGNATURE_TYPE || "") === "3" && String(process.env.POLYMARKET_CHAIN_ID || "137") === "137"; } function baseStatus() { const providers = providerStatus(); const origin = (envValue("WEBHOOK_BASE_URL") || envValue("PRODUCTION_APP_URL") || "").replace(/\/api\/?$/, "").replace(/\/$/, ""); const liveFlagEnabled = process.env.LIVE_TRADING_ENABLED === "true"; const signatureTypeOk = String(process.env.POLYMARKET_SIGNATURE_TYPE || "") === "3"; const chainOk = String(process.env.POLYMARKET_CHAIN_ID || "137") === "137"; const missing = providers.filter((x) => !x.configured).map((x) => x.label); const personal = personalStatus(); const personalMissing = personal.filter((x) => !x.configured).map((x) => x.label); if (process.env.PERSONAL_TRADING_ENABLED !== "true") personalMissing.push("PERSONAL_TRADING_ENABLED=true after your own wallet/CLOB test"); if (!liveFlagEnabled) missing.push("LIVE_TRADING_ENABLED=true after final approval"); if (!signatureTypeOk) missing.push("POLYMARKET_SIGNATURE_TYPE=3 for deposit-wallet users"); if (!chainOk) missing.push("POLYMARKET_CHAIN_ID=137"); return { ok: true, live_trading_enabled: liveTradingReady(), personal_trading_enabled: personalTradingReady(), live_flag_enabled: liveFlagEnabled, personal_flag_enabled: process.env.PERSONAL_TRADING_ENABLED === "true", signature_type_ok: signatureTypeOk, chain_ok: chainOk, locked_reason: liveTradingReady() ? null : "Live trading is locked until eligibility, payments, wallet/deposit-wallet signing, Polymarket CLOB credentials, audit storage, monitoring, and LIVE_TRADING_ENABLED=true are configured.", providers, provider_stack: stackStatus(), personal_requirements: personal, launch_requirements: LAUNCH_REQUIREMENTS.map(([key, label]) => ({ key, label })), webhooks: WEBHOOK_ROUTES.map(([provider, label, path]) => ({ provider, label, path, url: origin ? `${origin}${path}` : path, })), next_required: missing, personal_next_required: personalMissing, docs: { polymarket_overview: "https://docs.polymarket.com/trading/overview", polymarket_quickstart: "https://docs.polymarket.com/trading/quickstart", deposit_wallets: "https://docs.polymarket.com/trading/deposit-wallets", }, }; } function validateIntent(intent) { const missing = []; ["user_id", "wallet_address", "agent_id", "market_id", "side", "max_amount"].forEach((key) => { if (!intent || intent[key] === undefined || intent[key] === null || intent[key] === "") missing.push(key); }); if (intent && !["YES", "NO"].includes(String(intent.side).toUpperCase())) missing.push("side must be YES or NO"); if (intent && Number(intent.max_amount) <= 0) missing.push("max_amount must be positive"); return missing; } const VALID_TICKET_STATUSES = new Set(["staged", "reviewed", "placed_manually", "skipped", "cancelled"]); const VALID_FILL_ACTIONS = new Set(["BUY", "SELL"]); const VALID_CAPITAL_ACTIONS = new Set(["DEPOSIT", "WITHDRAW", "BUY_AGENT", "SELL_AGENT"]); function marketSearchUrl(question) { return `https://polymarket.com/search?query=${encodeURIComponent(String(question || ""))}`; } function buildTicketInstructions(ticket) { const marketUrl = ticket.market_url || ticket.marketUrl || marketSearchUrl(ticket.question || ticket.market_id || ticket.marketId); return { warning: "Manual review only. The AI/site does not hold a private key and does not submit orders.", steps: [ "Open the Polymarket market link.", "Confirm the exact market, side, current price, liquidity, and resolution terms.", "Keep the order at or below the staged limit price and max amount.", "Sign or place the order only from your own wallet/account.", "Return here and mark the ticket as placed manually or skipped.", ], market_url: marketUrl, }; } function normalizeTicket(body) { return { userId: String(body.user_id || "local-readiness-user").trim(), agentId: String(body.agent_id || "").trim(), marketId: String(body.market_id || "").trim(), question: String(body.question || "").trim(), marketUrl: String(body.market_url || "").trim(), side: String(body.side || "").trim().toUpperCase(), maxAmount: Number(body.max_amount || 0), limitPrice: body.limit_price === undefined || body.limit_price === "" ? null : Number(body.limit_price), rationale: String(body.rationale || "").trim(), }; } function validateTicket(ticket) { const errors = []; if (!ticket.userId) errors.push("user_id"); if (!ticket.agentId) errors.push("agent_id"); if (!ticket.marketId) errors.push("market_id"); if (!["YES", "NO"].includes(ticket.side)) errors.push("side must be YES or NO"); if (!Number.isFinite(ticket.maxAmount) || ticket.maxAmount <= 0) errors.push("max_amount must be positive"); if (ticket.limitPrice != null && (!Number.isFinite(ticket.limitPrice) || ticket.limitPrice <= 0 || ticket.limitPrice >= 1)) { errors.push("limit_price must be between 0 and 1"); } return errors; } function normalizeFill(body) { return { userId: String(body.user_id || "local-readiness-user").trim(), ticketId: body.ticket_id || null, agentId: String(body.agent_id || "").trim(), marketId: String(body.market_id || "").trim(), question: String(body.question || "").trim(), marketUrl: String(body.market_url || "").trim(), side: String(body.side || "").trim().toUpperCase(), action: String(body.fill_action || body.trade_action || "BUY").trim().toUpperCase(), shares: Number(body.shares || 0), price: Number(body.price || 0), fees: Number(body.fees || 0), txNote: String(body.tx_note || "").trim(), filledAt: body.filled_at || null, }; } function validateFill(fill) { const errors = []; if (!fill.userId) errors.push("user_id"); if (!fill.marketId) errors.push("market_id"); if (!["YES", "NO"].includes(fill.side)) errors.push("side must be YES or NO"); if (!VALID_FILL_ACTIONS.has(fill.action)) errors.push("fill_action must be BUY or SELL"); if (!Number.isFinite(fill.shares) || fill.shares <= 0) errors.push("shares must be positive"); if (!Number.isFinite(fill.price) || fill.price <= 0 || fill.price >= 1) errors.push("price must be between 0 and 1"); if (!Number.isFinite(fill.fees) || fill.fees < 0) errors.push("fees must be 0 or greater"); return errors; } function normalizeCapitalAction(body) { return { userId: String(body.user_id || "local-readiness-user").trim(), action: String(body.capital_action || body.capitalAction || "").trim().toUpperCase(), agentId: String(body.agent_id || "").trim(), amount: Number(body.amount || 0), note: String(body.note || "").trim(), }; } function validateCapitalAction(action) { const errors = []; if (!action.userId) errors.push("user_id"); if (!VALID_CAPITAL_ACTIONS.has(action.action)) errors.push("capital_action must be DEPOSIT, WITHDRAW, BUY_AGENT, or SELL_AGENT"); if ((action.action === "BUY_AGENT" || action.action === "SELL_AGENT") && !action.agentId) errors.push("agent_id"); if (!Number.isFinite(action.amount) || action.amount <= 0) errors.push("amount must be positive"); return errors; } export default async function handler(req, res) { res.setHeader("Cache-Control", "no-store"); if (req.method === "GET") { if (req.query?.action === "tickets") { const userId = String(req.query?.user_id || "local-readiness-user").trim(); const tickets = await listTradeTickets(userId, req.query?.limit || 50); return res.status(200).json({ ok: true, tickets }); } if (req.query?.action === "real_portfolio") { const userId = String(req.query?.user_id || "local-readiness-user").trim(); const portfolio = await listRealPortfolio(userId); return res.status(200).json({ ok: true, ...portfolio }); } if (req.query?.action === "personal_capital") { const userId = String(req.query?.user_id || "local-readiness-user").trim(); const capital = await listPersonalCapital(userId); return res.status(200).json({ ok: true, ...capital }); } return res.status(200).json(baseStatus()); } if (req.method === "POST") { const body = typeof req.body === "string" ? JSON.parse(req.body || "{}") : (req.body || {}); if (body.action === "ticket") { const ticket = normalizeTicket(body); const ticketErrors = validateTicket(ticket); if (ticketErrors.length) return res.status(400).json({ ok: false, error: "Invalid trade ticket", details: ticketErrors }); const saved = await createTradeTicket({ ...ticket, status: "staged", instructions: buildTicketInstructions(ticket), }); await recordAuditEvent("TRADE_TICKET_STAGED", { user_id: ticket.userId, ticket_id: saved?.id, agent_id: ticket.agentId, market_id: ticket.marketId, side: ticket.side, max_amount: ticket.maxAmount, }); return res.status(201).json({ ok: true, ticket: saved, private_key_used: false, live_order_placed: false, manual_review_required: true, }); } if (body.action === "ticket_status") { const userId = String(body.user_id || "local-readiness-user").trim(); const status = String(body.status || "").trim(); if (!VALID_TICKET_STATUSES.has(status)) return res.status(400).json({ ok: false, error: "Invalid ticket status" }); const ticket = await updateTradeTicketStatus(userId, body.ticket_id, status); if (!ticket) return res.status(404).json({ ok: false, error: "Ticket not found" }); await recordAuditEvent("TRADE_TICKET_STATUS_UPDATED", { user_id: userId, ticket_id: ticket.id, status }); return res.status(200).json({ ok: true, ticket }); } if (body.action === "record_fill") { const fill = normalizeFill(body); const fillErrors = validateFill(fill); if (fillErrors.length) return res.status(400).json({ ok: false, error: "Invalid manual fill", details: fillErrors }); const saved = await recordRealFill(fill); await recordAuditEvent("REAL_FILL_RECORDED", { user_id: fill.userId, ticket_id: fill.ticketId, market_id: fill.marketId, side: fill.side, action: fill.action, shares: fill.shares, price: fill.price, private_key_used: false, live_order_placed_by_site: false, }); return res.status(201).json({ ok: true, fill: saved, private_key_used: false, live_order_placed_by_site: false, note: "Manual fill recorded for tracking only. Poly Arena did not sign or place this trade.", }); } if (body.action === "mark_position") { const userId = String(body.user_id || "local-readiness-user").trim(); const price = Number(body.current_price || 0); if (!Number.isFinite(price) || price <= 0 || price >= 1) return res.status(400).json({ ok: false, error: "current_price must be between 0 and 1" }); const position = await updateRealPositionMark(userId, body.position_id, price); if (!position) return res.status(404).json({ ok: false, error: "Position not found" }); await recordAuditEvent("REAL_POSITION_MARK_UPDATED", { user_id: userId, position_id: position.id, current_price: price }); return res.status(200).json({ ok: true, position }); } if (body.action === "capital_action") { const capitalAction = normalizeCapitalAction(body); const capitalErrors = validateCapitalAction(capitalAction); if (capitalErrors.length) return res.status(400).json({ ok: false, error: "Invalid personal capital action", details: capitalErrors }); let capital; try { capital = await recordPersonalCapitalAction(capitalAction); } catch (err) { return res.status(400).json({ ok: false, error: err?.message || "Personal capital tracker update failed" }); } await recordAuditEvent("PERSONAL_CAPITAL_ACTION", { user_id: capitalAction.userId, action: capitalAction.action, agent_id: capitalAction.agentId, amount: capitalAction.amount, custody_by_site: false, real_order_placed_by_site: false, }); return res.status(200).json({ ok: true, ...capital, custody_by_site: false, real_order_placed_by_site: false, note: "Personal capital tracker updated for bookkeeping only. Poly Arena did not receive funds or place an order.", }); } const intent = body.intent || body; const errors = validateIntent(intent); if (errors.length) { return res.status(400).json({ ok: false, error: "Invalid order intent", details: errors }); } const status = baseStatus(); const auditEvent = { at: new Date().toISOString(), type: "ORDER_INTENT_DRY_RUN", user_id: String(intent.user_id), wallet_address: String(intent.wallet_address), agent_id: String(intent.agent_id), market_id: String(intent.market_id), side: String(intent.side).toUpperCase(), max_amount: Number(intent.max_amount), execution_mode: intent.execution_mode || "manual_approval", account_scope: intent.account_scope || body.account_scope || "public_readiness", }; if (auditEvent.account_scope === "personal") { await recordAuditEvent("PERSONAL_ORDER_INTENT_STAGED", auditEvent); return res.status(202).json({ ok: true, dry_run: true, manual_review_required: true, live_order_placed: false, message: personalTradingReady() ? "Personal prerequisites are configured, but this endpoint still stages manual review only." : "Personal order intent staged. Configure your own deposit wallet/CLOB credentials before any manual live execution.", audit_event: auditEvent, status, }); } if (!status.live_trading_enabled) { await recordAuditEvent("ORDER_INTENT_BLOCKED", auditEvent); return res.status(423).json({ ok: false, dry_run: true, error: status.locked_reason, audit_event: auditEvent, status, }); } await recordAuditEvent("ORDER_INTENT_NOT_IMPLEMENTED", auditEvent); return res.status(501).json({ ok: false, dry_run: true, error: "Live trading credentials are configured, but signed order placement is intentionally not implemented yet.", audit_event: auditEvent, status, }); } res.setHeader("Allow", "GET, POST"); return res.status(405).json({ ok: false, error: "Method not allowed" }); }