diff --git a/REAL_MONEY_ROADMAP.md b/REAL_MONEY_ROADMAP.md new file mode 100644 index 0000000..193237d --- /dev/null +++ b/REAL_MONEY_ROADMAP.md @@ -0,0 +1,56 @@ +# Real Money Enablement Roadmap + +This app is currently paper trading only. The Live Money tab and `/api/live` +endpoint are readiness scaffolding: they make the required product and backend +interfaces visible without moving funds or placing orders. + +## Required before live deposits or trading + +1. Legal and eligibility + - Terms, risk disclosures, privacy policy, and jurisdiction policy. + - KYC / KYB provider. + - Age, location, sanctions, and restricted-market checks. + +2. Account and wallet + - Real authentication. + - Non-custodial wallet connection or Polymarket deposit-wallet flow. + - User-controlled permissions for each agent. + +3. Funds + - Deposit provider. + - Withdrawal provider. + - Balance reconciliation, statements, support, and accounting exports. + +4. Execution + - Backend order router. + - Polymarket CLOB API credentials. + - EIP-712 order-signing flow. + - Order placement, cancellation, fill tracking, and position reconciliation. + +5. Risk and controls + - Per-agent max allocation. + - Per-market max position size. + - Category exclusions. + - Manual approval mode. + - Emergency pause and cancel-all. + - Always-available withdrawal path. + +6. Audit and monitoring + - Durable append-only audit log. + - Signal, approval, order, fill, cancellation, deposit, and withdrawal events. + - Admin monitoring and incident response. + +## Environment variables expected by `/api/live` + +- `KYC_PROVIDER` +- `PAYMENTS_PROVIDER` +- `WALLET_PROVIDER` +- `POLYMARKET_CLOB_API_KEY` +- `POLYMARKET_CLOB_SECRET` +- `POLYMARKET_CLOB_PASSPHRASE` +- `AUDIT_LOG_STORE` +- `LIVE_TRADING_ENABLED=true` + +Even when all variables are present, live order placement remains intentionally +unimplemented until wallet signing, compliance approval, and safe order routing +are completed. diff --git a/api/live.js b/api/live.js new file mode 100644 index 0000000..e6fafe6 --- /dev/null +++ b/api/live.js @@ -0,0 +1,95 @@ +const REQUIRED_ENV = [ + ["KYC_PROVIDER", "KYC / eligibility provider"], + ["PAYMENTS_PROVIDER", "Deposit and withdrawal provider"], + ["WALLET_PROVIDER", "Wallet or deposit-wallet provider"], + ["POLYMARKET_CLOB_API_KEY", "Polymarket CLOB API key"], + ["POLYMARKET_CLOB_SECRET", "Polymarket CLOB API secret"], + ["POLYMARKET_CLOB_PASSPHRASE", "Polymarket CLOB passphrase"], + ["AUDIT_LOG_STORE", "Durable audit log store"], +]; + +function providerStatus() { + return REQUIRED_ENV.map(([key, label]) => ({ + key, + label, + configured: Boolean(process.env[key]), + })); +} + +function liveTradingReady() { + return providerStatus().every((x) => x.configured) && process.env.LIVE_TRADING_ENABLED === "true"; +} + +function baseStatus() { + const providers = providerStatus(); + return { + ok: true, + live_trading_enabled: liveTradingReady(), + locked_reason: liveTradingReady() + ? null + : "Live trading is locked until KYC, payments, wallet signing, CLOB credentials, audit storage, and LIVE_TRADING_ENABLED=true are configured.", + providers, + next_required: providers.filter((x) => !x.configured).map((x) => x.label), + }; +} + +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; +} + +export default async function handler(req, res) { + res.setHeader("Cache-Control", "no-store"); + + if (req.method === "GET") { + return res.status(200).json(baseStatus()); + } + + if (req.method === "POST") { + const body = typeof req.body === "string" ? JSON.parse(req.body || "{}") : (req.body || {}); + 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", + }; + + if (!status.live_trading_enabled) { + return res.status(423).json({ + ok: false, + dry_run: true, + error: status.locked_reason, + audit_event: auditEvent, + status, + }); + } + + 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" }); +} diff --git a/index.html b/index.html index 11e47bf..959a17e 100644 --- a/index.html +++ b/index.html @@ -569,6 +569,14 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
${esc(JSON.stringify(d.audit_event||d,null,2))}