Split personal trading cockpit from public site

This commit is contained in:
Theodore Song
2026-07-14 07:54:50 -04:00
parent 3cc818a13f
commit 3367daf2f1
6 changed files with 132 additions and 50 deletions
+3
View File
@@ -7,6 +7,9 @@ PRODUCTION_APP_URL=https://polymarket-site-eta.vercel.app
# Keep false until legal/compliance approval, provider credentials, wallet signing, # Keep false until legal/compliance approval, provider credentials, wallet signing,
# payment rails, and audit logging are production-ready. # payment rails, and audit logging are production-ready.
LIVE_TRADING_ENABLED=false 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 # Accounts / authentication
AUTH_PROVIDER=Clerk AUTH_PROVIDER=Clerk
+33 -8
View File
@@ -1,14 +1,15 @@
# Real Money Enablement Roadmap # Real Money Enablement Roadmap
This app is currently paper trading only. The Live Money tab and `/api/live` This public app is currently paper trading only. The public site hides live
endpoint are readiness scaffolding: they make the required product and backend trading controls. The `/personal` route opens a private cockpit for your own
interfaces visible without moving funds or placing orders. wallet, your own money, manual approval, and audit logging.
## What is configured now ## What is configured now
- `.env.example` lists the environment variables the production app needs. - `.env.example` lists the environment variables the production app needs.
- `/api/live` reports which live-money providers are configured or missing. - `/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 - `/api/accounts` creates, logs into, and saves password-backed paper accounts
through Vercel Blob storage. through Vercel Blob storage.
- `/api/config` exposes safe public provider configuration, including the - `/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. permission settings server-side.
- `/api/incident` records incident events and can notify the configured incident - `/api/incident` records incident events and can notify the configured incident
webhook after `RISK_ADMIN_TOKEN` is set. webhook after `RISK_ADMIN_TOKEN` is set.
- The Live Money tab shows launch checks, wallet/deposit settings, agent risk - The public site hides the live-money tab. `/personal` shows wallet/deposit
limits, provider webhook URLs, a dry-run order console, and an audit preview. 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, The app still refuses to place live orders. Personal mode stages manual intents
provider, wallet-signing, reconciliation, and monitoring steps below are done. 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 ## Required before live deposits or trading
+49
View File
@@ -44,6 +44,16 @@ const REQUIRED_ENV = [
["CUSTOMER_SUPPORT_EMAIL", "Customer support contact"], ["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 = { const ENV_ALIASES = {
DATABASE_URL: ["NEON_DATABASE_URL"], DATABASE_URL: ["NEON_DATABASE_URL"],
KYC_API_KEY: ["VERIFF_API_KEY"], 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() { function stackStatus() {
return PROVIDER_STACK.map((provider) => { return PROVIDER_STACK.map((provider) => {
const checks = provider.env.map((key) => ({ key, configured: Boolean(envValue(key)) })); const checks = provider.env.map((key) => ({ key, configured: Boolean(envValue(key)) }));
@@ -152,6 +171,13 @@ function liveTradingReady() {
return requiredConfigured && liveFlagEnabled && signatureTypeOk && chainOk; 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() { function baseStatus() {
const providers = providerStatus(); const providers = providerStatus();
const origin = (envValue("WEBHOOK_BASE_URL") || envValue("PRODUCTION_APP_URL") || "").replace(/\/api\/?$/, "").replace(/\/$/, ""); 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 signatureTypeOk = String(process.env.POLYMARKET_SIGNATURE_TYPE || "") === "3";
const chainOk = String(process.env.POLYMARKET_CHAIN_ID || "137") === "137"; const chainOk = String(process.env.POLYMARKET_CHAIN_ID || "137") === "137";
const missing = providers.filter((x) => !x.configured).map((x) => x.label); 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 (!liveFlagEnabled) missing.push("LIVE_TRADING_ENABLED=true after final approval");
if (!signatureTypeOk) missing.push("POLYMARKET_SIGNATURE_TYPE=3 for deposit-wallet users"); if (!signatureTypeOk) missing.push("POLYMARKET_SIGNATURE_TYPE=3 for deposit-wallet users");
if (!chainOk) missing.push("POLYMARKET_CHAIN_ID=137"); if (!chainOk) missing.push("POLYMARKET_CHAIN_ID=137");
return { return {
ok: true, ok: true,
live_trading_enabled: liveTradingReady(), live_trading_enabled: liveTradingReady(),
personal_trading_enabled: personalTradingReady(),
live_flag_enabled: liveFlagEnabled, live_flag_enabled: liveFlagEnabled,
personal_flag_enabled: process.env.PERSONAL_TRADING_ENABLED === "true",
signature_type_ok: signatureTypeOk, signature_type_ok: signatureTypeOk,
chain_ok: chainOk, chain_ok: chainOk,
locked_reason: liveTradingReady() 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.", : "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, providers,
provider_stack: stackStatus(), provider_stack: stackStatus(),
personal_requirements: personal,
launch_requirements: LAUNCH_REQUIREMENTS.map(([key, label]) => ({ key, label })), launch_requirements: LAUNCH_REQUIREMENTS.map(([key, label]) => ({ key, label })),
webhooks: WEBHOOK_ROUTES.map(([provider, label, path]) => ({ webhooks: WEBHOOK_ROUTES.map(([provider, label, path]) => ({
provider, provider,
@@ -181,6 +213,7 @@ function baseStatus() {
url: origin ? `${origin}${path}` : path, url: origin ? `${origin}${path}` : path,
})), })),
next_required: missing, next_required: missing,
personal_next_required: personalMissing,
docs: { docs: {
polymarket_overview: "https://docs.polymarket.com/trading/overview", polymarket_overview: "https://docs.polymarket.com/trading/overview",
polymarket_quickstart: "https://docs.polymarket.com/trading/quickstart", polymarket_quickstart: "https://docs.polymarket.com/trading/quickstart",
@@ -225,8 +258,24 @@ export default async function handler(req, res) {
side: String(intent.side).toUpperCase(), side: String(intent.side).toUpperCase(),
max_amount: Number(intent.max_amount), max_amount: Number(intent.max_amount),
execution_mode: intent.execution_mode || "manual_approval", 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) { if (!status.live_trading_enabled) {
await recordAuditEvent("ORDER_INTENT_BLOCKED", auditEvent); await recordAuditEvent("ORDER_INTENT_BLOCKED", auditEvent);
return res.status(423).json({ return res.status(423).json({
+1 -3
View File
@@ -10,9 +10,7 @@ function validateProfile(profile) {
if (Number(permission.maxAllocation || 0) < 0) errors.push(`${agentId} maxAllocation must be 0 or greater`); if (Number(permission.maxAllocation || 0) < 0) errors.push(`${agentId} maxAllocation must be 0 or greater`);
const maxPositionPct = Number(permission.maxPositionPct || 0); const maxPositionPct = Number(permission.maxPositionPct || 0);
if (maxPositionPct < 0 || maxPositionPct > 100) errors.push(`${agentId} maxPositionPct must be between 0 and 100`); if (maxPositionPct < 0 || maxPositionPct > 100) errors.push(`${agentId} maxPositionPct must be between 0 and 100`);
if (permission.autoTrade && !permission.manualApprove) { if (permission.autoTrade) errors.push(`${agentId} cannot enable unattended real-money trading in personal mode`);
errors.push(`${agentId} cannot enable unattended real-money trading before final approval`);
}
}); });
return errors; return errors;
+44 -37
View File
@@ -69,7 +69,8 @@ h1,h2,h3,.brand-name{font-family:'Space Grotesk','Inter',sans-serif}
.btn.primary:hover{filter:brightness(1.08)} .btn.primary:hover{filter:brightness(1.08)}
.btn:disabled{opacity:.55;cursor:wait;transform:none} .btn:disabled{opacity:.55;cursor:wait;transform:none}
.btn.ghost{background:transparent} .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; .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} padding:12px 14px;background:rgba(251,191,36,.08);color:#f8e7b0;font-size:13px}
.personal-mode .personal-banner{display:block} .personal-mode .personal-banner{display:block}
@@ -337,7 +338,7 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
</div> </div>
</nav> </nav>
<div class="personal-banner" id="personalBanner"> <div class="personal-banner" id="personalBanner">
<b>Personal research mode.</b> 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. <b>Personal research mode.</b> 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.
</div> </div>
<!-- ============ OVERVIEW ============ --> <!-- ============ OVERVIEW ============ -->
@@ -545,18 +546,19 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
</div> </div>
</section> </section>
<!-- ============ LIVE MONEY READINESS ============ --> <!-- ============ PERSONAL TRADING COCKPIT ============ -->
<section class="tabpanel" data-tab="live"> <section class="tabpanel" data-tab="live">
<div class="section-title">🔐 Live Money Readiness</div> <div class="section-title">🔐 Personal Trading Cockpit</div>
<div class="disclaimer" style="margin-bottom:16px"><b>Private use only.</b> 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.</div>
<div class="grid2"> <div class="grid2">
<div class="card"> <div class="card">
<div class="card-h"><h3>Launch checklist</h3><span class="small muted" id="liveReadyPct"></span></div> <div class="card-h"><h3>Personal safety checklist</h3><span class="small muted" id="liveReadyPct"></span></div>
<div id="liveChecklist"></div> <div id="liveChecklist"></div>
</div> </div>
<div class="card"> <div class="card">
<div class="card-h"><h3>User wallet & funds</h3><span class="small muted">simulation only</span></div> <div class="card-h"><h3>Your wallet & limits</h3><span class="small muted">personal only</span></div>
<div class="account-row"> <div class="account-row">
<input class="input" id="liveWalletAddress" placeholder="Wallet / deposit wallet address" /> <input class="input" id="liveWalletAddress" placeholder="Your Polymarket deposit wallet address" />
<select class="select" id="liveJurisdiction"> <select class="select" id="liveJurisdiction">
<option value="">Jurisdiction</option><option>United States</option><option>Canada</option><option>European Union</option><option>Other</option> <option value="">Jurisdiction</option><option>United States</option><option>Canada</option><option>European Union</option><option>Other</option>
</select> </select>
@@ -565,35 +567,35 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
<input class="input" id="liveDepositLimit" type="number" min="0" step="100" placeholder="Max deposit" /> <input class="input" id="liveDepositLimit" type="number" min="0" step="100" placeholder="Max deposit" />
<input class="input" id="liveWithdrawReserve" type="number" min="0" step="100" placeholder="Withdrawal reserve" /> <input class="input" id="liveWithdrawReserve" type="number" min="0" step="100" placeholder="Withdrawal reserve" />
<button class="btn" id="saveLiveProfileBtn">Save profile</button> <button class="btn" id="saveLiveProfileBtn">Save profile</button>
<button class="btn primary" disabled>Real deposits locked</button> <button class="btn primary" disabled>No public deposits</button>
</div> </div>
<div class="risk-note" style="margin-top:12px">Real deposits stay locked until eligibility, wallet signing, payment rails, and legal approval are complete.</div> <div class="risk-note" style="margin-top:12px">Fund only your own wallet directly through approved provider flows. This site will not accept, hold, or withdraw money for other people.</div>
</div> </div>
</div> </div>
<div class="card"> <div class="card">
<div class="card-h"><h3>Agent permissions & risk limits</h3><span class="small muted">what a live account would allow</span></div> <div class="card-h"><h3>Agent permissions & risk limits</h3><span class="small muted">manual approval only</span></div>
<div id="liveAgentPermissions"></div> <div id="liveAgentPermissions"></div>
</div> </div>
<div class="grid2"> <div class="grid2">
<div class="card"> <div class="card">
<div class="card-h"><h3>Execution console</h3><span class="small muted">locked until backend exists</span></div> <div class="card-h"><h3>Manual order console</h3><span class="small muted">review before any trade</span></div>
<div id="liveBackendStatus" class="locked-panel">Checking backend readiness...</div> <div id="liveBackendStatus" class="locked-panel">Checking backend readiness...</div>
<div class="account-row"> <div class="account-row">
<select class="select" id="liveIntentAgent"></select> <select class="select" id="liveIntentAgent"></select>
<input class="input" id="liveIntentMarket" placeholder="Market ID" /> <input class="input" id="liveIntentMarket" placeholder="Market ID" />
<select class="select" id="liveIntentSide"><option>YES</option><option>NO</option></select> <select class="select" id="liveIntentSide"><option>YES</option><option>NO</option></select>
<input class="input" id="liveIntentAmount" type="number" min="1" step="25" placeholder="Max amount" /> <input class="input" id="liveIntentAmount" type="number" min="1" step="25" placeholder="Max amount" />
<button class="btn" id="dryRunOrderBtn">Dry-run order intent</button> <button class="btn" id="dryRunOrderBtn">Stage manual intent</button>
</div> </div>
<ol class="steps"> <ol class="steps">
<li><b>Wallet signature</b>user signs EIP-712 orders; the site never signs unauthorized trades.</li> <li><b>Research signal</b>choose an agent idea and inspect the exact market yourself.</li>
<li><b>CLOB router</b> — backend submits, cancels, and tracks orders through authenticated Polymarket APIs.</li> <li><b>Manual review</b> — confirm side, amount, price, risk cap, and current market status.</li>
<li><b>Fill sync</b>real positions, cash, unsettled orders, and agent actions reconcile continuously.</li> <li><b>Your wallet only</b>sign or submit only from your own Polymarket/deposit wallet.</li>
<li><b>Emergency controls</b>user can pause agents, cancel orders, and withdraw available funds any time.</li> <li><b>Emergency pause</b>keep agent automation off and cancel staged intents any time.</li>
</ol> </ol>
</div> </div>
<div class="card"> <div class="card">
<div class="card-h"><h3>Audit trail preview</h3><span class="small muted">required for real money</span></div> <div class="card-h"><h3>Personal audit trail</h3><span class="small muted">local + Neon records</span></div>
<div id="liveOrderDryRun"></div> <div id="liveOrderDryRun"></div>
<div id="liveAuditTrail"></div> <div id="liveAuditTrail"></div>
</div> </div>
@@ -616,8 +618,8 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
<div class="card"> <div class="card">
<div class="card-h"><h3>Server controls</h3><span class="small muted">live safety gates</span></div> <div class="card-h"><h3>Server controls</h3><span class="small muted">live safety gates</span></div>
<div class="locked-panel"> <div class="locked-panel">
<b>Production switch remains off</b> <b>Public production switch remains off</b>
<div class="small muted">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.</div> <div class="small muted">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.</div>
</div> </div>
</div> </div>
</div> </div>
@@ -720,7 +722,7 @@ function applyPersonalMode(){
if(sub)sub.textContent="private research · paper tracking · manual links"; if(sub)sub.textContent="private research · paper tracking · manual links";
const heroTitle=document.querySelector(".hero h1"),heroCopy=document.querySelector(".hero p"); const heroTitle=document.querySelector(".hero h1"),heroCopy=document.querySelector(".hero p");
if(heroTitle)heroTitle.innerHTML=`Your private agent lab for <span class="grad">manual market research</span>`; if(heroTitle)heroTitle.innerHTML=`Your private agent lab for <span class="grad">manual market research</span>`;
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)=>{ document.querySelectorAll(".hbadge").forEach((b,i)=>{
const labels=["Private research mode","Manual execution only","Cloud paper accounts","Live market links"]; const labels=["Private research mode","Manual execution only","Cloud paper accounts","Live market links"];
if(labels[i])b.textContent=labels[i]; if(labels[i])b.textContent=labels[i];
@@ -2331,19 +2333,18 @@ function renderAbout(){
<div class="fic">${a.emoji}</div><h4>${a.name}</h4><p>${agentBlurb(a,st)}</p></div>`).join(""); <div class="fic">${a.emoji}</div><h4>${a.name}</h4><p>${agentBlurb(a,st)}</p></div>`).join("");
} }
const LIVE_CHECKS=[ const LIVE_CHECKS=[
["legal","Terms, privacy policy, risk disclosures, and jurisdiction policy approved"], ["personal_scope","I will use only my own money and my own Polymarket/deposit wallet"],
["eligibility","KYC/KYB, age, sanctions, watchlist, and location screening active"], ["no_investors","No outside investors, pooled funds, user deposits, or copy-trading customers"],
["market_policy","Allowed market categories, restricted events, and manipulation rules defined"], ["eligibility","I personally satisfy the provider and Polymarket eligibility/location rules"],
["auth","Production accounts, sessions, password reset, MFA path, and sign-out implemented"], ["wallet","My own deposit wallet is created, controlled by me, and funded only by me"],
["wallet","User-controlled wallet/deposit-wallet signing and permission revocation implemented"], ["clob","Personal CLOB credentials are configured only for my own wallet"],
["payments","Deposits, withdrawals, webhooks, failed-payment handling, and support flows implemented"], ["manual_only","Every staged intent requires manual review before any order is placed"],
["clob","Signed order creation, CLOB submission, cancellation, retry, and fill tracking implemented"], ["reconciliation","I will compare cash, open orders, fills, and positions after each test"],
["reconciliation","Cash, open orders, positions, fills, fees, and withdrawals reconciled continuously"], ["risk","Personal max amount, per-agent caps, stop rules, and emergency pause are set"],
["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"], ["security","Secret management, encryption, rate limits, abuse controls, and security review completed"],
["records","Append-only audit logs, customer statements, exports, and retention policy active"], ["records","Append-only audit logs are recording profile changes and staged intents"],
["operations","Monitoring, incident response, customer support, and rollback plan ready"], ["operations","Incident alerting and emergency pause path are ready"],
["testing","Paper-to-live parity, dry-runs, test wallets, webhook tests, and edge-case QA completed"], ["testing","Tiny manual test trades are checked before increasing size"],
]; ];
function defaultLiveState(){ function defaultLiveState(){
const checks={};LIVE_CHECKS.forEach(([k])=>checks[k]=false); const checks={};LIVE_CHECKS.forEach(([k])=>checks[k]=false);
@@ -2386,10 +2387,12 @@ function renderLiveBackendStatus(){
const s=LIVE_BACKEND_STATUS; 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;} 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=>`<div class="leader-row"><span>${esc(p.label)}${p.expected?`<div class="small muted">${esc(p.expected)}</div>`:""}</span><b class="${p.configured?"pos-val":"neg-val"}">${p.configured?"configured":"missing"}</b></div>`).join(""); const providers=(s.providers||[]).map(p=>`<div class="leader-row"><span>${esc(p.label)}${p.expected?`<div class="small muted">${esc(p.expected)}</div>`:""}</span><b class="${p.configured?"pos-val":"neg-val"}">${p.configured?"configured":"missing"}</b></div>`).join("");
const personalReqs=(s.personal_requirements||[]).map(p=>`<div class="leader-row"><span>${esc(p.label)}${p.expected?`<div class="small muted">${esc(p.expected)}</div>`:""}</span><b class="${p.configured?"pos-val":"neg-val"}">${p.configured?"ready":"missing"}</b></div>`).join("");
const stack=(s.provider_stack||[]).map(p=>`<div class="leader-row"><span><b>${esc(p.provider)}</b><div class="small muted">${esc(p.role)}</div><div class="small muted">${(p.checks||[]).map(c=>`${esc(c.key)}: ${c.configured?"ok":"missing"}`).join(" · ")}</div></span><b class="${p.configured?"pos-val":"neg-val"}">${p.configured?"ready":"needs keys"}</b></div>`).join(""); const stack=(s.provider_stack||[]).map(p=>`<div class="leader-row"><span><b>${esc(p.provider)}</b><div class="small muted">${esc(p.role)}</div><div class="small muted">${(p.checks||[]).map(c=>`${esc(c.key)}: ${c.configured?"ok":"missing"}`).join(" · ")}</div></span><b class="${p.configured?"pos-val":"neg-val"}">${p.configured?"ready":"needs keys"}</b></div>`).join("");
const next=(s.next_required||[]).slice(0,12).map(x=>`<li>${esc(x)}</li>`).join(""); const next=(s.next_required||[]).slice(0,12).map(x=>`<li>${esc(x)}</li>`).join("");
const personalNext=(s.personal_next_required||[]).slice(0,12).map(x=>`<li>${esc(x)}</li>`).join("");
const reqs=(s.launch_requirements||[]).map(x=>`<li>${esc(x.label)}</li>`).join(""); const reqs=(s.launch_requirements||[]).map(x=>`<li>${esc(x.label)}</li>`).join("");
root.innerHTML=`<b>${s.live_trading_enabled?"Live trading unlocked":"Live trading locked"}</b><div class="small muted" style="margin:6px 0">${esc(s.locked_reason||"All required backend providers are configured.")}</div>${stack?`<div class="locked-panel" style="margin-top:10px"><b>Your provider stack</b>${stack}</div>`:""}${providers}${next?`<div class="locked-panel" style="margin-top:10px"><b>Next setup items</b><ul>${next}</ul></div>`:""}${reqs?`<div class="locked-panel" style="margin-top:10px"><b>Full launch requirements</b><ol>${reqs}</ol></div>`:""}<div class="small muted" style="margin-top:8px">Live execution stays disabled until every provider is configured, every launch requirement is complete, and final approval flips LIVE_TRADING_ENABLED=true.</div>`; root.innerHTML=`<b>${s.personal_trading_enabled?"Personal prerequisites ready":"Manual personal mode locked to staging"}</b><div class="small muted" style="margin:6px 0">${esc(s.locked_reason||"Public trading remains locked.")}</div>${personalReqs?`<div class="locked-panel" style="margin-top:10px"><b>Personal-only requirements</b>${personalReqs}</div>`:""}${personalNext?`<div class="locked-panel" style="margin-top:10px"><b>Before personal live execution</b><ul>${personalNext}</ul></div>`:""}${stack?`<div class="locked-panel" style="margin-top:10px"><b>Public provider stack</b>${stack}</div>`:""}${providers}${next?`<div class="locked-panel" style="margin-top:10px"><b>Public platform blockers</b><ul>${next}</ul></div>`:""}${reqs?`<div class="locked-panel" style="margin-top:10px"><b>Public launch requirements</b><ol>${reqs}</ol></div>`:""}<div class="small muted" style="margin-top:8px">This cockpit stages manual intents only. Public investor deposits and unattended agent trading stay off.</div>`;
if(hookRoot){ if(hookRoot){
const hooks=(s.webhooks||[]).map(h=>`<div class="leader-row"><span><b>${esc(h.provider)}</b><div class="small muted">${esc(h.label)}</div></span><a class="market-link" href="${esc(h.url)}" target="_blank" rel="noopener">${esc(h.url)}</a></div>`).join(""); const hooks=(s.webhooks||[]).map(h=>`<div class="leader-row"><span><b>${esc(h.provider)}</b><div class="small muted">${esc(h.label)}</div></span><a class="market-link" href="${esc(h.url)}" target="_blank" rel="noopener">${esc(h.url)}</a></div>`).join("");
hookRoot.innerHTML=hooks||"Webhook URLs will appear after backend readiness loads."; hookRoot.innerHTML=hooks||"Webhook URLs will appear after backend readiness loads.";
@@ -2411,24 +2414,27 @@ async function dryRunLiveOrderIntent(){
side:$("liveIntentSide").value, side:$("liveIntentSide").value,
max_amount:Number($("liveIntentAmount").value||0), max_amount:Number($("liveIntentAmount").value||0),
execution_mode:"manual_approval", execution_mode:"manual_approval",
account_scope:PERSONAL_MODE?"personal":"public_readiness",
}; };
try{ try{
const r=await fetch("/api/live",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({intent})}); const r=await fetch("/api/live",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({intent})});
const d=await r.json(); const d=await r.json();
$("liveOrderDryRun").innerHTML=`<div class="locked-panel"><b>Order intent dry-run</b><div class="small muted">${esc(d.error||"Accepted dry-run.")}</div><pre style="white-space:pre-wrap;font-size:11px;color:#cdd6e8">${esc(JSON.stringify(d.audit_event||d,null,2))}</pre></div>`; $("liveOrderDryRun").innerHTML=`<div class="locked-panel"><b>Order intent dry-run</b><div class="small muted">${esc(d.error||"Accepted dry-run.")}</div><pre style="white-space:pre-wrap;font-size:11px;color:#cdd6e8">${esc(JSON.stringify(d.audit_event||d,null,2))}</pre></div>`;
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){ }catch(e){
toast("Order dry-run failed."); toast("Order dry-run failed.");
} }
} }
async function saveRiskProfileToServer(s){ 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={ const body={
user_id:"local-readiness-user", user_id:"local-readiness-user",
wallet_address:s.walletAddress, wallet_address:s.walletAddress,
jurisdiction:s.jurisdiction, jurisdiction:s.jurisdiction,
deposit_limit:s.depositLimit, deposit_limit:s.depositLimit,
withdraw_reserve:s.withdrawReserve, 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 r=await fetch("/api/risk-profile",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)});
const d=await r.json(); const d=await r.json();
@@ -2473,7 +2479,7 @@ function renderLiveMoneyTab(){
<div><div class="invest-name">${esc(a.name)}</div><div class="invest-copy">${esc(a.blurb.slice(0,120))}${a.blurb.length>120?"...":""}</div></div> <div><div class="invest-name">${esc(a.name)}</div><div class="invest-copy">${esc(a.blurb.slice(0,120))}${a.blurb.length>120?"...":""}</div></div>
<div class="trade-actions"> <div class="trade-actions">
<label class="small muted"><input type="checkbox" data-live-agent="${a.id}" data-field="enabled" ${p.enabled?"checked":""}> enabled</label> <label class="small muted"><input type="checkbox" data-live-agent="${a.id}" data-field="enabled" ${p.enabled?"checked":""}> enabled</label>
<label class="small muted"><input type="checkbox" data-live-agent="${a.id}" data-field="autoTrade" ${p.autoTrade?"checked":""}> auto</label> <label class="small muted"><input type="checkbox" data-live-agent="${a.id}" data-field="autoTrade" ${p.autoTrade?"checked":""} disabled> auto unavailable</label>
<input class="input" type="number" min="0" step="100" value="${p.maxAllocation||""}" placeholder="Max $" data-live-agent="${a.id}" data-field="maxAllocation"> <input class="input" type="number" min="0" step="100" value="${p.maxAllocation||""}" placeholder="Max $" data-live-agent="${a.id}" data-field="maxAllocation">
<input class="input" type="number" min="1" max="100" step="1" value="${p.maxPositionPct||10}" placeholder="Max %" data-live-agent="${a.id}" data-field="maxPositionPct"> <input class="input" type="number" min="1" max="100" step="1" value="${p.maxPositionPct||10}" placeholder="Max %" data-live-agent="${a.id}" data-field="maxPositionPct">
</div> </div>
@@ -2490,7 +2496,8 @@ function esc(s){return String(s).replace(/[&<>"]/g,c=>({"&":"&amp;","<":"&lt;","
const TABS=["overview","leaderboard","suggestions","portfolio","invest","paper","live","about"]; const TABS=["overview","leaderboard","suggestions","portfolio","invest","paper","live","about"];
function showTab(name){ function showTab(name){
if(!TABS.includes(name))name="overview"; 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(".tabpanel").forEach(p=>p.classList.toggle("active",p.dataset.tab===name));
document.querySelectorAll(".tab").forEach(t=>t.classList.toggle("active",t.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); if(location.hash.slice(1)!==name)history.replaceState(null,"","#"+name);
+2 -2
View File
@@ -4,8 +4,8 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Personal Polymarket Arena</title> <title>Personal Polymarket Arena</title>
<meta http-equiv="refresh" content="0; url=/?personal=1#overview" /> <meta http-equiv="refresh" content="0; url=/?personal=1#live" />
<script>location.replace("/?personal=1#overview");</script> <script>location.replace("/?personal=1#live");</script>
</head> </head>
<body> <body>
<p>Opening personal research mode...</p> <p>Opening personal research mode...</p>