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
- Personal research mode. This copy is for your own analysis, paper tracking, and manual trade research only. It does not pool money, onboard investors, custody funds, bypass eligibility rules, or place orders for you. + Personal research mode. This copy is for your own analysis, paper tracking, and manual trade research only. It does not pool money, onboard investors, custody funds, bypass eligibility rules, or place orders without your manual approval.
@@ -545,18 +546,19 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color - +
-
๐Ÿ” Live Money Readiness
+
๐Ÿ” Personal Trading Cockpit
+
Private use only. This area is hidden from the public site and is for your own wallet, your own money, and manual approval only. It does not accept investor deposits, pool funds, sell agent access, or execute unattended trades.
-

Launch checklist

+

Personal safety checklist

-

User wallet & funds

simulation only
+

Your wallet & limits

personal only
-
Real deposits stay locked until eligibility, wallet signing, payment rails, and legal approval are complete.
+
Fund only your own wallet directly through approved provider flows. This site will not accept, hold, or withdraw money for other people.
-

Agent permissions & risk limits

what a live account would allow
+

Agent permissions & risk limits

manual approval only
-

Execution console

locked until backend exists
+

Manual order console

review before any trade
Checking backend readiness...
    -
  1. Wallet signature โ€” user signs EIP-712 orders; the site never signs unauthorized trades.
  2. -
  3. CLOB router โ€” backend submits, cancels, and tracks orders through authenticated Polymarket APIs.
  4. -
  5. Fill sync โ€” real positions, cash, unsettled orders, and agent actions reconcile continuously.
  6. -
  7. Emergency controls โ€” user can pause agents, cancel orders, and withdraw available funds any time.
  8. +
  9. Research signal โ€” choose an agent idea and inspect the exact market yourself.
  10. +
  11. Manual review โ€” confirm side, amount, price, risk cap, and current market status.
  12. +
  13. Your wallet only โ€” sign or submit only from your own Polymarket/deposit wallet.
  14. +
  15. Emergency pause โ€” keep agent automation off and cancel staged intents any time.
-

Audit trail preview

required for real money
+

Personal audit trail

local + Neon records
@@ -616,8 +618,8 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color

Server controls

live safety gates
- Production switch remains off -
The backend can now store policies, consent, risk profiles, webhooks, incidents, and dry-run order intents. Real deposits and live orders still require final approval and real CLOB/deposit-wallet setup.
+ Public production switch remains off +
The public product remains paper trading only. Personal mode can store your wallet limits, consent status, staged manual intents, webhooks, and incident records, but does not onboard investors or pool money.
@@ -720,7 +722,7 @@ function applyPersonalMode(){ if(sub)sub.textContent="private research ยท paper tracking ยท manual links"; const heroTitle=document.querySelector(".hero h1"),heroCopy=document.querySelector(".hero p"); if(heroTitle)heroTitle.innerHTML=`Your private agent lab for manual market research`; - if(heroCopy)heroCopy.textContent="Use the same agents, charts, paper portfolios, and Polymarket links for your own research. Nothing here pools outside money, manages investor funds, or places live orders."; + if(heroCopy)heroCopy.textContent="Use the same agents, charts, paper portfolios, and Polymarket links for your own research. Nothing here pools outside money, manages investor funds, or places live orders without manual review."; document.querySelectorAll(".hbadge").forEach((b,i)=>{ const labels=["Private research mode","Manual execution only","Cloud paper accounts","Live market links"]; if(labels[i])b.textContent=labels[i]; @@ -2331,19 +2333,18 @@ function renderAbout(){
${a.emoji}

${a.name}

${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(p.label)}${p.expected?`
${esc(p.expected)}
`:""}
${p.configured?"configured":"missing"}
`).join(""); + const personalReqs=(s.personal_requirements||[]).map(p=>`
${esc(p.label)}${p.expected?`
${esc(p.expected)}
`:""}
${p.configured?"ready":"missing"}
`).join(""); const stack=(s.provider_stack||[]).map(p=>`
${esc(p.provider)}
${esc(p.role)}
${(p.checks||[]).map(c=>`${esc(c.key)}: ${c.configured?"ok":"missing"}`).join(" ยท ")}
${p.configured?"ready":"needs keys"}
`).join(""); const next=(s.next_required||[]).slice(0,12).map(x=>`
  • ${esc(x)}
  • `).join(""); + const personalNext=(s.personal_next_required||[]).slice(0,12).map(x=>`
  • ${esc(x)}
  • `).join(""); const reqs=(s.launch_requirements||[]).map(x=>`
  • ${esc(x.label)}
  • `).join(""); - root.innerHTML=`${s.live_trading_enabled?"Live trading unlocked":"Live trading locked"}
    ${esc(s.locked_reason||"All required backend providers are configured.")}
    ${stack?`
    Your provider stack${stack}
    `:""}${providers}${next?`
    Next setup items
    `:""}${reqs?`
    Full launch requirements
      ${reqs}
    `:""}
    Live execution stays disabled until every provider is configured, every launch requirement is complete, and final approval flips LIVE_TRADING_ENABLED=true.
    `; + root.innerHTML=`${s.personal_trading_enabled?"Personal prerequisites ready":"Manual personal mode locked to staging"}
    ${esc(s.locked_reason||"Public trading remains locked.")}
    ${personalReqs?`
    Personal-only requirements${personalReqs}
    `:""}${personalNext?`
    Before personal live execution
    `:""}${stack?`
    Public provider stack${stack}
    `:""}${providers}${next?`
    Public platform blockers
    `:""}${reqs?`
    Public launch requirements
      ${reqs}
    `:""}
    This cockpit stages manual intents only. Public investor deposits and unattended agent trading stay off.
    `; if(hookRoot){ const hooks=(s.webhooks||[]).map(h=>`
    ${esc(h.provider)}
    ${esc(h.label)}
    ${esc(h.url)}
    `).join(""); hookRoot.innerHTML=hooks||"Webhook URLs will appear after backend readiness loads."; @@ -2411,24 +2414,27 @@ async function dryRunLiveOrderIntent(){ side:$("liveIntentSide").value, max_amount:Number($("liveIntentAmount").value||0), execution_mode:"manual_approval", + account_scope:PERSONAL_MODE?"personal":"public_readiness", }; 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}`); + saveLiveState(s,`${PERSONAL_MODE?"Staged manual":"Dry-ran"} ${intent.side} order intent for ${intent.agent_id} on ${intent.market_id}`); }catch(e){ toast("Order dry-run failed."); } } async function saveRiskProfileToServer(s){ + const permissions=JSON.parse(JSON.stringify(s.agents||{})); + Object.values(permissions).forEach(p=>{p.autoTrade=false;p.manualApprove=true;}); const body={ user_id:"local-readiness-user", wallet_address:s.walletAddress, jurisdiction:s.jurisdiction, deposit_limit:s.depositLimit, withdraw_reserve:s.withdrawReserve, - agent_permissions:s.agents, + agent_permissions:permissions, }; const r=await fetch("/api/risk-profile",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)}); const d=await r.json(); @@ -2473,7 +2479,7 @@ function renderLiveMoneyTab(){
    ${esc(a.name)}
    ${esc(a.blurb.slice(0,120))}${a.blurb.length>120?"...":""}
    - +
    @@ -2490,7 +2496,8 @@ function esc(s){return String(s).replace(/[&<>"]/g,c=>({"&":"&","<":"<"," const TABS=["overview","leaderboard","suggestions","portfolio","invest","paper","live","about"]; function showTab(name){ if(!TABS.includes(name))name="overview"; - if(PERSONAL_MODE&&["invest","live"].includes(name))name="overview"; + if(PERSONAL_MODE&&name==="invest")name="overview"; + if(!PERSONAL_MODE&&name==="live")name="overview"; document.querySelectorAll(".tabpanel").forEach(p=>p.classList.toggle("active",p.dataset.tab===name)); document.querySelectorAll(".tab").forEach(t=>t.classList.toggle("active",t.dataset.tab===name)); if(location.hash.slice(1)!==name)history.replaceState(null,"","#"+name); diff --git a/personal.html b/personal.html index 5f7b6f5..67decbe 100644 --- a/personal.html +++ b/personal.html @@ -4,8 +4,8 @@ Personal Polymarket Arena - - + +

    Opening personal research mode...