/* * Live Polymarket arbitrage scanner — portfolio demo. * * What this does, in plain English: * - Loads a list of 6–8 currently-active categorical Polymarket events * from a static JSON file that a GitHub Action refreshes every hour. * - Opens a WebSocket to Polymarket's public CLOB and subscribes to every * outcome token of those events. * - Maintains order book state per outcome and recomputes "basket cost" — * the total cost of buying one share of every answer — on every update. * - If that basket cost drops below $1.00, it's a risk-free arbitrage and * the page flashes green. * * This is a browser-only port of the Python engine in the repo. No backend, * no keys, no orders submitted. */ const EVENTS_URL = "./data/events.json"; const WS_URL = "wss://ws-subscriptions-clob.polymarket.com/ws/market"; const MAX_EVENTS_TO_WATCH = 6; const PING_INTERVAL_MS = 10_000; const NEAR_ARB_THRESHOLD = 0.01; // $0.01 above $1 counts as "near miss" // --- DOM shortcuts --------------------------------------------------------- const $ = (sel) => document.querySelector(sel); const el = { connDot: $("#conn-dot"), connLabel: $("#conn-label"), aLine: $("#a-line"), aSub: $("#a-sub"), watchCount: $("#watch-count"), eventList: $("#event-list"), eventTitle: $("#event-title"), eventSub: $("#event-subtitle"), whyBtn: $("#why-this-event"), basketCost: $("#basket-cost"), basketCostSub:$("#basket-cost-sub"), basketPnl: $("#basket-pnl"), basketPnlSub: $("#basket-pnl-sub"), basketStatus: $("#basket-status"), basketFill: $("#basket-fill"), scaleButtons: $("#scale-buttons"), scaleOut: $("#scale-out"), outcomeList: $("#outcome-list"), statEvents: $("#stat-events"), statTokens: $("#stat-tokens"), statBooks: $("#stat-books"), statMsgs: $("#stat-msgs"), statUptime: $("#stat-uptime"), modal: $("#modal"), modalContent: $("#modal-content"), }; // --- state ---------------------------------------------------------------- const state = { events: {}, books: {}, tokenToEvent: {}, selectedEventId: null, msgCount: 0, startTime: Date.now(), ws: null, pingTimer: null, scale: 1, }; class Ladder { constructor() { this.m = {}; this.sortedKeys = null; } set(price, size) { if (size <= 0) this.del(price); else { this.m[price] = size; this.sortedKeys = null; } } del(price) { delete this.m[price]; this.sortedKeys = null; } clear() { this.m = {}; this.sortedKeys = null; } size() { return Object.keys(this.m).length; } keys() { if (this.sortedKeys === null) this.sortedKeys = Object.keys(this.m).map(parseFloat).sort((a,b)=>a-b); return this.sortedKeys; } best(ascending) { const ks = this.keys(); if (!ks.length) return null; const p = ascending ? ks[0] : ks[ks.length - 1]; return { price: p, size: this.m[p] }; } } // --- bootstrap ------------------------------------------------------------ bootstrap().catch((err) => { console.error("bootstrap failed", err); setConn("bad", "unable to load events — " + err.message); el.aLine.innerHTML = `Couldn't load.`; el.aSub.textContent = "Refresh the page in a minute — the events list refreshes hourly."; }); async function bootstrap() { setConn("pend", "loading active events…"); const events = await fetchActiveNegRiskEvents(); if (!events.length) { setConn("bad", "no active events found"); return; } for (const e of events) { state.events[e.id] = e; for (const o of e.outcomes) state.tokenToEvent[o.token_id] = e.id; } state.selectedEventId = events[0].id; el.watchCount.textContent = events.length; el.statEvents.textContent = events.length; el.statTokens.textContent = Object.keys(state.tokenToEvent).length; renderEventList(); renderSelectedEvent(); wireInteractions(); startUptimeTicker(); connectWS(); } // --- REST (same-origin events.json — GitHub Action refreshes hourly) ------ async function fetchActiveNegRiskEvents() { const resp = await fetch(EVENTS_URL + "?t=" + Date.now()); if (!resp.ok) throw new Error("events.json " + resp.status); const payload = await resp.json(); const raw = Array.isArray(payload?.events) ? payload.events : []; const kept = []; for (const ev of raw) { if (!Array.isArray(ev.outcomes) || ev.outcomes.length < 2) continue; kept.push({ id: String(ev.id), title: String(ev.title || "Event"), slug: ev.slug, outcomes: ev.outcomes.map(o => ({ token_id: String(o.token_id), name: String(o.name) })), sum: null, lastUpdate: 0, }); if (kept.length >= MAX_EVENTS_TO_WATCH) break; } return kept; } // --- WebSocket ------------------------------------------------------------ function connectWS() { const tokenIds = Object.keys(state.tokenToEvent); if (!tokenIds.length) { setConn("bad", "no tokens"); return; } setConn("pend", "opening connection to Polymarket…"); const ws = new WebSocket(WS_URL); state.ws = ws; ws.addEventListener("open", () => { setConn("on", `live · reading order books for ${Object.keys(state.events).length} events`); ws.send(JSON.stringify({ type: "market", assets_ids: tokenIds, custom_feature_enabled: true })); if (state.pingTimer) clearInterval(state.pingTimer); state.pingTimer = setInterval(() => { try { ws.send("PING"); } catch {} }, PING_INTERVAL_MS); }); ws.addEventListener("message", (ev) => { const raw = ev.data; if (typeof raw === "string") { const t = raw.trim(); if (t === "PONG" || t === "PING") return; try { handlePayload(JSON.parse(t)); } catch {} } }); ws.addEventListener("close", () => { setConn("bad", "connection dropped — reconnecting…"); if (state.pingTimer) clearInterval(state.pingTimer); setTimeout(connectWS, 3000); }); ws.addEventListener("error", () => setConn("bad", "connection error")); } function handlePayload(payload) { if (Array.isArray(payload)) for (const m of payload) dispatch(m); else if (payload && typeof payload === "object") dispatch(payload); } function dispatch(msg) { state.msgCount++; el.statMsgs.textContent = state.msgCount.toLocaleString(); const t = msg.event_type; if (t === "book") applyBookSnapshot(msg); else if (t === "price_change") applyPriceChange(msg); } function applyBookSnapshot(msg) { const assetId = msg.asset_id; if (!assetId || !state.tokenToEvent[assetId]) return; const book = getBook(assetId); book.bids.clear(); book.asks.clear(); for (const lvl of (msg.bids || [])) { const p = parseFloat(lvl.price), s = parseFloat(lvl.size); if (p > 0 && s > 0) book.bids.set(p, s); } for (const lvl of (msg.asks || [])) { const p = parseFloat(lvl.price), s = parseFloat(lvl.size); if (p > 0 && s > 0) book.asks.set(p, s); } onBookUpdate(assetId); } function applyPriceChange(msg) { if (!Array.isArray(msg.price_changes)) return; const touched = new Set(); for (const c of msg.price_changes) { const assetId = c.asset_id; if (!assetId || !state.tokenToEvent[assetId]) continue; const p = parseFloat(c.price), s = parseFloat(c.size); if (!(p > 0) || isNaN(s)) continue; const book = getBook(assetId); const side = (c.side || "").toUpperCase(); if (side === "BUY") book.bids.set(p, s); else if (side === "SELL") book.asks.set(p, s); else continue; touched.add(assetId); } for (const id of touched) onBookUpdate(id); } function getBook(tokenId) { if (!state.books[tokenId]) { state.books[tokenId] = { bids: new Ladder(), asks: new Ladder() }; el.statBooks.textContent = Object.keys(state.books).length.toLocaleString(); } return state.books[tokenId]; } // --- engine --------------------------------------------------------------- function onBookUpdate(tokenId) { const eventId = state.tokenToEvent[tokenId]; if (!eventId) return; evaluateEvent(eventId); renderEventList(); if (eventId === state.selectedEventId) renderSelectedEvent(); renderHeroAnswer(); } function evaluateEvent(eventId) { const ev = state.events[eventId]; if (!ev) return; let sum = 0, missing = 0; for (const o of ev.outcomes) { const b = state.books[o.token_id]; const best = b && b.asks.best(true); if (!best) { missing += 1; continue; } sum += best.price; } ev.sum = missing > 0 ? null : sum; ev.lastUpdate = Date.now(); } // --- rendering ------------------------------------------------------------ function setConn(cls, text) { el.connDot.className = "dot " + cls; el.connLabel.textContent = text; } function renderHeroAnswer() { // Find the cheapest basket across all events let bestEv = null, bestSum = Infinity; for (const id of Object.keys(state.events)) { const ev = state.events[id]; if (ev.sum === null || ev.sum === undefined) continue; if (ev.sum < bestSum) { bestSum = ev.sum; bestEv = ev; } } if (!bestEv) { el.aLine.innerHTML = `checking live prices…`; return; } const cls = classForSum(bestSum); const gap = bestSum - 1; if (cls === "arb") { const bps = Math.round(-gap * 10_000); el.aLine.innerHTML = `Yes! The cheapest bundle is $${bestSum.toFixed(4)}— free ${Math.abs(bps)} bps (${(-gap*100).toFixed(2)}¢) per $1 bet`; el.aSub.innerHTML = `On ${escapeHtml(bestEv.title)}. Click it on the left to see the details.`; } else if (cls === "near") { el.aLine.innerHTML = `Almost. The cheapest bundle is $${bestSum.toFixed(4)}— you'd lose ${(gap*100).toFixed(2)}¢ per $1 bet`; el.aSub.innerHTML = `On ${escapeHtml(bestEv.title)}. Watch it — if another trader sells off this might flip into arbitrage.`; } else { el.aLine.innerHTML = `Not right now. The cheapest bundle is $${bestSum.toFixed(4)}— you'd lose ${(gap*100).toFixed(2)}¢ per $1 bet`; el.aSub.innerHTML = `Watching ${Object.keys(state.events).length} events. This is the normal state — arbitrage windows are rare.`; } } function renderEventList() { el.eventList.innerHTML = ""; for (const id of Object.keys(state.events)) { const ev = state.events[id]; const cls = classForSum(ev.sum); const costText = ev.sum === null ? "waiting" : "$" + ev.sum.toFixed(3); const costCls = ev.sum === null ? "" : cls === "arb" ? "yes" : cls === "near" ? "near" : "no"; const card = document.createElement("div"); card.className = "event-card" + (id === state.selectedEventId ? " active" : ""); card.innerHTML = `
This is a real, currently-open question on Polymarket. There are ${ev.outcomes.length} possible answers, and when the real event resolves, one will be declared the winner. Shares of the winning answer pay $1. Shares of every losing answer pay $0.
The numbers on this page come straight from Polymarket's live order book — same feed their website uses. See the original market here:
`; el.modal.hidden = false; }); // modal close el.modal.addEventListener("click", (e) => { if (e.target.dataset?.close !== undefined) el.modal.hidden = true; }); document.addEventListener("keydown", (e) => { if (e.key === "Escape") el.modal.hidden = true; }); } function startUptimeTicker() { setInterval(() => { const secs = Math.floor((Date.now() - state.startTime) / 1000); const mm = String(Math.floor(secs / 60)).padStart(2, "0"); const ss = String(secs % 60).padStart(2, "0"); el.statUptime.textContent = `${mm}:${ss}`; }, 1000); } // --- utils ---------------------------------------------------------------- function formatSize(s) { if (s >= 1_000_000) return (s / 1_000_000).toFixed(2) + "M"; if (s >= 10_000) return (s / 1_000).toFixed(1) + "k"; if (s >= 1_000) return (s / 1_000).toFixed(2) + "k"; return s.toFixed(0); } function escapeHtml(s) { return String(s).replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); }