mirror of
https://github.com/theodore-song/polymarket-analyst.git
synced 2026-07-27 16:07:46 +00:00
Scaffold live money backend readiness
This commit is contained in:
@@ -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.
|
||||
+95
@@ -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" });
|
||||
}
|
||||
+51
@@ -569,6 +569,14 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
|
||||
<div class="grid2">
|
||||
<div class="card">
|
||||
<div class="card-h"><h3>Execution console</h3><span class="small muted">locked until backend exists</span></div>
|
||||
<div id="liveBackendStatus" class="locked-panel">Checking backend readiness...</div>
|
||||
<div class="account-row">
|
||||
<select class="select" id="liveIntentAgent"></select>
|
||||
<input class="input" id="liveIntentMarket" placeholder="Market ID" />
|
||||
<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" />
|
||||
<button class="btn" id="dryRunOrderBtn">Dry-run order intent</button>
|
||||
</div>
|
||||
<ol class="steps">
|
||||
<li><b>Wallet signature</b> — user signs EIP-712 orders; the site never signs unauthorized trades.</li>
|
||||
<li><b>CLOB router</b> — backend submits, cancels, and tracks orders through authenticated Polymarket APIs.</li>
|
||||
@@ -578,6 +586,7 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-h"><h3>Audit trail preview</h3><span class="small muted">required for real money</span></div>
|
||||
<div id="liveOrderDryRun"></div>
|
||||
<div id="liveAuditTrail"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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=>`<div class="leader-row"><span>${esc(p.label)}</span><b class="${p.configured?"pos-val":"neg-val"}">${p.configured?"configured":"missing"}</b></div>`).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>${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=`<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}`);
|
||||
}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=>`<option value="${a.id}">${a.emoji} ${esc(a.name)}</option>`).join("");
|
||||
renderLiveBackendStatus();
|
||||
$("liveAgentPermissions").innerHTML=AGENTS.map(a=>{const p=s.agents[a.id];
|
||||
return `<div class="invest-agent" style="border-left:3px solid ${a.color};padding-left:12px">
|
||||
<div class="lb-emoji">${a.emoji}</div>
|
||||
@@ -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){
|
||||
|
||||
Reference in New Issue
Block a user