mirror of
https://github.com/theodore-song/polymarket-analyst.git
synced 2026-07-27 16:07:46 +00:00
Add SMTP support for trade emails
This commit is contained in:
+7
-1
@@ -85,11 +85,17 @@ CUSTOMER_SUPPORT_EMAIL=
|
||||
RISK_ADMIN_WALLET=
|
||||
|
||||
# Trade email alerts
|
||||
# Use either Resend directly or a webhook from Zapier/Make/another email automation.
|
||||
# Use Resend, SMTP, or a webhook from Zapier/Make/another email automation.
|
||||
RESEND_API_KEY=
|
||||
TRADE_EMAIL_FROM=Poly Arena <alerts@yourdomain.com>
|
||||
TRADE_EMAIL_REPLY_TO=
|
||||
TRADE_EMAIL_WEBHOOK_URL=
|
||||
SMTP_HOST=
|
||||
SMTP_PORT=
|
||||
SMTP_SECURE=true
|
||||
SMTP_USER=
|
||||
SMTP_PASS=
|
||||
SMTP_FROM=
|
||||
|
||||
# Agent chat
|
||||
OPENAI_API_KEY=
|
||||
|
||||
+66
-7
@@ -9,6 +9,7 @@ import {
|
||||
updateRealPositionMark,
|
||||
updateTradeTicketStatus,
|
||||
} from "./_db.js";
|
||||
import nodemailer from "nodemailer";
|
||||
|
||||
const REQUIRED_ENV = [
|
||||
["PRODUCTION_APP_URL", "Production app URL"],
|
||||
@@ -173,6 +174,28 @@ function stackStatus() {
|
||||
});
|
||||
}
|
||||
|
||||
function tradeEmailStatus() {
|
||||
const resend = Boolean(process.env.RESEND_API_KEY);
|
||||
const webhook = Boolean(process.env.TRADE_EMAIL_WEBHOOK_URL);
|
||||
const smtpMissing = ["SMTP_HOST", "SMTP_PORT", "SMTP_USER", "SMTP_PASS"].filter((key) => !process.env[key]);
|
||||
const smtp = smtpMissing.length === 0;
|
||||
const provider = resend ? "resend" : (smtp ? "smtp" : (webhook ? "webhook" : null));
|
||||
return {
|
||||
configured: Boolean(provider),
|
||||
provider,
|
||||
options: {
|
||||
resend: { configured: resend, missing: resend ? [] : ["RESEND_API_KEY"] },
|
||||
smtp: { configured: smtp, missing: smtpMissing },
|
||||
webhook: { configured: webhook, missing: webhook ? [] : ["TRADE_EMAIL_WEBHOOK_URL"] },
|
||||
},
|
||||
missing_any_one_of: provider ? [] : [
|
||||
"RESEND_API_KEY",
|
||||
"or SMTP_HOST + SMTP_PORT + SMTP_USER + SMTP_PASS",
|
||||
"or TRADE_EMAIL_WEBHOOK_URL",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function liveTradingReady() {
|
||||
const requiredConfigured = providerStatus().every((x) => x.configured);
|
||||
const liveFlagEnabled = process.env.LIVE_TRADING_ENABLED === "true";
|
||||
@@ -214,6 +237,7 @@ function baseStatus() {
|
||||
: "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(),
|
||||
trade_email: tradeEmailStatus(),
|
||||
personal_requirements: personal,
|
||||
launch_requirements: LAUNCH_REQUIREMENTS.map(([key, label]) => ({ key, label })),
|
||||
webhooks: WEBHOOK_ROUTES.map(([provider, label, path]) => ({
|
||||
@@ -336,6 +360,31 @@ async function sendTradeEmail({ to, events, appUrl, test }) {
|
||||
return { status: 200, body: { ok: true, provider: "resend", id: data?.id || null, sent: events.length } };
|
||||
}
|
||||
|
||||
if (process.env.SMTP_HOST && process.env.SMTP_PORT && process.env.SMTP_USER && process.env.SMTP_PASS) {
|
||||
const port = Number(process.env.SMTP_PORT);
|
||||
const secure = String(process.env.SMTP_SECURE || "").toLowerCase() === "true" || port === 465;
|
||||
const from = process.env.TRADE_EMAIL_FROM || process.env.SMTP_FROM || process.env.SMTP_USER;
|
||||
const replyTo = process.env.TRADE_EMAIL_REPLY_TO || process.env.CUSTOMER_SUPPORT_EMAIL || undefined;
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: process.env.SMTP_HOST,
|
||||
port,
|
||||
secure,
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS,
|
||||
},
|
||||
});
|
||||
const info = await transporter.sendMail({
|
||||
from,
|
||||
to: email,
|
||||
replyTo,
|
||||
subject: payload.subject,
|
||||
text: payload.text,
|
||||
html: payload.html,
|
||||
});
|
||||
return { status: 200, body: { ok: true, provider: "smtp", id: info?.messageId || null, sent: events.length } };
|
||||
}
|
||||
|
||||
if (process.env.TRADE_EMAIL_WEBHOOK_URL) {
|
||||
const response = await fetch(process.env.TRADE_EMAIL_WEBHOOK_URL, {
|
||||
method: "POST",
|
||||
@@ -350,7 +399,8 @@ async function sendTradeEmail({ to, events, appUrl, test }) {
|
||||
status: 501,
|
||||
body: {
|
||||
ok: false,
|
||||
error: "Email alerts need RESEND_API_KEY plus TRADE_EMAIL_FROM, or TRADE_EMAIL_WEBHOOK_URL, in Vercel environment variables.",
|
||||
error: "Email alerts need a delivery provider in Vercel: RESEND_API_KEY, or SMTP_HOST + SMTP_PORT + SMTP_USER + SMTP_PASS, or TRADE_EMAIL_WEBHOOK_URL.",
|
||||
trade_email: tradeEmailStatus(),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -627,12 +677,21 @@ export default async function handler(req, res) {
|
||||
|
||||
if (body.action === "trade_email") {
|
||||
const events = normalizeTradeEmailEvents(body.events);
|
||||
const result = await sendTradeEmail({
|
||||
to: body.to,
|
||||
events,
|
||||
appUrl: body.app_url,
|
||||
test: Boolean(body.test),
|
||||
});
|
||||
let result;
|
||||
try {
|
||||
result = await sendTradeEmail({
|
||||
to: body.to,
|
||||
events,
|
||||
appUrl: body.app_url,
|
||||
test: Boolean(body.test),
|
||||
});
|
||||
} catch (err) {
|
||||
return res.status(502).json({
|
||||
ok: false,
|
||||
error: err?.response || err?.message || "Email provider failed to send the alert.",
|
||||
trade_email: tradeEmailStatus(),
|
||||
});
|
||||
}
|
||||
if (result.body?.ok) {
|
||||
await recordAuditEvent("TRADE_EMAIL_ALERT_SENT", {
|
||||
to_domain: String(body.to || "").split("@")[1] || null,
|
||||
|
||||
+19
-2
@@ -739,7 +739,7 @@ footer{margin-top:34px;padding-top:22px;border-top:1px solid var(--border);color
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
Build agent-email-filters · Paper trading only · Live prices from Polymarket's public Gamma API · Not financial advice ·
|
||||
Build email-provider-smtp · Paper trading only · Live prices from Polymarket's public Gamma API · Not financial advice ·
|
||||
<a class="market-link" href="https://github.com/theodore-song/polymarket-analyst" target="_blank" rel="noopener">Source on GitHub</a>
|
||||
</footer>
|
||||
</div>
|
||||
@@ -796,6 +796,7 @@ let LIVE_MARKET_OFFSET = 0;
|
||||
let LIVE_MARKET_QUERY = "";
|
||||
let LIVE_BACKEND_STATUS = null;
|
||||
let LIVE_POLICY_STATUS = null;
|
||||
let TRADE_EMAIL_STATUS = null;
|
||||
let PERSONAL_TICKETS = [];
|
||||
let REAL_PORTFOLIO = {positions:[],fills:[]};
|
||||
let PERSONAL_CAPITAL = {account:{cash:0},allocations:[],events:[]};
|
||||
@@ -833,6 +834,14 @@ async function initProviderConfig(){
|
||||
}
|
||||
}catch(e){}
|
||||
}
|
||||
async function refreshTradeEmailStatus(){
|
||||
try{
|
||||
const r=await fetch("/api/live",{cache:"no-store"});
|
||||
const d=await r.json();
|
||||
TRADE_EMAIL_STATUS=d&&d.trade_email?d.trade_email:null;
|
||||
}catch(e){TRADE_EMAIL_STATUS=null;}
|
||||
renderEmailAlerts();
|
||||
}
|
||||
|
||||
/* ---------- the ten competing agents ----------
|
||||
Each shares the same frequently refreshed suggestions but ranks/sizes them differently. */
|
||||
@@ -1974,9 +1983,15 @@ function renderEmailAlerts(){
|
||||
<label class="chip ${cfg.agents&&cfg.agents[a.id]?"active":""}" style="cursor:pointer;border-color:${cfg.agents&&cfg.agents[a.id]?a.color:""}">
|
||||
<input type="checkbox" data-email-agent="${esc(a.id)}" ${cfg.agents&&cfg.agents[a.id]?"checked":""} style="display:none"> ${a.emoji} ${esc(a.name)}
|
||||
</label>`).join("");
|
||||
const providerLine=TRADE_EMAIL_STATUS
|
||||
? (TRADE_EMAIL_STATUS.configured
|
||||
? `Email backend ready via ${TRADE_EMAIL_STATUS.provider}.`
|
||||
: `Email backend missing: add Resend, SMTP, or webhook env vars in Vercel.`)
|
||||
: "Checking email backend...";
|
||||
const sentLine=cfg.last_sent_at?` Last sent ${new Date(cfg.last_sent_at).toLocaleString()}.`:"";
|
||||
status.textContent=cfg.last_error
|
||||
? `Last error: ${cfg.last_error}`
|
||||
: (cfg.last_sent_at?`Last sent ${new Date(cfg.last_sent_at).toLocaleString()}.`:"Send a digest whenever selected agents open, close, stop out, or take gains during a cycle.");
|
||||
: `${providerLine}${sentLine} Sends a digest whenever selected agents trade during a cycle.`;
|
||||
const readAlertForm=()=>{
|
||||
const email=input.value.trim();
|
||||
const events={};
|
||||
@@ -2011,6 +2026,7 @@ function renderEmailAlerts(){
|
||||
equity:STARTING_BALANCE,
|
||||
return_pct:0,
|
||||
}],{test:true});
|
||||
await refreshTradeEmailStatus();
|
||||
pushCloudState(true);
|
||||
};
|
||||
document.querySelectorAll("[data-email-event],[data-email-agent]").forEach(el=>el.onchange=()=>{
|
||||
@@ -3564,6 +3580,7 @@ window.addEventListener("hashchange",()=>showTab(location.hash.slice(1)));
|
||||
/* ---------- Wire up ---------- */
|
||||
applyPersonalMode();
|
||||
initProviderConfig();
|
||||
refreshTradeEmailStatus();
|
||||
$("runBtn").addEventListener("click",async()=>{
|
||||
const btn=$("runBtn");btn.disabled=true;btn.textContent="Running…";
|
||||
try{const r=await runDailyCycle();const b=board();toast(r.busy?`Cycle already running · ${b[0].c.emoji} ${b[0].c.name} leads`:r.skipped?`Already ran this minute · ${b[0].c.emoji} ${b[0].c.name} leads`:`Cycle done · ${r.suggestions} ideas · ${b[0].c.emoji} ${b[0].c.name} leads`);renderAll();}
|
||||
|
||||
Generated
+10
@@ -7,6 +7,7 @@
|
||||
"dependencies": {
|
||||
"@neondatabase/serverless": "^1.1.0",
|
||||
"@vercel/blob": "2.5.0",
|
||||
"nodemailer": "^9.0.3",
|
||||
"svix": "^1.96.1"
|
||||
}
|
||||
},
|
||||
@@ -222,6 +223,15 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.3.tgz",
|
||||
"integrity": "sha512-n+YP+NKwR5zRWa60k3GiQ6Q3B4KXCoAw40dAKeCtYn020iNN74aWK2liXIC3ZEATeGql7we3tE3t8QwhY0eskw==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/npm-run-path": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"dependencies": {
|
||||
"@neondatabase/serverless": "^1.1.0",
|
||||
"@vercel/blob": "2.5.0",
|
||||
"nodemailer": "^9.0.3",
|
||||
"svix": "^1.96.1"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user