mirror of
https://github.com/theodore-song/polymarket-analyst.git
synced 2026-07-28 08:27:45 +00:00
Add non-custodial manual trade tickets
This commit is contained in:
+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;
|
||||
}
|
||||
|
||||
@@ -194,6 +218,64 @@ export async function latestRiskProfile(userId) {
|
||||
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() {
|
||||
await ensureSchema();
|
||||
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" });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user