mirror of
https://github.com/theodore-song/polymarket-analyst.git
synced 2026-07-27 16:07:46 +00:00
Add non-custodial manual trade tickets
This commit is contained in:
+3
-2
@@ -7,8 +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
|
# Personal-only readiness gate. Keep false until your own wallet/CLOB
|
||||||
# are configured and tiny manual test orders are reviewed.
|
# credentials are configured and tiny manual test orders are reviewed.
|
||||||
|
# This does not let the AI or server hold private keys.
|
||||||
PERSONAL_TRADING_ENABLED=false
|
PERSONAL_TRADING_ENABLED=false
|
||||||
|
|
||||||
# Accounts / authentication
|
# Accounts / authentication
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ wallet, your own money, manual approval, and audit logging.
|
|||||||
- `/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 public order intents only as dry-runs and returns an
|
- `/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.
|
audit event. Personal-mode intents are staged for manual review, not executed.
|
||||||
|
- `/api/trade-tickets` stores non-custodial manual trade tickets. These tickets
|
||||||
|
contain the market, side, amount, limit, rationale, Polymarket link, and
|
||||||
|
review instructions, but never a private key or signed order.
|
||||||
- `/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
|
||||||
@@ -44,6 +47,8 @@ Personal mode is narrower than the public business launch:
|
|||||||
- No unattended agent trading.
|
- No unattended agent trading.
|
||||||
- Every live trade starts as a staged manual intent and must be reviewed before
|
- Every live trade starts as a staged manual intent and must be reviewed before
|
||||||
you do anything in Polymarket.
|
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.
|
||||||
- Public `LIVE_TRADING_ENABLED` stays `false`.
|
- Public `LIVE_TRADING_ENABLED` stays `false`.
|
||||||
|
|
||||||
Personal mode still needs:
|
Personal mode still needs:
|
||||||
|
|||||||
+82
@@ -77,6 +77,30 @@ export async function ensureSchema() {
|
|||||||
)
|
)
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
await db`
|
||||||
|
create table if not exists trade_tickets (
|
||||||
|
id bigserial primary key,
|
||||||
|
user_id text not null,
|
||||||
|
agent_id text not null,
|
||||||
|
market_id text not null,
|
||||||
|
question text,
|
||||||
|
market_url text,
|
||||||
|
side text not null,
|
||||||
|
max_amount numeric not null,
|
||||||
|
limit_price numeric,
|
||||||
|
rationale text,
|
||||||
|
status text not null default 'staged',
|
||||||
|
instructions jsonb not null default '{}'::jsonb,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now()
|
||||||
|
)
|
||||||
|
`;
|
||||||
|
|
||||||
|
await db`
|
||||||
|
create index if not exists trade_tickets_user_status_idx
|
||||||
|
on trade_tickets (user_id, status, created_at desc)
|
||||||
|
`;
|
||||||
|
|
||||||
schemaReady = true;
|
schemaReady = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,6 +218,64 @@ export async function latestRiskProfile(userId) {
|
|||||||
return rows[0] || null;
|
return rows[0] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createTradeTicket(ticket) {
|
||||||
|
await ensureSchema();
|
||||||
|
const db = sql();
|
||||||
|
const rows = await db`
|
||||||
|
insert into trade_tickets (
|
||||||
|
user_id,
|
||||||
|
agent_id,
|
||||||
|
market_id,
|
||||||
|
question,
|
||||||
|
market_url,
|
||||||
|
side,
|
||||||
|
max_amount,
|
||||||
|
limit_price,
|
||||||
|
rationale,
|
||||||
|
status,
|
||||||
|
instructions
|
||||||
|
) values (
|
||||||
|
${ticket.userId},
|
||||||
|
${ticket.agentId},
|
||||||
|
${ticket.marketId},
|
||||||
|
${ticket.question || null},
|
||||||
|
${ticket.marketUrl || null},
|
||||||
|
${ticket.side},
|
||||||
|
${Number(ticket.maxAmount || 0)},
|
||||||
|
${ticket.limitPrice == null ? null : Number(ticket.limitPrice)},
|
||||||
|
${ticket.rationale || null},
|
||||||
|
${ticket.status || "staged"},
|
||||||
|
${JSON.stringify(ticket.instructions || {})}::jsonb
|
||||||
|
)
|
||||||
|
returning *
|
||||||
|
`;
|
||||||
|
return rows[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listTradeTickets(userId, limit = 50) {
|
||||||
|
await ensureSchema();
|
||||||
|
const db = sql();
|
||||||
|
return db`
|
||||||
|
select *
|
||||||
|
from trade_tickets
|
||||||
|
where user_id = ${userId}
|
||||||
|
order by created_at desc
|
||||||
|
limit ${Math.max(1, Math.min(Number(limit) || 50, 100))}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateTradeTicketStatus(userId, ticketId, status) {
|
||||||
|
await ensureSchema();
|
||||||
|
const db = sql();
|
||||||
|
const rows = await db`
|
||||||
|
update trade_tickets
|
||||||
|
set status = ${status}, updated_at = now()
|
||||||
|
where user_id = ${userId} and id = ${Number(ticketId)}
|
||||||
|
returning *
|
||||||
|
`;
|
||||||
|
return rows[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
export async function providerEventSummary() {
|
export async function providerEventSummary() {
|
||||||
await ensureSchema();
|
await ensureSchema();
|
||||||
const db = sql();
|
const db = sql();
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { createTradeTicket, listTradeTickets, recordAuditEvent, updateTradeTicketStatus } from "./_db.js";
|
||||||
|
|
||||||
|
const VALID_STATUSES = new Set(["staged", "reviewed", "placed_manually", "skipped", "cancelled"]);
|
||||||
|
|
||||||
|
function marketSearchUrl(question) {
|
||||||
|
return `https://polymarket.com/search?query=${encodeURIComponent(String(question || ""))}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildInstructions(ticket) {
|
||||||
|
const marketUrl = ticket.marketUrl || marketSearchUrl(ticket.question || ticket.marketId);
|
||||||
|
return {
|
||||||
|
warning: "Manual review only. The AI/site does not hold a private key and does not submit orders.",
|
||||||
|
steps: [
|
||||||
|
"Open the Polymarket market link.",
|
||||||
|
"Confirm the exact market, side, current price, liquidity, and resolution terms.",
|
||||||
|
"Keep the order at or below the staged limit price and max amount.",
|
||||||
|
"Sign or place the order only from your own wallet/account.",
|
||||||
|
"Return here and mark the ticket as placed manually or skipped.",
|
||||||
|
],
|
||||||
|
market_url: marketUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTicket(body) {
|
||||||
|
return {
|
||||||
|
userId: String(body.user_id || "local-readiness-user").trim(),
|
||||||
|
agentId: String(body.agent_id || "").trim(),
|
||||||
|
marketId: String(body.market_id || "").trim(),
|
||||||
|
question: String(body.question || "").trim(),
|
||||||
|
marketUrl: String(body.market_url || "").trim(),
|
||||||
|
side: String(body.side || "").trim().toUpperCase(),
|
||||||
|
maxAmount: Number(body.max_amount || 0),
|
||||||
|
limitPrice: body.limit_price === undefined || body.limit_price === "" ? null : Number(body.limit_price),
|
||||||
|
rationale: String(body.rationale || "").trim(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function validate(ticket) {
|
||||||
|
const errors = [];
|
||||||
|
if (!ticket.userId) errors.push("user_id");
|
||||||
|
if (!ticket.agentId) errors.push("agent_id");
|
||||||
|
if (!ticket.marketId) errors.push("market_id");
|
||||||
|
if (!["YES", "NO"].includes(ticket.side)) errors.push("side must be YES or NO");
|
||||||
|
if (!Number.isFinite(ticket.maxAmount) || ticket.maxAmount <= 0) errors.push("max_amount must be positive");
|
||||||
|
if (ticket.limitPrice != null && (!Number.isFinite(ticket.limitPrice) || ticket.limitPrice <= 0 || ticket.limitPrice >= 1)) {
|
||||||
|
errors.push("limit_price must be between 0 and 1");
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function handler(req, res) {
|
||||||
|
res.setHeader("Cache-Control", "no-store");
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (req.method === "GET") {
|
||||||
|
const userId = String(req.query?.user_id || "local-readiness-user").trim();
|
||||||
|
const tickets = await listTradeTickets(userId, req.query?.limit || 50);
|
||||||
|
return res.status(200).json({ ok: true, tickets });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "POST") {
|
||||||
|
const body = typeof req.body === "string" ? JSON.parse(req.body || "{}") : (req.body || {});
|
||||||
|
if (body.action === "status") {
|
||||||
|
const userId = String(body.user_id || "local-readiness-user").trim();
|
||||||
|
const status = String(body.status || "").trim();
|
||||||
|
if (!VALID_STATUSES.has(status)) return res.status(400).json({ ok: false, error: "Invalid ticket status" });
|
||||||
|
const ticket = await updateTradeTicketStatus(userId, body.ticket_id, status);
|
||||||
|
if (!ticket) return res.status(404).json({ ok: false, error: "Ticket not found" });
|
||||||
|
await recordAuditEvent("TRADE_TICKET_STATUS_UPDATED", { user_id: userId, ticket_id: ticket.id, status });
|
||||||
|
return res.status(200).json({ ok: true, ticket });
|
||||||
|
}
|
||||||
|
|
||||||
|
const ticket = normalizeTicket(body);
|
||||||
|
const errors = validate(ticket);
|
||||||
|
if (errors.length) return res.status(400).json({ ok: false, error: "Invalid trade ticket", details: errors });
|
||||||
|
|
||||||
|
const saved = await createTradeTicket({
|
||||||
|
...ticket,
|
||||||
|
status: "staged",
|
||||||
|
instructions: buildInstructions(ticket),
|
||||||
|
});
|
||||||
|
await recordAuditEvent("TRADE_TICKET_STAGED", {
|
||||||
|
user_id: ticket.userId,
|
||||||
|
ticket_id: saved?.id,
|
||||||
|
agent_id: ticket.agentId,
|
||||||
|
market_id: ticket.marketId,
|
||||||
|
side: ticket.side,
|
||||||
|
max_amount: ticket.maxAmount,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(201).json({
|
||||||
|
ok: true,
|
||||||
|
ticket: saved,
|
||||||
|
private_key_used: false,
|
||||||
|
live_order_placed: false,
|
||||||
|
manual_review_required: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.setHeader("Allow", "GET, POST");
|
||||||
|
return res.status(405).json({ ok: false, error: "Method not allowed" });
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(500).json({ ok: false, error: err?.message || "Trade ticket request failed" });
|
||||||
|
}
|
||||||
|
}
|
||||||
+95
-2
@@ -583,8 +583,10 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
|
|||||||
<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" />
|
||||||
|
<input class="input" id="liveIntentQuestion" placeholder="Market question / note" />
|
||||||
<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" />
|
||||||
|
<input class="input" id="liveIntentPrice" type="number" min="0.01" max="0.99" step="0.01" placeholder="Limit price" />
|
||||||
<button class="btn" id="dryRunOrderBtn">Stage manual intent</button>
|
<button class="btn" id="dryRunOrderBtn">Stage manual intent</button>
|
||||||
</div>
|
</div>
|
||||||
<ol class="steps">
|
<ol class="steps">
|
||||||
@@ -600,6 +602,10 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
|
|||||||
<div id="liveAuditTrail"></div>
|
<div id="liveAuditTrail"></div>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-h"><h3>Provider webhooks</h3><span class="small muted">events are stored in Neon</span></div>
|
<div class="card-h"><h3>Provider webhooks</h3><span class="small muted">events are stored in Neon</span></div>
|
||||||
<div id="liveWebhookStatus" class="small muted">Checking webhook setup...</div>
|
<div id="liveWebhookStatus" class="small muted">Checking webhook setup...</div>
|
||||||
@@ -697,6 +703,7 @@ const PAPER_KEY = "pma_paper_accounts_v1";
|
|||||||
const LIVE_KEY = "pma_live_readiness_v1";
|
const LIVE_KEY = "pma_live_readiness_v1";
|
||||||
const PAPER_SESSION_PREFIX = "pma_cloud_session_";
|
const PAPER_SESSION_PREFIX = "pma_cloud_session_";
|
||||||
const PERSONAL_MODE = new URLSearchParams(location.search).get("personal") === "1";
|
const PERSONAL_MODE = new URLSearchParams(location.search).get("personal") === "1";
|
||||||
|
const PERSONAL_USER_ID = "local-readiness-user";
|
||||||
const CATEGORIES = ["All","Politics","Sports","Crypto","Economy","Pop Culture","Other"];
|
const CATEGORIES = ["All","Politics","Sports","Crypto","Economy","Pop Culture","Other"];
|
||||||
const SYNC_KEYS=[AGENTS_KEY,SUG_KEY,FOCUS_KEY,VIEW_KEY,PF_SORT_KEY,CHART_RANGE_KEY,INVEST_KEY,PAPER_KEY,LIVE_KEY];
|
const SYNC_KEYS=[AGENTS_KEY,SUG_KEY,FOCUS_KEY,VIEW_KEY,PF_SORT_KEY,CHART_RANGE_KEY,INVEST_KEY,PAPER_KEY,LIVE_KEY];
|
||||||
const CAT_COLORS = {Politics:"#fb7185",Sports:"#34d399",Crypto:"#fbbf24",Economy:"#7c8cff",
|
const CAT_COLORS = {Politics:"#fb7185",Sports:"#34d399",Crypto:"#fbbf24",Economy:"#7c8cff",
|
||||||
@@ -709,6 +716,7 @@ let PAPER_MARKET_OFFSET = 0;
|
|||||||
let PAPER_MARKET_QUERY = "";
|
let PAPER_MARKET_QUERY = "";
|
||||||
let LIVE_BACKEND_STATUS = null;
|
let LIVE_BACKEND_STATUS = null;
|
||||||
let LIVE_POLICY_STATUS = null;
|
let LIVE_POLICY_STATUS = null;
|
||||||
|
let PERSONAL_TICKETS = [];
|
||||||
let PROVIDER_CONFIG = null;
|
let PROVIDER_CONFIG = null;
|
||||||
const catColor = (c) => CAT_COLORS[c] || CAT_COLORS.Other;
|
const catColor = (c) => CAT_COLORS[c] || CAT_COLORS.Other;
|
||||||
const getFocus = () => localStorage.getItem(FOCUS_KEY) || "All";
|
const getFocus = () => localStorage.getItem(FOCUS_KEY) || "All";
|
||||||
@@ -1808,8 +1816,10 @@ function renderSuggestions(){
|
|||||||
<div class="metrics">
|
<div class="metrics">
|
||||||
<span>Vol <b>${fmtUSD(s.volume)}</b></span><span>24h <b>${fmtUSD(s.volume_24hr)}</b></span><span>Resolves <b>${days}</b></span>
|
<span>Vol <b>${fmtUSD(s.volume)}</b></span><span>24h <b>${fmtUSD(s.volume_24hr)}</b></span><span>Resolves <b>${days}</b></span>
|
||||||
${s.url?`<a class="market-link" href="${esc(s.url)}" target="_blank" rel="noopener">Open on Polymarket ↗</a>`:""}
|
${s.url?`<a class="market-link" href="${esc(s.url)}" target="_blank" rel="noopener">Open on Polymarket ↗</a>`:""}
|
||||||
|
${PERSONAL_MODE?`<button class="btn ghost" data-stage-suggestion="${esc(s.market_id)}">Stage ticket</button>`:""}
|
||||||
</div></div>`;
|
</div></div>`;
|
||||||
}).join("");
|
}).join("");
|
||||||
|
document.querySelectorAll("[data-stage-suggestion]").forEach(btn=>btn.onclick=()=>stageSuggestionTicket(btn.dataset.stageSuggestion));
|
||||||
}
|
}
|
||||||
function renderPortfolioTab(){
|
function renderPortfolioTab(){
|
||||||
const st=loadState();
|
const st=loadState();
|
||||||
@@ -2379,6 +2389,7 @@ async function fetchLiveBackendStatus(){
|
|||||||
LIVE_POLICY_STATUS={ok:false,note:"Policy status endpoint is unavailable."};
|
LIVE_POLICY_STATUS={ok:false,note:"Policy status endpoint is unavailable."};
|
||||||
}
|
}
|
||||||
renderLiveBackendStatus();
|
renderLiveBackendStatus();
|
||||||
|
if(PERSONAL_MODE)loadTradeTickets();
|
||||||
}
|
}
|
||||||
function renderLiveBackendStatus(){
|
function renderLiveBackendStatus(){
|
||||||
const root=$("liveBackendStatus");if(!root)return;
|
const root=$("liveBackendStatus");if(!root)return;
|
||||||
@@ -2407,24 +2418,106 @@ function renderLiveBackendStatus(){
|
|||||||
async function dryRunLiveOrderIntent(){
|
async function dryRunLiveOrderIntent(){
|
||||||
const s=loadLiveState();
|
const s=loadLiveState();
|
||||||
const intent={
|
const intent={
|
||||||
user_id:"local-readiness-user",
|
user_id:PERSONAL_USER_ID,
|
||||||
wallet_address:s.walletAddress,
|
wallet_address:s.walletAddress,
|
||||||
agent_id:$("liveIntentAgent").value,
|
agent_id:$("liveIntentAgent").value,
|
||||||
market_id:$("liveIntentMarket").value.trim(),
|
market_id:$("liveIntentMarket").value.trim(),
|
||||||
|
question:($("liveIntentQuestion")&&$("liveIntentQuestion").value||"").trim(),
|
||||||
side:$("liveIntentSide").value,
|
side:$("liveIntentSide").value,
|
||||||
max_amount:Number($("liveIntentAmount").value||0),
|
max_amount:Number($("liveIntentAmount").value||0),
|
||||||
|
limit_price:Number(($("liveIntentPrice")&&$("liveIntentPrice").value)||0)||null,
|
||||||
execution_mode:"manual_approval",
|
execution_mode:"manual_approval",
|
||||||
account_scope:PERSONAL_MODE?"personal":"public_readiness",
|
account_scope:PERSONAL_MODE?"personal":"public_readiness",
|
||||||
};
|
};
|
||||||
try{
|
try{
|
||||||
|
if(PERSONAL_MODE)await createTradeTicket({
|
||||||
|
user_id:intent.user_id,
|
||||||
|
agent_id:intent.agent_id,
|
||||||
|
market_id:intent.market_id,
|
||||||
|
question:intent.question,
|
||||||
|
side:intent.side,
|
||||||
|
max_amount:intent.max_amount,
|
||||||
|
limit_price:intent.limit_price,
|
||||||
|
rationale:"Manually staged from the personal cockpit.",
|
||||||
|
});
|
||||||
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,`${PERSONAL_MODE?"Staged manual":"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}`);
|
||||||
|
if(PERSONAL_MODE)await loadTradeTickets();
|
||||||
}catch(e){
|
}catch(e){
|
||||||
toast("Order dry-run failed.");
|
toast(e.message||"Order dry-run failed.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async function createTradeTicket(ticket){
|
||||||
|
const r=await fetch("/api/trade-tickets",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(ticket)});
|
||||||
|
const d=await r.json();
|
||||||
|
if(!r.ok)throw new Error(d.error||"Ticket staging failed");
|
||||||
|
toast("Manual trade ticket staged.");
|
||||||
|
return d.ticket;
|
||||||
|
}
|
||||||
|
async function stageSuggestionTicket(marketId){
|
||||||
|
const s=(loadSuggestions().suggestions||[]).find(x=>String(x.market_id)===String(marketId));
|
||||||
|
if(!s)return toast("Suggestion not found.");
|
||||||
|
const amount=Number(prompt("Max amount for this manual ticket?", "10")||0);
|
||||||
|
if(!amount||amount<=0)return;
|
||||||
|
try{
|
||||||
|
await createTradeTicket({
|
||||||
|
user_id:PERSONAL_USER_ID,
|
||||||
|
agent_id:localStorage.getItem(VIEW_KEY)||"value",
|
||||||
|
market_id:s.market_id,
|
||||||
|
question:s.question,
|
||||||
|
market_url:s.url,
|
||||||
|
side:s.side,
|
||||||
|
max_amount:amount,
|
||||||
|
limit_price:s.entry_price,
|
||||||
|
rationale:s.rationale,
|
||||||
|
});
|
||||||
|
await loadTradeTickets();
|
||||||
|
showTab("live");
|
||||||
|
}catch(e){toast(e.message||"Ticket staging failed.");}
|
||||||
|
}
|
||||||
|
async function loadTradeTickets(){
|
||||||
|
if(!PERSONAL_MODE)return;
|
||||||
|
try{
|
||||||
|
const r=await fetch(`/api/trade-tickets?user_id=${encodeURIComponent(PERSONAL_USER_ID)}&limit=30`,{cache:"no-store"});
|
||||||
|
const d=await r.json();
|
||||||
|
PERSONAL_TICKETS=d.tickets||[];
|
||||||
|
}catch(e){PERSONAL_TICKETS=[];}
|
||||||
|
renderTradeTickets();
|
||||||
|
}
|
||||||
|
async function updateTicketStatus(ticketId,status){
|
||||||
|
try{
|
||||||
|
const r=await fetch("/api/trade-tickets",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:"status",user_id:PERSONAL_USER_ID,ticket_id:ticketId,status})});
|
||||||
|
const d=await r.json();
|
||||||
|
if(!r.ok)throw new Error(d.error||"Ticket update failed");
|
||||||
|
await loadTradeTickets();
|
||||||
|
toast("Ticket updated.");
|
||||||
|
}catch(e){toast(e.message||"Ticket update failed.");}
|
||||||
|
}
|
||||||
|
function renderTradeTickets(){
|
||||||
|
const root=$("personalTicketQueue");if(!root)return;
|
||||||
|
if(!PERSONAL_MODE){root.innerHTML=`<div class="empty">Personal trade tickets are hidden on the public site.</div>`;return;}
|
||||||
|
if(!PERSONAL_TICKETS.length){root.innerHTML=`<div class="empty">No staged tickets yet. Stage one from Suggestions or the manual console.</div>`;return;}
|
||||||
|
root.innerHTML=PERSONAL_TICKETS.map(t=>{
|
||||||
|
const instructions=t.instructions||{},steps=instructions.steps||[];
|
||||||
|
const url=t.market_url||instructions.market_url||marketSearchUrl(t.question||t.market_id);
|
||||||
|
return `<div class="trade-card">
|
||||||
|
<div class="trade-head"><div><div class="trade-title">${esc(t.question||t.market_id)}</div><div class="trade-meta"><span>${esc(t.agent_id)}</span><span>${esc(t.status)}</span><span>${new Date(t.created_at).toLocaleString()}</span></div></div><span class="pill ${esc(t.side)}">BUY ${esc(t.side)}</span></div>
|
||||||
|
<div class="small muted">Max ${fmtUSD(Number(t.max_amount||0))}${t.limit_price?` · limit ${Math.round(Number(t.limit_price)*100)}¢`:""} · ticket #${t.id}</div>
|
||||||
|
${t.rationale?`<div class="rationale">${esc(t.rationale)}</div>`:""}
|
||||||
|
<ol class="steps">${steps.map(step=>`<li>${esc(step)}</li>`).join("")}</ol>
|
||||||
|
<div class="trade-actions">
|
||||||
|
<a class="btn" href="${esc(url)}" target="_blank" rel="noopener">Open Polymarket</a>
|
||||||
|
<button class="btn ghost" data-ticket-status="${t.id}:reviewed">Reviewed</button>
|
||||||
|
<button class="btn ghost" data-ticket-status="${t.id}:placed_manually">Placed manually</button>
|
||||||
|
<button class="btn ghost" data-ticket-status="${t.id}:skipped">Skipped</button>
|
||||||
|
<button class="btn ghost" data-ticket-status="${t.id}:cancelled">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}).join("");
|
||||||
|
document.querySelectorAll("[data-ticket-status]").forEach(btn=>btn.onclick=()=>{const [id,status]=btn.dataset.ticketStatus.split(":");updateTicketStatus(id,status);});
|
||||||
|
}
|
||||||
async function saveRiskProfileToServer(s){
|
async function saveRiskProfileToServer(s){
|
||||||
const permissions=JSON.parse(JSON.stringify(s.agents||{}));
|
const permissions=JSON.parse(JSON.stringify(s.agents||{}));
|
||||||
Object.values(permissions).forEach(p=>{p.autoTrade=false;p.manualApprove=true;});
|
Object.values(permissions).forEach(p=>{p.autoTrade=false;p.manualApprove=true;});
|
||||||
|
|||||||
Reference in New Issue
Block a user