Add personal capital tracker and market browser

This commit is contained in:
Theodore Song
2026-07-15 09:25:57 -04:00
parent 24d367b4a6
commit 4aee60c644
4 changed files with 356 additions and 3 deletions
+6
View File
@@ -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`.
+138
View File
@@ -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();
+54
View File
@@ -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) {
+158 -3
View File
@@ -572,6 +572,21 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
<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 class="card">
<div class="card-h"><h3>Personal capital & agent shares</h3><span class="small muted">synced tracker, no custody</span></div>
<div id="personalCapitalSummary"></div>
<div class="account-row">
<input class="input" id="personalCapitalAmount" type="number" min="1" step="25" placeholder="Amount" />
<select class="select" id="personalCapitalAgent"></select>
<button class="btn" id="personalDepositBtn">Record external deposit</button>
<button class="btn ghost" id="personalWithdrawBtn">Record withdrawal</button>
<button class="btn primary" id="personalBuyAgentBtn">Allocate to agent</button>
<button class="btn ghost" id="personalSellAgentBtn">Reduce agent</button>
</div>
<div class="risk-note" style="margin-top:12px">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.</div>
<div id="personalAgentAllocations" style="margin-top:12px"></div>
<div id="personalCapitalHistory" style="margin-top:12px"></div>
</div>
<div class="card">
<div class="card-h"><h3>Agent permissions & risk limits</h3><span class="small muted">manual approval only</span></div>
<div id="liveAgentPermissions"></div>
@@ -602,6 +617,18 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
<div id="liveAuditTrail"></div>
</div>
</div>
<div class="card">
<div class="card-h"><h3>Browse Polymarket for manual tickets</h3><span class="small muted">search active markets</span></div>
<div class="account-row">
<input class="input" id="liveSearchInput" placeholder="Search Polymarket markets" />
<button class="btn" id="liveSearchBtn">Search</button>
<button class="btn" id="liveAllBtn">All active</button>
<button class="btn ghost" id="liveMoreBtn">Load more</button>
<button class="btn ghost" id="liveClearSearchBtn">Clear</button>
</div>
<div class="filterbar" id="liveMarketFilterbar"></div>
<div class="trade-list" id="liveMarketList"></div>
</div>
<div class="card">
<div class="card-h"><h3>Manual trade ticket queue</h3><span class="small muted">AI suggests, you sign elsewhere</span></div>
<div id="personalTicketQueue"></div>
@@ -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=>`<div class="log-item"><span class="badge ${esc(f.action)}">${esc(f.action)}</span><span class="log-date">${new Date(f.filled_at).toLocaleString()}</span> — ${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))}</div>`).join(""):`<div class="empty">No manual fills yet.</div>`;
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=>`<option value="${esc(a.id)}">${a.emoji} ${esc(a.name)}</option>`).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=`<div class="invest-total">
<div class="invest-num"><div class="k">Tracked outside cash</div><div class="v">${fmtUSD(cash)}</div></div>
<div class="invest-num"><div class="k">Agent allocations</div><div class="v">${fmtUSD(allocated)}</div></div>
<div class="invest-num"><div class="k">Personal tracker total</div><div class="v">${fmtUSD(cash+allocated)}</div></div>
</div>`;
allocRoot.innerHTML=allocations.length?allocations.map(a=>{
const cfg=agentById(a.agent_id)||{name:a.agent_id,emoji:"",color:"#7c8cff"};
return `<div class="leader-row"><span><b>${cfg.emoji} ${esc(cfg.name)}</b><div class="small muted">bookkeeping allocation only</div></span><b>${fmtUSD(Number(a.amount||0))}</b></div>`;
}).join(""):`<div class="empty">No agent allocations yet. Record outside cash, then allocate any amount to an agent you want to shadow.</div>`;
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 `<div class="log-item"><span class="badge">${esc(e.action)}</span><span class="log-date">${new Date(e.created_at).toLocaleString()}</span> — ${fmtUSD(Number(e.amount||0))}${cfg?` · ${cfg.emoji} ${esc(cfg.name)}`:""}</div>`;
}).join(""):`<div class="empty">No personal tracker activity yet.</div>`;
}
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=>`<span class="chip ${focus===c?"active":""}" data-live-cat="${c}">${c}<span class="ct">${counts[c]||0}</span></span>`).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=`<div class="empty">Search Polymarket or load active markets to stage manual tickets from the cockpit.</div>`;return;}
root.innerHTML=shown.map(s=>`<div class="trade-card">
<div class="trade-head"><div><div class="trade-title"><a class="market-title" href="${esc(s.url||marketSearchUrl(s.question))}" target="_blank" rel="noopener">${esc(s.question)}</a></div>
<div class="trade-meta"><span>${esc(s.category)}</span><span>YES ${pct(s.yes_price)}</span><span>NO ${pct(s.no_price)}</span><span>${Math.round(s.conviction||0)} conviction</span></div></div>
<a class="btn ghost" href="${esc(s.url||marketSearchUrl(s.question))}" target="_blank" rel="noopener">Open Polymarket</a></div>
<div class="rationale">${esc(s.rationale||"Active Polymarket market available for manual review.")}</div>
<div class="trade-actions">
<select class="select" data-live-side="${esc(s.market_id)}"><option>YES</option><option>NO</option></select>
<input class="input" type="number" min="1" step="25" value="100" data-live-amount="${esc(s.market_id)}" aria-label="Manual amount for ${esc(s.question)}">
<input class="input" type="number" min="0.01" max="0.99" step="0.01" value="${Number(s.entry_price||s.yes_price||0).toFixed(2)}" data-live-price="${esc(s.market_id)}" aria-label="Limit price for ${esc(s.question)}">
<button class="btn primary" data-live-ticket="${esc(s.market_id)}">Stage manual ticket</button>
</div>
</div>`).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=>`<div class="log-item"><span class="log-date">${new Date(x.at).toLocaleString()}</span> — ${esc(x.detail)}</div>`).join(""):`<div class="empty">No readiness changes logged yet.</div>`;
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",()=>{