diff --git a/REAL_MONEY_ROADMAP.md b/REAL_MONEY_ROADMAP.md index 1e63b00..bbb50fa 100644 --- a/REAL_MONEY_ROADMAP.md +++ b/REAL_MONEY_ROADMAP.md @@ -17,6 +17,9 @@ wallet, your own money, manual approval, and audit logging. - `/api/live` manual-fill actions record trades that you placed yourself on Polymarket, then update tracked real positions and P&L. These records are for reconciliation only: Poly Arena still does not sign or submit the trade. +- `/api/live` personal-capital actions track money you control outside Poly + Arena and bookkeeping allocations to agent return streams. This is not a + deposit system, custody account, public investment product, or order router. - `/api/accounts` creates, logs into, and saves password-backed paper accounts through Vercel Blob storage. - `/api/config` exposes safe public provider configuration, including the @@ -53,6 +56,9 @@ Personal mode is narrower than the public business launch: you do anything in Polymarket. - Trade execution happens outside Poly Arena through your own Polymarket wallet or UI. Poly Arena remains the analyst/ticketing layer. +- Personal agent shares are allocation notes for your own tracking only. They + do not represent pooled investor shares, securities, or money held by Poly + Arena. - After you manually trade, record the actual shares, price, fees, and note in Poly Arena so the personal cockpit can track real position value and P&L. - Public `LIVE_TRADING_ENABLED` stays `false`. diff --git a/api/_db.js b/api/_db.js index 7349a8f..6fb39ea 100644 --- a/api/_db.js +++ b/api/_db.js @@ -145,6 +145,41 @@ export async function ensureSchema() { ) `; + await db` + create table if not exists personal_capital_accounts ( + user_id text primary key, + cash numeric not null default 0, + updated_at timestamptz not null default now() + ) + `; + + await db` + create table if not exists personal_agent_allocations ( + user_id text not null, + agent_id text not null, + amount numeric not null default 0, + updated_at timestamptz not null default now(), + primary key (user_id, agent_id) + ) + `; + + await db` + create table if not exists personal_capital_events ( + id bigserial primary key, + user_id text not null, + action text not null, + agent_id text, + amount numeric not null, + note text, + created_at timestamptz not null default now() + ) + `; + + await db` + create index if not exists personal_capital_events_user_idx + on personal_capital_events (user_id, created_at desc) + `; + schemaReady = true; } @@ -472,6 +507,109 @@ export async function listRealPortfolio(userId) { return { positions, fills }; } +export async function listPersonalCapital(userId) { + await ensureSchema(); + const db = sql(); + await db` + insert into personal_capital_accounts (user_id, cash) + values (${userId}, 0) + on conflict (user_id) do nothing + `; + const accounts = await db` + select * + from personal_capital_accounts + where user_id = ${userId} + limit 1 + `; + const allocations = await db` + select * + from personal_agent_allocations + where user_id = ${userId} and amount > 0 + order by updated_at desc + `; + const events = await db` + select * + from personal_capital_events + where user_id = ${userId} + order by created_at desc + limit 100 + `; + return { account: accounts[0] || { user_id: userId, cash: 0 }, allocations, events }; +} + +export async function recordPersonalCapitalAction(action) { + await ensureSchema(); + const db = sql(); + const userId = action.userId; + const type = String(action.action || "").toUpperCase(); + const agentId = action.agentId || null; + const amount = Number(action.amount || 0); + + await db` + insert into personal_capital_accounts (user_id, cash) + values (${userId}, 0) + on conflict (user_id) do nothing + `; + + const accounts = await db` + select * + from personal_capital_accounts + where user_id = ${userId} + limit 1 + `; + const account = accounts[0] || { cash: 0 }; + let nextCash = Number(account.cash || 0); + + if (type === "DEPOSIT") { + nextCash += amount; + } else if (type === "WITHDRAW") { + nextCash -= amount; + } else if (type === "BUY_AGENT") { + nextCash -= amount; + await db` + insert into personal_agent_allocations (user_id, agent_id, amount, updated_at) + values (${userId}, ${agentId}, ${amount}, now()) + on conflict (user_id, agent_id) do update set + amount = personal_agent_allocations.amount + excluded.amount, + updated_at = now() + `; + } else if (type === "SELL_AGENT") { + const rows = await db` + select * + from personal_agent_allocations + where user_id = ${userId} and agent_id = ${agentId} + limit 1 + `; + const current = rows[0] ? Number(rows[0].amount || 0) : 0; + const reduction = Math.min(amount, current); + nextCash += reduction; + await db` + insert into personal_agent_allocations (user_id, agent_id, amount, updated_at) + values (${userId}, ${agentId}, ${Math.max(0, current - reduction)}, now()) + on conflict (user_id, agent_id) do update set + amount = excluded.amount, + updated_at = now() + `; + } + + if (nextCash < -0.000001) { + throw new Error("Tracked personal cash is too low for that action"); + } + + await db` + update personal_capital_accounts + set cash = ${nextCash}, updated_at = now() + where user_id = ${userId} + `; + + await db` + insert into personal_capital_events (user_id, action, agent_id, amount, note) + values (${userId}, ${type}, ${agentId}, ${amount}, ${action.note || null}) + `; + + return listPersonalCapital(userId); +} + export async function providerEventSummary() { await ensureSchema(); const db = sql(); diff --git a/api/live.js b/api/live.js index bfdd03c..b9bdb29 100644 --- a/api/live.js +++ b/api/live.js @@ -1,8 +1,10 @@ import { createTradeTicket, + listPersonalCapital, listRealPortfolio, listTradeTickets, recordAuditEvent, + recordPersonalCapitalAction, recordRealFill, updateRealPositionMark, updateTradeTicketStatus, @@ -242,6 +244,7 @@ function validateIntent(intent) { const VALID_TICKET_STATUSES = new Set(["staged", "reviewed", "placed_manually", "skipped", "cancelled"]); const VALID_FILL_ACTIONS = new Set(["BUY", "SELL"]); +const VALID_CAPITAL_ACTIONS = new Set(["DEPOSIT", "WITHDRAW", "BUY_AGENT", "SELL_AGENT"]); function marketSearchUrl(question) { return `https://polymarket.com/search?query=${encodeURIComponent(String(question || ""))}`; @@ -319,6 +322,25 @@ function validateFill(fill) { return errors; } +function normalizeCapitalAction(body) { + return { + userId: String(body.user_id || "local-readiness-user").trim(), + action: String(body.capital_action || body.capitalAction || "").trim().toUpperCase(), + agentId: String(body.agent_id || "").trim(), + amount: Number(body.amount || 0), + note: String(body.note || "").trim(), + }; +} + +function validateCapitalAction(action) { + const errors = []; + if (!action.userId) errors.push("user_id"); + if (!VALID_CAPITAL_ACTIONS.has(action.action)) errors.push("capital_action must be DEPOSIT, WITHDRAW, BUY_AGENT, or SELL_AGENT"); + if ((action.action === "BUY_AGENT" || action.action === "SELL_AGENT") && !action.agentId) errors.push("agent_id"); + if (!Number.isFinite(action.amount) || action.amount <= 0) errors.push("amount must be positive"); + return errors; +} + export default async function handler(req, res) { res.setHeader("Cache-Control", "no-store"); @@ -333,6 +355,11 @@ export default async function handler(req, res) { const portfolio = await listRealPortfolio(userId); return res.status(200).json({ ok: true, ...portfolio }); } + if (req.query?.action === "personal_capital") { + const userId = String(req.query?.user_id || "local-readiness-user").trim(); + const capital = await listPersonalCapital(userId); + return res.status(200).json({ ok: true, ...capital }); + } return res.status(200).json(baseStatus()); } @@ -410,6 +437,33 @@ export default async function handler(req, res) { return res.status(200).json({ ok: true, position }); } + if (body.action === "capital_action") { + const capitalAction = normalizeCapitalAction(body); + const capitalErrors = validateCapitalAction(capitalAction); + if (capitalErrors.length) return res.status(400).json({ ok: false, error: "Invalid personal capital action", details: capitalErrors }); + let capital; + try { + capital = await recordPersonalCapitalAction(capitalAction); + } catch (err) { + return res.status(400).json({ ok: false, error: err?.message || "Personal capital tracker update failed" }); + } + await recordAuditEvent("PERSONAL_CAPITAL_ACTION", { + user_id: capitalAction.userId, + action: capitalAction.action, + agent_id: capitalAction.agentId, + amount: capitalAction.amount, + custody_by_site: false, + real_order_placed_by_site: false, + }); + return res.status(200).json({ + ok: true, + ...capital, + custody_by_site: false, + real_order_placed_by_site: false, + note: "Personal capital tracker updated for bookkeeping only. Poly Arena did not receive funds or place an order.", + }); + } + const intent = body.intent || body; const errors = validateIntent(intent); if (errors.length) { diff --git a/index.html b/index.html index 311fdaf..add6605 100644 --- a/index.html +++ b/index.html @@ -572,6 +572,21 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
Fund only your own wallet directly through approved provider flows. This site will not accept, hold, or withdraw money for other people.
+
+

Personal capital & agent shares

synced tracker, no custody
+
+
+ + + + + + +
+
This tracks money you personally control outside Poly Arena. Agent shares are bookkeeping allocations only, not securities, pooled deposits, or orders placed by the site.
+
+
+

Agent permissions & risk limits

manual approval only
@@ -602,6 +617,18 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
+
+

Browse Polymarket for manual tickets

search active markets
+
+ + + + + +
+
+
+

Manual trade ticket queue

AI suggests, you sign elsewhere
@@ -725,10 +752,14 @@ const CLOB = "https://clob.polymarket.com"; let PAPER_SEARCH_RESULTS = null; let PAPER_MARKET_OFFSET = 0; let PAPER_MARKET_QUERY = ""; +let LIVE_SEARCH_RESULTS = []; +let LIVE_MARKET_OFFSET = 0; +let LIVE_MARKET_QUERY = ""; let LIVE_BACKEND_STATUS = null; let LIVE_POLICY_STATUS = null; let PERSONAL_TICKETS = []; let REAL_PORTFOLIO = {positions:[],fills:[]}; +let PERSONAL_CAPITAL = {account:{cash:0},allocations:[],events:[]}; let PROVIDER_CONFIG = null; const catColor = (c) => CAT_COLORS[c] || CAT_COLORS.Other; const getFocus = () => localStorage.getItem(FOCUS_KEY) || "All"; @@ -2565,16 +2596,19 @@ async function stageSuggestionTicket(marketId){ async function loadTradeTickets(){ if(!PERSONAL_MODE)return; try{ - const [ticketRes,portfolioRes]=await Promise.all([ + const [ticketRes,portfolioRes,capitalRes]=await Promise.all([ fetch(`/api/live?action=tickets&user_id=${encodeURIComponent(PERSONAL_USER_ID)}&limit=30`,{cache:"no-store"}), fetch(`/api/live?action=real_portfolio&user_id=${encodeURIComponent(PERSONAL_USER_ID)}`,{cache:"no-store"}), + fetch(`/api/live?action=personal_capital&user_id=${encodeURIComponent(PERSONAL_USER_ID)}`,{cache:"no-store"}), ]); - const d=await ticketRes.json(),pf=await portfolioRes.json(); + const d=await ticketRes.json(),pf=await portfolioRes.json(),cap=await capitalRes.json(); PERSONAL_TICKETS=d.tickets||[]; REAL_PORTFOLIO={positions:pf.positions||[],fills:pf.fills||[]}; - }catch(e){PERSONAL_TICKETS=[];REAL_PORTFOLIO={positions:[],fills:[]};} + PERSONAL_CAPITAL={account:cap.account||{cash:0},allocations:cap.allocations||[],events:cap.events||[]}; + }catch(e){PERSONAL_TICKETS=[];REAL_PORTFOLIO={positions:[],fills:[]};PERSONAL_CAPITAL={account:{cash:0},allocations:[],events:[]};} renderTradeTickets(); renderRealPortfolio(); + renderPersonalCapital(); } async function updateTicketStatus(ticketId,status){ try{ @@ -2682,6 +2716,117 @@ function renderRealPortfolio(){ fillsRoot.innerHTML=fills.length?fills.slice(0,25).map(f=>`
${esc(f.action)}${new Date(f.filled_at).toLocaleString()} — ${Number(f.shares||0).toFixed(2)} ${esc(f.side)} @ ${Math.round(Number(f.price||0)*100)}¢ on ${esc((f.question||f.market_id||"").slice(0,52))}
`).join(""):`
No manual fills yet.
`; document.querySelectorAll("[data-update-mark]").forEach(btn=>btn.onclick=()=>updateRealMark(btn.dataset.updateMark)); } +async function personalCapitalAction(capitalAction){ + const amount=Number(($("personalCapitalAmount")&&$("personalCapitalAmount").value)||0); + const agentId=($("personalCapitalAgent")&&$("personalCapitalAgent").value)||""; + try{ + const r=await fetch("/api/live",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({ + action:"capital_action", + user_id:PERSONAL_USER_ID, + capital_action:capitalAction, + agent_id:agentId, + amount, + note:"Personal cockpit bookkeeping action", + })}); + const d=await r.json(); + if(!r.ok)throw new Error(d.error||"Capital tracker update failed"); + PERSONAL_CAPITAL={account:d.account||{cash:0},allocations:d.allocations||[],events:d.events||[]}; + renderPersonalCapital(); + toast("Personal tracker updated."); + }catch(e){toast(e.message||"Capital tracker update failed.");} +} +function renderPersonalCapital(){ + const summary=$("personalCapitalSummary"),allocRoot=$("personalAgentAllocations"),histRoot=$("personalCapitalHistory"),agentSel=$("personalCapitalAgent"); + if(!summary||!allocRoot||!histRoot)return; + if(agentSel)agentSel.innerHTML=AGENTS.map(a=>``).join(""); + const cash=Number(PERSONAL_CAPITAL.account&&PERSONAL_CAPITAL.account.cash||0); + const allocations=PERSONAL_CAPITAL.allocations||[]; + const allocated=allocations.reduce((s,a)=>s+Number(a.amount||0),0); + summary.innerHTML=`
+
Tracked outside cash
${fmtUSD(cash)}
+
Agent allocations
${fmtUSD(allocated)}
+
Personal tracker total
${fmtUSD(cash+allocated)}
+
`; + allocRoot.innerHTML=allocations.length?allocations.map(a=>{ + const cfg=agentById(a.agent_id)||{name:a.agent_id,emoji:"",color:"#7c8cff"}; + return `
${cfg.emoji} ${esc(cfg.name)}
bookkeeping allocation only
${fmtUSD(Number(a.amount||0))}
`; + }).join(""):`
No agent allocations yet. Record outside cash, then allocate any amount to an agent you want to shadow.
`; + const events=(PERSONAL_CAPITAL.events||[]).slice(0,20); + histRoot.innerHTML=events.length?events.map(e=>{ + const cfg=e.agent_id?agentById(e.agent_id):null; + return `
${esc(e.action)}${new Date(e.created_at).toLocaleString()} — ${fmtUSD(Number(e.amount||0))}${cfg?` · ${cfg.emoji} ${esc(cfg.name)}`:""}
`; + }).join(""):`
No personal tracker activity yet.
`; +} +function currentLiveMarkets(){return LIVE_SEARCH_RESULTS||[];} +function renderLiveMarkets(){ + const root=$("liveMarketList"),filter=$("liveMarketFilterbar");if(!root)return; + const all=currentLiveMarkets(),focus=getFocus(); + if(filter){ + const counts={};CATEGORIES.forEach(c=>counts[c]=0);all.forEach(s=>{counts[counts[s.category]!==undefined?s.category:"Other"]++;});counts.All=all.length; + filter.innerHTML=CATEGORIES.map(c=>`${c}${counts[c]||0}`).join(""); + document.querySelectorAll("[data-live-cat]").forEach(el=>el.onclick=()=>{setFocus(el.dataset.liveCat);renderLiveMarkets();}); + } + const shown=all.filter(s=>focus==="All"||!focus||s.category===focus); + if(!shown.length){root.innerHTML=`
Search Polymarket or load active markets to stage manual tickets from the cockpit.
`;return;} + root.innerHTML=shown.map(s=>`
+
+
${esc(s.category)}YES ${pct(s.yes_price)}NO ${pct(s.no_price)}${Math.round(s.conviction||0)} conviction
+ Open Polymarket
+
${esc(s.rationale||"Active Polymarket market available for manual review.")}
+
+ + + + +
+
`).join(""); + document.querySelectorAll("[data-live-ticket]").forEach(btn=>btn.onclick=()=>stageLiveMarketTicket(btn.dataset.liveTicket)); +} +async function searchLiveMarkets(){ + const q=($("liveSearchInput").value||"").trim(); + if(!q)return toast("Enter a Polymarket search."); + LIVE_MARKET_OFFSET=0;LIVE_MARKET_QUERY=q; + await loadLiveMarketPage(true); +} +async function loadLiveMarketPage(reset=false){ + const q=LIVE_MARKET_QUERY||(($("liveSearchInput")&&$("liveSearchInput").value)||"").trim(); + setStatus("searching Polymarket...",true); + try{ + if(reset){LIVE_SEARCH_RESULTS=[];LIVE_MARKET_OFFSET=0;} + const params=new URLSearchParams({closed:"false",active:"true",archived:"false",include_tag:"true",limit:"100",offset:String(LIVE_MARKET_OFFSET),order:"volume24hr",ascending:"false"}); + if(q)params.set("search",q); + const r=await fetch(`${GAMMA}/markets?${params}`); + if(!r.ok)throw new Error("market load failed"); + const raw=await r.json(); + const next=raw.map(normalizeMarket).filter(Boolean).map(paperTradeFromMarket); + LIVE_SEARCH_RESULTS=(LIVE_SEARCH_RESULTS||[]).concat(next); + LIVE_MARKET_OFFSET+=raw.length; + renderLiveMarkets();setStatus("up to date",false); + toast(next.length?`Loaded ${LIVE_SEARCH_RESULTS.length} active markets.`:"No more active markets found."); + }catch(e){setStatus("market load error",false);toast("Polymarket market load failed.");} +} +async function stageLiveMarketTicket(marketId){ + const s=currentLiveMarkets().find(x=>String(x.market_id)===String(marketId)); + if(!s)return toast("Market not found."); + const side=(document.querySelector(`[data-live-side="${marketId}"]`)||{}).value||"YES"; + const amount=Number((document.querySelector(`[data-live-amount="${marketId}"]`)||{}).value||0); + const price=Number((document.querySelector(`[data-live-price="${marketId}"]`)||{}).value||0); + try{ + await createTradeTicket({ + user_id:PERSONAL_USER_ID, + agent_id:($("liveIntentAgent")&&$("liveIntentAgent").value)||localStorage.getItem(VIEW_KEY)||"value", + market_id:s.market_id, + question:s.question, + market_url:s.url, + side, + max_amount:amount, + limit_price:price, + rationale:`Manually staged from the personal Polymarket browser. ${s.rationale||""}`.trim(), + }); + await loadTradeTickets(); + toast("Manual ticket staged. Open Polymarket to place it yourself."); + }catch(e){toast(e.message||"Ticket staging failed.");} +} async function saveRiskProfileToServer(s){ const permissions=JSON.parse(JSON.stringify(s.agents||{})); Object.values(permissions).forEach(p=>{p.autoTrade=false;p.manualApprove=true;}); @@ -2744,6 +2889,8 @@ function renderLiveMoneyTab(){ }).join(""); const audit=s.audit.slice(-10).reverse(); $("liveAuditTrail").innerHTML=audit.length?audit.map(x=>`
${new Date(x.at).toLocaleString()} — ${esc(x.detail)}
`).join(""):`
No readiness changes logged yet.
`; + renderPersonalCapital(); + renderLiveMarkets(); document.querySelectorAll("[data-live-check]").forEach(el=>el.onchange=()=>{const next=loadLiveState();next.checks[el.dataset.liveCheck]=el.checked;saveLiveState(next,`${el.checked?"Completed":"Reopened"}: ${LIVE_CHECKS.find(([k])=>k===el.dataset.liveCheck)[1]}`);renderLiveMoneyTab();}); document.querySelectorAll("[data-live-agent]").forEach(el=>el.onchange=()=>{const next=loadLiveState(),p=next.agents[el.dataset.liveAgent],field=el.dataset.field;p[field]=el.type==="checkbox"?el.checked:Number(el.value||0);saveLiveState(next,`Updated ${agentById(el.dataset.liveAgent).name} permission: ${field}`);renderLiveMoneyTab();}); } @@ -2874,6 +3021,14 @@ $("saveLiveProfileBtn").addEventListener("click",async()=>{ } }); $("dryRunOrderBtn").addEventListener("click",()=>dryRunLiveOrderIntent()); +$("personalDepositBtn").addEventListener("click",()=>personalCapitalAction("DEPOSIT")); +$("personalWithdrawBtn").addEventListener("click",()=>personalCapitalAction("WITHDRAW")); +$("personalBuyAgentBtn").addEventListener("click",()=>personalCapitalAction("BUY_AGENT")); +$("personalSellAgentBtn").addEventListener("click",()=>personalCapitalAction("SELL_AGENT")); +$("liveSearchBtn").addEventListener("click",()=>searchLiveMarkets()); +$("liveAllBtn").addEventListener("click",()=>{LIVE_MARKET_QUERY="";LIVE_SEARCH_RESULTS=[];loadLiveMarketPage(true);}); +$("liveMoreBtn").addEventListener("click",()=>loadLiveMarketPage(false)); +$("liveClearSearchBtn").addEventListener("click",()=>{LIVE_SEARCH_RESULTS=[];LIVE_MARKET_OFFSET=0;LIVE_MARKET_QUERY="";renderLiveMarkets();}); $("recordConsentBtn").addEventListener("click",()=>recordLiveConsent()); window.addEventListener("focus",()=>refreshFromCloudAndRender(true)); document.addEventListener("visibilitychange",()=>{