Files
polymarket-analyst_github/api/live.js
T
2026-07-14 07:54:50 -04:00

303 lines
13 KiB
JavaScript

import { recordAuditEvent } from "./_db.js";
const REQUIRED_ENV = [
["PRODUCTION_APP_URL", "Production app URL"],
["AUTH_PROVIDER", "Real account authentication provider"],
["DATABASE_URL", "Production account database"],
["SESSION_SECRET", "Session signing secret"],
["ENCRYPTION_KEY", "Encryption key for sensitive user settings"],
["KYC_PROVIDER", "KYC / eligibility provider"],
["KYC_API_KEY", "KYC provider API key"],
["KYC_WEBHOOK_SECRET", "KYC webhook secret"],
["SANCTIONS_PROVIDER", "Sanctions / watchlist screening provider"],
["SANCTIONS_API_KEY", "Sanctions provider API key"],
["GEOIP_PROVIDER", "Geo-IP / geofencing provider"],
["GEOIP_API_KEY", "Geo-IP provider API key"],
["RESTRICTED_JURISDICTIONS", "Restricted jurisdiction rules"],
["TERMS_VERSION", "Approved terms version"],
["PRIVACY_VERSION", "Approved privacy policy version"],
["RISK_DISCLOSURE_VERSION", "Approved risk disclosure version"],
["MARKET_POLICY_STORE", "Market eligibility and category policy store"],
["CONSENT_STORE", "Versioned user consent store"],
["PAYMENTS_PROVIDER", "Deposit and withdrawal provider"],
["PAYMENTS_API_KEY", "Payments provider API key"],
["PAYMENTS_WEBHOOK_SECRET", "Payments webhook secret"],
["WEBHOOK_BASE_URL", "Public webhook base URL"],
["WALLET_PROVIDER", "Wallet or deposit-wallet provider"],
["WALLET_PROJECT_ID", "Wallet provider project ID"],
["WALLET_API_KEY", "Wallet provider API key"],
["DEPOSIT_WALLET_ADDRESS", "Polymarket deposit-wallet address"],
["POLYMARKET_SIGNATURE_TYPE", "Polymarket signature type"],
["POLYMARKET_CLOB_API_KEY", "Polymarket CLOB API key"],
["POLYMARKET_CLOB_SECRET", "Polymarket CLOB API secret"],
["POLYMARKET_CLOB_PASSPHRASE", "Polymarket CLOB passphrase"],
["SETTLEMENT_ASSET", "Settlement asset"],
["SETTLEMENT_CHAIN", "Settlement chain"],
["RECONCILIATION_STORE", "Balance, position, and fill reconciliation store"],
["ACCOUNTING_EXPORT_STORE", "Statements, tax, and accounting export store"],
["RATE_LIMIT_STORE", "Rate limit and abuse prevention store"],
["AUDIT_LOG_STORE", "Durable audit log store"],
["MONITORING_DSN", "Error and performance monitoring"],
["INCIDENT_WEBHOOK_URL", "Incident alert webhook"],
["RISK_ADMIN_TOKEN", "Risk admin incident-control token"],
["ADMIN_ALERT_EMAIL", "Admin alert destination"],
["CUSTOMER_SUPPORT_EMAIL", "Customer support contact"],
];
const PERSONAL_REQUIRED_ENV = [
["DATABASE_URL", "Audit database"],
["DEPOSIT_WALLET_ADDRESS", "Personal Polymarket deposit wallet address"],
["POLYMARKET_SIGNATURE_TYPE", "Polymarket signature type"],
["POLYMARKET_CLOB_API_KEY", "Personal Polymarket CLOB API key"],
["POLYMARKET_CLOB_SECRET", "Personal Polymarket CLOB API secret"],
["POLYMARKET_CLOB_PASSPHRASE", "Personal Polymarket CLOB passphrase"],
["RISK_ADMIN_TOKEN", "Personal admin token"],
];
const ENV_ALIASES = {
DATABASE_URL: ["NEON_DATABASE_URL"],
KYC_API_KEY: ["VERIFF_API_KEY"],
KYC_WEBHOOK_SECRET: ["VERIFF_WEBHOOK_SECRET", "VERIFF_SECRET_KEY"],
SANCTIONS_API_KEY: ["VERIFF_API_KEY"],
GEOIP_API_KEY: ["VERIFF_API_KEY"],
PAYMENTS_API_KEY: ["CIRCLE_API_KEY"],
PAYMENTS_WEBHOOK_SECRET: ["CIRCLE_WEBHOOK_SECRET"],
WALLET_PROJECT_ID: ["CIRCLE_WALLET_SET_ID"],
WALLET_API_KEY: ["CIRCLE_API_KEY"],
RECONCILIATION_STORE: ["DATABASE_URL", "NEON_DATABASE_URL"],
ACCOUNTING_EXPORT_STORE: ["DATABASE_URL", "NEON_DATABASE_URL"],
RATE_LIMIT_STORE: ["DATABASE_URL", "NEON_DATABASE_URL"],
AUDIT_LOG_STORE: ["DATABASE_URL", "NEON_DATABASE_URL"],
CONSENT_STORE: ["DATABASE_URL", "NEON_DATABASE_URL"],
MONITORING_DSN: ["SENTRY_DSN"],
INCIDENT_WEBHOOK_URL: ["SENTRY_WEBHOOK_URL"],
};
const PROVIDER_STACK = [
{
provider: "Clerk",
role: "User auth, sessions, sign-in/sign-out, account identity",
env: ["AUTH_PROVIDER", "CLERK_PUBLISHABLE_KEY", "CLERK_SECRET_KEY", "CLERK_WEBHOOK_SIGNING_SECRET"],
dashboard: "https://dashboard.clerk.com/",
},
{
provider: "Neon",
role: "Postgres database for users, portfolios, audit metadata, reconciliation, and history",
env: ["DATABASE_URL"],
dashboard: "https://console.neon.tech/",
},
{
provider: "Veriff",
role: "KYC/KYB, identity verification, age checks, AML/sanctions workflow",
env: ["KYC_PROVIDER", "VERIFF_API_KEY", "VERIFF_SECRET_KEY", "VERIFF_WEBHOOK_SECRET"],
dashboard: "https://station.veriff.com/",
},
{
provider: "Circle",
role: "USDC, embedded wallets, payments/deposits/withdrawals, webhooks",
env: ["PAYMENTS_PROVIDER", "WALLET_PROVIDER", "CIRCLE_API_KEY", "CIRCLE_WEBHOOK_SECRET", "CIRCLE_WALLET_SET_ID"],
dashboard: "https://console.circle.com/",
},
{
provider: "Sentry",
role: "Error monitoring, browser/backend issue tracking, incident visibility",
env: ["MONITORING_DSN", "SENTRY_DSN"],
dashboard: "https://sentry.io/",
},
];
const LAUNCH_REQUIREMENTS = [
["legal", "Terms, privacy policy, risk disclosures, and jurisdiction policy approved"],
["eligibility", "KYC/KYB, age, sanctions, watchlist, and location screening active"],
["market_policy", "Allowed market categories, restricted events, and manipulation rules defined"],
["auth", "Production accounts, sessions, password reset, MFA path, and sign-out implemented"],
["wallets", "User-controlled wallet/deposit-wallet signing and permission revocation implemented"],
["payments", "Deposits, withdrawals, webhooks, failed-payment handling, and support flows implemented"],
["execution", "Signed order creation, CLOB submission, cancellation, retry, and fill tracking implemented"],
["reconciliation", "Cash, open orders, positions, fills, fees, and withdrawals reconciled continuously"],
["risk", "Agent allocation caps, market caps, stop rules, manual approval, and emergency pause enforced server-side"],
["security", "Secret management, encryption, rate limits, abuse controls, and security review completed"],
["records", "Append-only audit logs, customer statements, exports, and retention policy active"],
["operations", "Monitoring, incident response, customer support, and rollback plan ready"],
["testing", "Paper-to-live parity, dry-runs, test wallets, webhook tests, and edge-case QA completed"],
];
const WEBHOOK_ROUTES = [
["clerk", "Clerk user and session events", "/api/webhooks/clerk"],
["veriff", "Veriff verification and review events", "/api/webhooks/veriff"],
["circle", "Circle payment, wallet, and transfer events", "/api/webhooks/circle"],
];
function envValue(key) {
if (process.env[key]) return process.env[key];
return (ENV_ALIASES[key] || []).map((alias) => process.env[alias]).find(Boolean);
}
function providerStatus() {
return REQUIRED_ENV.map(([key, label]) => ({
key,
label,
configured: Boolean(envValue(key)),
aliases: ENV_ALIASES[key] || [],
expected: key === "POLYMARKET_SIGNATURE_TYPE" ? "3 for new deposit-wallet API users" : undefined,
}));
}
function personalStatus() {
return PERSONAL_REQUIRED_ENV.map(([key, label]) => ({
key,
label,
configured: Boolean(envValue(key)),
expected: key === "POLYMARKET_SIGNATURE_TYPE" ? "3 for deposit-wallet users" : undefined,
}));
}
function stackStatus() {
return PROVIDER_STACK.map((provider) => {
const checks = provider.env.map((key) => ({ key, configured: Boolean(envValue(key)) }));
return {
...provider,
configured: checks.every((x) => x.configured),
checks,
};
});
}
function liveTradingReady() {
const requiredConfigured = providerStatus().every((x) => x.configured);
const liveFlagEnabled = process.env.LIVE_TRADING_ENABLED === "true";
const signatureTypeOk = String(process.env.POLYMARKET_SIGNATURE_TYPE || "") === "3";
const chainOk = String(process.env.POLYMARKET_CHAIN_ID || "137") === "137";
return requiredConfigured && liveFlagEnabled && signatureTypeOk && chainOk;
}
function personalTradingReady() {
return personalStatus().every((x) => x.configured)
&& process.env.PERSONAL_TRADING_ENABLED === "true"
&& String(process.env.POLYMARKET_SIGNATURE_TYPE || "") === "3"
&& String(process.env.POLYMARKET_CHAIN_ID || "137") === "137";
}
function baseStatus() {
const providers = providerStatus();
const origin = (envValue("WEBHOOK_BASE_URL") || envValue("PRODUCTION_APP_URL") || "").replace(/\/api\/?$/, "").replace(/\/$/, "");
const liveFlagEnabled = process.env.LIVE_TRADING_ENABLED === "true";
const signatureTypeOk = String(process.env.POLYMARKET_SIGNATURE_TYPE || "") === "3";
const chainOk = String(process.env.POLYMARKET_CHAIN_ID || "137") === "137";
const missing = providers.filter((x) => !x.configured).map((x) => x.label);
const personal = personalStatus();
const personalMissing = personal.filter((x) => !x.configured).map((x) => x.label);
if (process.env.PERSONAL_TRADING_ENABLED !== "true") personalMissing.push("PERSONAL_TRADING_ENABLED=true after your own wallet/CLOB test");
if (!liveFlagEnabled) missing.push("LIVE_TRADING_ENABLED=true after final approval");
if (!signatureTypeOk) missing.push("POLYMARKET_SIGNATURE_TYPE=3 for deposit-wallet users");
if (!chainOk) missing.push("POLYMARKET_CHAIN_ID=137");
return {
ok: true,
live_trading_enabled: liveTradingReady(),
personal_trading_enabled: personalTradingReady(),
live_flag_enabled: liveFlagEnabled,
personal_flag_enabled: process.env.PERSONAL_TRADING_ENABLED === "true",
signature_type_ok: signatureTypeOk,
chain_ok: chainOk,
locked_reason: liveTradingReady()
? null
: "Live trading is locked until eligibility, payments, wallet/deposit-wallet signing, Polymarket CLOB credentials, audit storage, monitoring, and LIVE_TRADING_ENABLED=true are configured.",
providers,
provider_stack: stackStatus(),
personal_requirements: personal,
launch_requirements: LAUNCH_REQUIREMENTS.map(([key, label]) => ({ key, label })),
webhooks: WEBHOOK_ROUTES.map(([provider, label, path]) => ({
provider,
label,
path,
url: origin ? `${origin}${path}` : path,
})),
next_required: missing,
personal_next_required: personalMissing,
docs: {
polymarket_overview: "https://docs.polymarket.com/trading/overview",
polymarket_quickstart: "https://docs.polymarket.com/trading/quickstart",
deposit_wallets: "https://docs.polymarket.com/trading/deposit-wallets",
},
};
}
function validateIntent(intent) {
const missing = [];
["user_id", "wallet_address", "agent_id", "market_id", "side", "max_amount"].forEach((key) => {
if (!intent || intent[key] === undefined || intent[key] === null || intent[key] === "") missing.push(key);
});
if (intent && !["YES", "NO"].includes(String(intent.side).toUpperCase())) missing.push("side must be YES or NO");
if (intent && Number(intent.max_amount) <= 0) missing.push("max_amount must be positive");
return missing;
}
export default async function handler(req, res) {
res.setHeader("Cache-Control", "no-store");
if (req.method === "GET") {
return res.status(200).json(baseStatus());
}
if (req.method === "POST") {
const body = typeof req.body === "string" ? JSON.parse(req.body || "{}") : (req.body || {});
const intent = body.intent || body;
const errors = validateIntent(intent);
if (errors.length) {
return res.status(400).json({ ok: false, error: "Invalid order intent", details: errors });
}
const status = baseStatus();
const auditEvent = {
at: new Date().toISOString(),
type: "ORDER_INTENT_DRY_RUN",
user_id: String(intent.user_id),
wallet_address: String(intent.wallet_address),
agent_id: String(intent.agent_id),
market_id: String(intent.market_id),
side: String(intent.side).toUpperCase(),
max_amount: Number(intent.max_amount),
execution_mode: intent.execution_mode || "manual_approval",
account_scope: intent.account_scope || body.account_scope || "public_readiness",
};
if (auditEvent.account_scope === "personal") {
await recordAuditEvent("PERSONAL_ORDER_INTENT_STAGED", auditEvent);
return res.status(202).json({
ok: true,
dry_run: true,
manual_review_required: true,
live_order_placed: false,
message: personalTradingReady()
? "Personal prerequisites are configured, but this endpoint still stages manual review only."
: "Personal order intent staged. Configure your own deposit wallet/CLOB credentials before any manual live execution.",
audit_event: auditEvent,
status,
});
}
if (!status.live_trading_enabled) {
await recordAuditEvent("ORDER_INTENT_BLOCKED", auditEvent);
return res.status(423).json({
ok: false,
dry_run: true,
error: status.locked_reason,
audit_event: auditEvent,
status,
});
}
await recordAuditEvent("ORDER_INTENT_NOT_IMPLEMENTED", auditEvent);
return res.status(501).json({
ok: false,
dry_run: true,
error: "Live trading credentials are configured, but signed order placement is intentionally not implemented yet.",
audit_event: auditEvent,
status,
});
}
res.setHeader("Allow", "GET, POST");
return res.status(405).json({ ok: false, error: "Method not allowed" });
}