diff --git a/.env.example b/.env.example index 741c09c..6b2f6ac 100644 --- a/.env.example +++ b/.env.example @@ -7,6 +7,9 @@ PRODUCTION_APP_URL=https://polymarket-site-eta.vercel.app # Keep false until legal/compliance approval, provider credentials, wallet signing, # payment rails, and audit logging are production-ready. LIVE_TRADING_ENABLED=false +# Personal-only feature gate. Keep false until your own wallet/CLOB credentials +# are configured and tiny manual test orders are reviewed. +PERSONAL_TRADING_ENABLED=false # Accounts / authentication AUTH_PROVIDER=Clerk diff --git a/REAL_MONEY_ROADMAP.md b/REAL_MONEY_ROADMAP.md index fd492c9..794f4b7 100644 --- a/REAL_MONEY_ROADMAP.md +++ b/REAL_MONEY_ROADMAP.md @@ -1,14 +1,15 @@ # 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. +This public app is currently paper trading only. The public site hides live +trading controls. The `/personal` route opens a private cockpit for your own +wallet, your own money, manual approval, and audit logging. ## What is configured now - `.env.example` lists the environment variables the production app needs. - `/api/live` reports which live-money providers are configured or missing. -- `/api/live` accepts order intents only as dry-runs and returns an audit event. +- `/api/live` accepts public order intents only as dry-runs and returns an + audit event. Personal-mode intents are staged for manual review, not executed. - `/api/accounts` creates, logs into, and saves password-backed paper accounts through Vercel Blob storage. - `/api/config` exposes safe public provider configuration, including the @@ -25,11 +26,35 @@ interfaces visible without moving funds or placing orders. permission settings server-side. - `/api/incident` records incident events and can notify the configured incident webhook after `RISK_ADMIN_TOKEN` is set. -- The Live Money tab shows launch checks, wallet/deposit settings, agent risk - limits, provider webhook URLs, a dry-run order console, and an audit preview. +- The public site hides the live-money tab. `/personal` shows wallet/deposit + settings, personal risk limits, provider webhook URLs, a manual intent + console, and an audit preview. -The app still refuses to place live orders. That is intentional until the legal, -provider, wallet-signing, reconciliation, and monitoring steps below are done. +The app still refuses to place live orders. Personal mode stages manual intents +only until your own deposit wallet, CLOB credentials, and tiny test flow are +ready. + +## Personal-use path + +Personal mode is narrower than the public business launch: + +- Your own money only. +- Your own Polymarket/deposit wallet only. +- No outside investors, deposits, pooled funds, or copy-trading customers. +- No unattended agent trading. +- Every live trade starts as a staged manual intent and must be reviewed before + you do anything in Polymarket. +- Public `LIVE_TRADING_ENABLED` stays `false`. + +Personal mode still needs: + +- `DEPOSIT_WALLET_ADDRESS` +- `POLYMARKET_SIGNATURE_TYPE=3` +- `POLYMARKET_CLOB_API_KEY` +- `POLYMARKET_CLOB_SECRET` +- `POLYMARKET_CLOB_PASSPHRASE` +- `RISK_ADMIN_TOKEN` +- `PERSONAL_TRADING_ENABLED=true` only after your own tiny manual test flow ## Required before live deposits or trading diff --git a/api/live.js b/api/live.js index fd8d9ad..01050d7 100644 --- a/api/live.js +++ b/api/live.js @@ -44,6 +44,16 @@ const REQUIRED_ENV = [ ["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"], @@ -133,6 +143,15 @@ function providerStatus() { })); } +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)) })); @@ -152,6 +171,13 @@ function liveTradingReady() { 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(/\/$/, ""); @@ -159,13 +185,18 @@ function baseStatus() { 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() @@ -173,6 +204,7 @@ function baseStatus() { : "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, @@ -181,6 +213,7 @@ function baseStatus() { 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", @@ -225,8 +258,24 @@ export default async function handler(req, res) { 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({ diff --git a/api/risk-profile.js b/api/risk-profile.js index 8586167..800d3ed 100644 --- a/api/risk-profile.js +++ b/api/risk-profile.js @@ -10,9 +10,7 @@ function validateProfile(profile) { if (Number(permission.maxAllocation || 0) < 0) errors.push(`${agentId} maxAllocation must be 0 or greater`); const maxPositionPct = Number(permission.maxPositionPct || 0); if (maxPositionPct < 0 || maxPositionPct > 100) errors.push(`${agentId} maxPositionPct must be between 0 and 100`); - if (permission.autoTrade && !permission.manualApprove) { - errors.push(`${agentId} cannot enable unattended real-money trading before final approval`); - } + if (permission.autoTrade) errors.push(`${agentId} cannot enable unattended real-money trading in personal mode`); }); return errors; diff --git a/index.html b/index.html index 26fed1b..86af8c6 100644 --- a/index.html +++ b/index.html @@ -69,7 +69,8 @@ h1,h2,h3,.brand-name{font-family:'Space Grotesk','Inter',sans-serif} .btn.primary:hover{filter:brightness(1.08)} .btn:disabled{opacity:.55;cursor:wait;transform:none} .btn.ghost{background:transparent} -.personal-mode [data-tab="invest"],.personal-mode [data-tab="live"]{display:none!important} +body:not(.personal-mode) [data-tab="live"]{display:none!important} +.personal-mode [data-tab="invest"]{display:none!important} .personal-banner{display:none;margin:0 0 14px;border:1px solid rgba(251,191,36,.22);border-radius:14px; padding:12px 14px;background:rgba(251,191,36,.08);color:#f8e7b0;font-size:13px} .personal-mode .personal-banner{display:block} @@ -337,7 +338,7 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
@@ -545,18 +546,19 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color - +${agentBlurb(a,st)}
`).join(""); } const LIVE_CHECKS=[ - ["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"], - ["wallet","User-controlled wallet/deposit-wallet signing and permission revocation implemented"], - ["payments","Deposits, withdrawals, webhooks, failed-payment handling, and support flows implemented"], - ["clob","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"], + ["personal_scope","I will use only my own money and my own Polymarket/deposit wallet"], + ["no_investors","No outside investors, pooled funds, user deposits, or copy-trading customers"], + ["eligibility","I personally satisfy the provider and Polymarket eligibility/location rules"], + ["wallet","My own deposit wallet is created, controlled by me, and funded only by me"], + ["clob","Personal CLOB credentials are configured only for my own wallet"], + ["manual_only","Every staged intent requires manual review before any order is placed"], + ["reconciliation","I will compare cash, open orders, fills, and positions after each test"], + ["risk","Personal max amount, per-agent caps, stop rules, and emergency pause are set"], ["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"], + ["records","Append-only audit logs are recording profile changes and staged intents"], + ["operations","Incident alerting and emergency pause path are ready"], + ["testing","Tiny manual test trades are checked before increasing size"], ]; function defaultLiveState(){ const checks={};LIVE_CHECKS.forEach(([k])=>checks[k]=false); @@ -2386,10 +2387,12 @@ function renderLiveBackendStatus(){ const s=LIVE_BACKEND_STATUS; if(!s){root.innerHTML="Checking backend readiness...";if(hookRoot)hookRoot.innerHTML="Checking webhook setup...";if(policyRoot)policyRoot.innerHTML="Checking policy status...";return;} const providers=(s.providers||[]).map(p=>`${esc(JSON.stringify(d.audit_event||d,null,2))}Opening personal research mode...