mirror of
https://github.com/theodore-song/polymarket-analyst.git
synced 2026-08-13 07:38:04 +00:00
Fold trade tickets into live API
This commit is contained in:
@@ -10,9 +10,10 @@ 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
|
- `/api/live?action=tickets` and `/api/live` ticket actions store
|
||||||
contain the market, side, amount, limit, rationale, Polymarket link, and
|
non-custodial manual trade tickets. These tickets contain the market, side,
|
||||||
review instructions, but never a private key or signed order.
|
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
|
||||||
|
|||||||
+91
-1
@@ -1,4 +1,4 @@
|
|||||||
import { recordAuditEvent } from "./_db.js";
|
import { createTradeTicket, listTradeTickets, recordAuditEvent, updateTradeTicketStatus } from "./_db.js";
|
||||||
|
|
||||||
const REQUIRED_ENV = [
|
const REQUIRED_ENV = [
|
||||||
["PRODUCTION_APP_URL", "Production app URL"],
|
["PRODUCTION_APP_URL", "Production app URL"],
|
||||||
@@ -232,15 +232,105 @@ function validateIntent(intent) {
|
|||||||
return missing;
|
return missing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const VALID_TICKET_STATUSES = new Set(["staged", "reviewed", "placed_manually", "skipped", "cancelled"]);
|
||||||
|
|
||||||
|
function marketSearchUrl(question) {
|
||||||
|
return `https://polymarket.com/search?query=${encodeURIComponent(String(question || ""))}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTicketInstructions(ticket) {
|
||||||
|
const marketUrl = ticket.market_url || ticket.marketUrl || marketSearchUrl(ticket.question || ticket.market_id || 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 validateTicket(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) {
|
export default async function handler(req, res) {
|
||||||
res.setHeader("Cache-Control", "no-store");
|
res.setHeader("Cache-Control", "no-store");
|
||||||
|
|
||||||
if (req.method === "GET") {
|
if (req.method === "GET") {
|
||||||
|
if (req.query?.action === "tickets") {
|
||||||
|
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 });
|
||||||
|
}
|
||||||
return res.status(200).json(baseStatus());
|
return res.status(200).json(baseStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.method === "POST") {
|
if (req.method === "POST") {
|
||||||
const body = typeof req.body === "string" ? JSON.parse(req.body || "{}") : (req.body || {});
|
const body = typeof req.body === "string" ? JSON.parse(req.body || "{}") : (req.body || {});
|
||||||
|
|
||||||
|
if (body.action === "ticket") {
|
||||||
|
const ticket = normalizeTicket(body);
|
||||||
|
const ticketErrors = validateTicket(ticket);
|
||||||
|
if (ticketErrors.length) return res.status(400).json({ ok: false, error: "Invalid trade ticket", details: ticketErrors });
|
||||||
|
const saved = await createTradeTicket({
|
||||||
|
...ticket,
|
||||||
|
status: "staged",
|
||||||
|
instructions: buildTicketInstructions(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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.action === "ticket_status") {
|
||||||
|
const userId = String(body.user_id || "local-readiness-user").trim();
|
||||||
|
const status = String(body.status || "").trim();
|
||||||
|
if (!VALID_TICKET_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 intent = body.intent || body;
|
const intent = body.intent || body;
|
||||||
const errors = validateIntent(intent);
|
const errors = validateIntent(intent);
|
||||||
if (errors.length) {
|
if (errors.length) {
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
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" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+3
-3
@@ -2450,7 +2450,7 @@ async function dryRunLiveOrderIntent(){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function createTradeTicket(ticket){
|
async function createTradeTicket(ticket){
|
||||||
const r=await fetch("/api/trade-tickets",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(ticket)});
|
const r=await fetch("/api/live",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({...ticket,action:"ticket"})});
|
||||||
const d=await r.json();
|
const d=await r.json();
|
||||||
if(!r.ok)throw new Error(d.error||"Ticket staging failed");
|
if(!r.ok)throw new Error(d.error||"Ticket staging failed");
|
||||||
toast("Manual trade ticket staged.");
|
toast("Manual trade ticket staged.");
|
||||||
@@ -2480,7 +2480,7 @@ async function stageSuggestionTicket(marketId){
|
|||||||
async function loadTradeTickets(){
|
async function loadTradeTickets(){
|
||||||
if(!PERSONAL_MODE)return;
|
if(!PERSONAL_MODE)return;
|
||||||
try{
|
try{
|
||||||
const r=await fetch(`/api/trade-tickets?user_id=${encodeURIComponent(PERSONAL_USER_ID)}&limit=30`,{cache:"no-store"});
|
const r=await fetch(`/api/live?action=tickets&user_id=${encodeURIComponent(PERSONAL_USER_ID)}&limit=30`,{cache:"no-store"});
|
||||||
const d=await r.json();
|
const d=await r.json();
|
||||||
PERSONAL_TICKETS=d.tickets||[];
|
PERSONAL_TICKETS=d.tickets||[];
|
||||||
}catch(e){PERSONAL_TICKETS=[];}
|
}catch(e){PERSONAL_TICKETS=[];}
|
||||||
@@ -2488,7 +2488,7 @@ async function loadTradeTickets(){
|
|||||||
}
|
}
|
||||||
async function updateTicketStatus(ticketId,status){
|
async function updateTicketStatus(ticketId,status){
|
||||||
try{
|
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 r=await fetch("/api/live",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:"ticket_status",user_id:PERSONAL_USER_ID,ticket_id:ticketId,status})});
|
||||||
const d=await r.json();
|
const d=await r.json();
|
||||||
if(!r.ok)throw new Error(d.error||"Ticket update failed");
|
if(!r.ok)throw new Error(d.error||"Ticket update failed");
|
||||||
await loadTradeTickets();
|
await loadTradeTickets();
|
||||||
|
|||||||
Reference in New Issue
Block a user