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.