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

Execution console

locked until backend exists
+
Checking backend readiness...
+
  1. Wallet signature — user signs EIP-712 orders; the site never signs unauthorized trades.
  2. CLOB router — backend submits, cancels, and tracks orders through authenticated Polymarket APIs.
  3. @@ -578,6 +586,7 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color

Audit trail preview

required for real money
+
@@ -661,6 +670,7 @@ const CLOB = "https://clob.polymarket.com"; let PAPER_SEARCH_RESULTS = null; let PAPER_MARKET_OFFSET = 0; let PAPER_MARKET_QUERY = ""; +let LIVE_BACKEND_STATUS = null; const catColor = (c) => CAT_COLORS[c] || CAT_COLORS.Other; const getFocus = () => localStorage.getItem(FOCUS_KEY) || "All"; const setFocus = (c) => localStorage.setItem(FOCUS_KEY, c); @@ -2236,6 +2246,42 @@ function saveLiveState(s,detail){ localStorage.setItem(LIVE_KEY,JSON.stringify(s)); pushCloudState(); } +async function fetchLiveBackendStatus(){ + try{ + const r=await fetch("/api/live",{cache:"no-store"}); + LIVE_BACKEND_STATUS=await r.json(); + }catch(e){ + LIVE_BACKEND_STATUS={ok:false,locked_reason:"Live backend status endpoint is unavailable.",providers:[]}; + } + renderLiveBackendStatus(); +} +function renderLiveBackendStatus(){ + const root=$("liveBackendStatus");if(!root)return; + const s=LIVE_BACKEND_STATUS; + if(!s){root.innerHTML="Checking backend readiness...";return;} + const providers=(s.providers||[]).map(p=>`
${esc(p.label)}${p.configured?"configured":"missing"}
`).join(""); + root.innerHTML=`${s.live_trading_enabled?"Live trading unlocked":"Live trading locked"}
${esc(s.locked_reason||"All required backend providers are configured.")}
${providers}`; +} +async function dryRunLiveOrderIntent(){ + const s=loadLiveState(); + const intent={ + user_id:"local-readiness-user", + wallet_address:s.walletAddress, + agent_id:$("liveIntentAgent").value, + market_id:$("liveIntentMarket").value.trim(), + side:$("liveIntentSide").value, + max_amount:Number($("liveIntentAmount").value||0), + execution_mode:"manual_approval", + }; + try{ + const r=await fetch("/api/live",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({intent})}); + const d=await r.json(); + $("liveOrderDryRun").innerHTML=`
Order intent dry-run
${esc(d.error||"Accepted dry-run.")}
${esc(JSON.stringify(d.audit_event||d,null,2))}
`; + saveLiveState(s,`Dry-ran ${intent.side} order intent for ${intent.agent_id} on ${intent.market_id}`); + }catch(e){ + toast("Order dry-run failed."); + } +} function renderLiveMoneyTab(){ const root=$("liveChecklist");if(!root)return; const s=loadLiveState(),done=LIVE_CHECKS.filter(([k])=>s.checks[k]).length; @@ -2247,6 +2293,9 @@ function renderLiveMoneyTab(){ $("liveJurisdiction").value=s.jurisdiction||""; $("liveDepositLimit").value=s.depositLimit||""; $("liveWithdrawReserve").value=s.withdrawReserve||""; + const intentAgent=$("liveIntentAgent"); + if(intentAgent)intentAgent.innerHTML=AGENTS.map(a=>``).join(""); + renderLiveBackendStatus(); $("liveAgentPermissions").innerHTML=AGENTS.map(a=>{const p=s.agents[a.id]; return `
${a.emoji}
@@ -2360,6 +2409,7 @@ $("saveLiveProfileBtn").addEventListener("click",()=>{ saveLiveState(s,"Updated live-money wallet/funds profile"); renderLiveMoneyTab();toast("Live-money readiness profile saved."); }); +$("dryRunOrderBtn").addEventListener("click",()=>dryRunLiveOrderIntent()); window.addEventListener("focus",()=>refreshFromCloudAndRender(true)); document.addEventListener("visibilitychange",()=>{ if(!document.hidden)refreshFromCloudAndRender(true); @@ -2368,6 +2418,7 @@ document.addEventListener("visibilitychange",()=>{ /* On load: first visit runs a 7-day backtest; afterwards the shared portfolio advances once per hour. */ (async function init(){ showTab(location.hash.slice(1)||"overview"); + fetchLiveBackendStatus(); const cloud=await loadAuthoritativeCloudState(); let st=loadState(); if(cloud.loaded){