Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 26 additions & 15 deletions tools/synth-overlay/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Synth Overlay — Polymarket Edge & Position Sizing Extension
# Synth Overlay — Polymarket & Kalshi Edge Extension

Chrome extension that uses Chrome's **native Side Panel** to show Synth market context on Polymarket and convert that edge into a **concrete position size**. The panel is data-first: Synth Up/Down prices, edge, confidence, signal explanation, invalidation conditions, and a **Kelly-based position sizing calculator**.
Chrome extension that uses Chrome's **native Side Panel** to show Synth market context on **Polymarket** and **Kalshi** and convert that edge into a **concrete position size**. The panel is data-first: Synth Up/Down prices, edge, confidence, signal explanation, invalidation conditions, and a **Kelly-based position sizing calculator**.

## What it does

Expand All @@ -10,16 +10,25 @@ Chrome extension that uses Chrome's **native Side Panel** to show Synth market c
- **Synth-sourced prices only**: Displays prices from the Synth API to avoid sync issues with DOM-scraped market data.
- **Manual + auto refresh**: Refresh button in panel plus automatic 15s refresh. "Data as of" timestamp shows when the Synth data was generated.
- **Clear confidence colors**: red (<40%), amber (40–70%), green (≥70%).
- **Contextual only**: Enabled on Polymarket pages; panel shows guidance when page/slug is unsupported.
- **Multi-platform**: Works on both Polymarket and Kalshi. Platform is auto-detected from URL; the UI dynamically shows "Poly" or "Kalshi" labels.
- **Contextual only**: Enabled on supported platform pages; panel shows guidance when page/slug is unsupported.

## Architecture

The codebase uses a **platform registry pattern** — each platform (Polymarket, Kalshi) is a self-contained module with its own URL patterns, asset maps, slug normalisation, and market-type detection. Adding a third platform means adding one entry to the registry; no if/else scattering across the codebase.

- **`matcher.py`** — Platform registry (Python, server-side). Polymarket checked first to prevent slug collisions with Kalshi legacy tickers.
- **`extension/platforms.js`** — Platform registry (JS, extension-side). Single source of truth for origins, domain hints, URL templates.
- **`extension/content.js`** — Strategy pattern: platform detected once at init, scrapers dispatched per platform.

## How it works

1. **Content script** (on `polymarket.com`) reads the market slug from the page URL and scrapes **live prices** and **balance** from the DOM.
2. **Side panel page** requests context from the content script and fetches Synth edge data from local API (`GET /api/edge?slug=...`).
3. **Panel rendering** displays Synth forecast data (prices, edge, signal, confidence, analysis, invalidation) and updates every 15s or on manual refresh.
4. **Position Sizing card** combines Synth probabilities, Polymarket-implied odds, forecast confidence, and user balance into a Kelly-based recommendation.
5. **Background service worker** enables/disables side panel per-tab based on URL and runs the alert polling engine.
6. **Edge alerts** poll watched markets every 60s via `chrome.alarms`. When edge exceeds the user's threshold, a browser notification fires with asset, edge size, signal direction, and confidence. Clicking the notification focuses or opens the relevant Polymarket page. Notifications are suppressed when the user is already viewing the market and have a 5-minute cooldown per market to avoid spam.
1. **Content script** (on `polymarket.com` and `kalshi.com`) detects platform from hostname, reads the market slug using platform-specific extraction, and scrapes **live prices** and **balance** from the DOM.
2. **Side panel page** requests context from the content script and fetches Synth edge data from local API (`GET /api/edge?slug=...&platform=...`).
3. **Panel rendering** displays Synth forecast data (prices, edge, signal, confidence, analysis, invalidation) with dynamic platform labels and updates every 30s or on manual refresh.
4. **Position Sizing card** combines Synth probabilities, market-implied odds, forecast confidence, and user balance into a Kelly-based recommendation.
5. **Background service worker** enables/disables side panel per-tab based on URL (any supported platform) and runs the alert polling engine.
6. **Edge alerts** poll watched markets every 60s via `chrome.alarms`. Each watchlist entry stores its platform. When edge exceeds the user's threshold, a browser notification fires with asset, edge size, signal direction, and confidence. Clicking the notification focuses or opens the relevant market page on the correct platform. Notifications are suppressed when the user is already viewing the market and have a 5-minute cooldown per market to avoid spam.

## Synth API usage

Expand All @@ -37,7 +46,7 @@ The **Position Sizing** card in the side panel answers “how much should I bet?
### Balance detection

- The content script (`content.js`) runs on `polymarket.com` and:
- Scrapes **wallet / account balance** from compact DOM text such as `Balance 123.45 USDC` or `$123.45`.
- Scrapes **wallet / account balance** from compact DOM text such as `Balance 123.45 USDC` or `$123.45` (works on both Polymarket and Kalshi).
- Exposes this numeric balance as `balance` in the context returned to the side panel.
- In the side panel (`sidepanel.html` / `sidepanel.js`):
- The **Balance** field is pre-filled with the scraped value when available.
Expand Down Expand Up @@ -113,9 +122,9 @@ The UI shows:
## Run locally

1. Install: `pip install -r requirements.txt` (from repo root: `pip install -r tools/synth-overlay/requirements.txt`).
2. Start server (from repo root): `python tools/synth-overlay/server.py` (or from `tools/synth-overlay`: `python server.py`). Listens on `127.0.0.1:8765`.
2. Start server (from repo root): `python tools/synth-overlay/server.py` (or from `tools/synth-overlay`: `python server.py`). Listens on `127.0.0.1:8765`. Set `SERVER_HOST` env var to change bind address.
3. Load extension: Chrome → Extensions → Load unpacked → select `tools/synth-overlay/extension`.
4. Click the extension icon to open **Chrome Side Panel** (or pin and open from Side Panel UI). On Polymarket pages, the panel auto-enables.
4. Click the extension icon to open **Chrome Side Panel** (or pin and open from Side Panel UI). On Polymarket or Kalshi pages, the panel auto-enables.

## Verify the side panel (before recording)

Expand All @@ -126,9 +135,11 @@ The UI shows:
You should see JSON with `"signal"`, `"edge_pct"`, etc. If you see `"error"` or 404, the slug is not supported for the current mock/API.

2. **Open the exact URL** in Chrome (with the extension loaded from `extension/`):
- Daily (BTC): `https://polymarket.com/event/bitcoin-up-or-down-on-february-26`
- Hourly (ETH): `https://polymarket.com/event/ethereum-up-or-down-february-25-6pm-et`
- 15-Min (SOL): `https://polymarket.com/event/sol-updown-15m-1772204400`
- **Polymarket** Daily (BTC): `https://polymarket.com/event/bitcoin-up-or-down-on-february-26`
- **Polymarket** Hourly (ETH): `https://polymarket.com/event/ethereum-up-or-down-february-25-6pm-et`
- **Polymarket** 15-Min (SOL): `https://polymarket.com/event/sol-updown-15m-1772204400`
- **Kalshi** Daily (BTC): `https://kalshi.com/markets/kxbtcd`
- **Kalshi** Range (BTC): `https://kalshi.com/markets/kxbtc`
- The side panel requests the slug from the page and fetches Synth data from the local API. If API returns 200, panel fields populate.

3. **Interaction:**
Expand Down
16 changes: 12 additions & 4 deletions tools/synth-overlay/extension/alerts.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* Storage keys (chrome.storage.local):
* synth_alerts_enabled : boolean
* synth_alerts_threshold : number (edge pp, default 3.0)
* synth_alerts_watchlist : Array<{ slug, asset, label, addedAt }>
* synth_alerts_watchlist : Array<{ slug, asset, label, platform, addedAt }>
* synth_alerts_cooldowns : Object { slug: timestamp }
* synth_alerts_history : Array<{ slug, label, title, message, edgePct, signal, timestamp }>
* synth_alerts_auto_dismiss : boolean (default false)
Expand Down Expand Up @@ -72,8 +72,10 @@ var SynthAlerts = (function () {

// ---- Watchlist ----

function addToWatchlist(slug, asset, label, callback) {
function addToWatchlist(slug, asset, label, platform, callback) {
if (!slug) { if (callback) callback([]); return; }
// Backward compat: if platform is a function, it's the old callback signature
if (typeof platform === "function") { callback = platform; platform = "polymarket"; }
load(function (settings) {
var exists = settings.watchlist.some(function (w) { return w.slug === slug; });
if (exists) { if (callback) callback(settings.watchlist); return; }
Expand All @@ -82,6 +84,7 @@ var SynthAlerts = (function () {
slug: slug,
asset: asset || "BTC",
label: label || slug,
platform: platform || "polymarket",
addedAt: Date.now(),
});
saveWatchlist(settings.watchlist);
Expand Down Expand Up @@ -180,9 +183,14 @@ var SynthAlerts = (function () {

// ---- Format ----

function formatMarketLabel(asset, marketType) {
function formatMarketLabel(asset, marketType, platform) {
var typeMap = { daily: "24h", hourly: "1h", "15min": "15m", "5min": "5m" };
return (asset || "BTC") + " " + (typeMap[marketType] || marketType || "daily");
var prefix = "";
if (platform && platform !== "polymarket" && typeof SynthPlatforms !== "undefined") {
var p = SynthPlatforms.get(platform);
if (p) prefix = p.label + " ";
}
return prefix + (asset || "BTC") + " " + (typeMap[marketType] || marketType || "daily");
}

return {
Expand Down
76 changes: 47 additions & 29 deletions tools/synth-overlay/extension/background.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
var SUPPORTED_ORIGINS = [
"https://polymarket.com/"
];
// ---- Platform-aware background service worker ----
// Uses SynthPlatforms (loaded via importScripts) as single source of truth
// for supported origins and market URLs.

try { importScripts("platforms.js"); } catch (_e) {}

var API_BASE = "http://127.0.0.1:8765";
var ALARM_NAME = "synth-alert-poll";
Expand All @@ -20,10 +22,8 @@ var STORE_KEYS = {
var COOLDOWN_MS = 5 * 60 * 1000;

function isSupportedUrl(url) {
for (var i = 0; i < SUPPORTED_ORIGINS.length; i++) {
if (url.indexOf(SUPPORTED_ORIGINS[i]) === 0) return true;
}
return false;
if (typeof SynthPlatforms !== "undefined") return SynthPlatforms.isSupportedUrl(url);
return url && url.indexOf("polymarket.com") !== -1;
}

// ---- Side Panel ----
Expand All @@ -47,8 +47,7 @@ chrome.tabs.onUpdated.addListener(function (tabId, info, tab) {
}
});

// Poll immediately when user switches away from a Polymarket tab.
// If tab.url is unavailable (e.g. chrome:// pages), assume non-Polymarket.
// Poll immediately when user switches away from a supported tab.
chrome.tabs.onActivated.addListener(function (activeInfo) {
chrome.tabs.get(activeInfo.tabId, function (tab) {
if (chrome.runtime.lastError || !tab) { pollWatchlist(); return; }
Expand Down Expand Up @@ -143,7 +142,9 @@ function pollWatchlist() {
}

function checkMarketEdge(item, threshold) {
var url = API_BASE + "/api/edge?slug=" + encodeURIComponent(item.slug);
var platform = item.platform || "polymarket";
var url = API_BASE + "/api/edge?slug=" + encodeURIComponent(item.slug)
+ "&platform=" + encodeURIComponent(platform);
fetch(url)
.then(function (res) {
if (!res.ok) throw new Error("HTTP " + res.status);
Expand All @@ -170,7 +171,7 @@ function suppressAndNotify(item, data, cooldowns) {

chrome.tabs.query({ active: true, lastFocusedWindow: true }, function (tabs) {
var activeUrl = (tabs && tabs[0] && tabs[0].url) || "";
if (activeUrl.indexOf("polymarket.com") !== -1 && activeUrl.indexOf(item.slug) !== -1) {
if (isSupportedUrl(activeUrl) && activeUrl.indexOf(item.slug) !== -1) {
return;
}
cooldowns[item.slug] = Date.now();
Expand All @@ -196,12 +197,15 @@ function createEdgeNotification(notifId, item, data) {
var conf = data.confidence_score != null ? Math.round(data.confidence_score * 100) + "%" : "—";
var confLabel = data.confidence_score >= 0.7 ? "High" : data.confidence_score >= 0.4 ? "Med" : "Low";

var platform = item.platform || "polymarket";
var platformLabel = (typeof SynthPlatforms !== "undefined" && SynthPlatforms.get(platform))
? SynthPlatforms.get(platform).label : "Poly";
var synthUp = fmtProb(data.synth_probability_up != null ? data.synth_probability_up : data.synth_probability);
var polyUp = fmtProb(data.polymarket_probability_up != null ? data.polymarket_probability_up : data.polymarket_probability);

var title = (item.label || item.slug) + " — " + direction + " " + sign + edge + "pp";
var lines = [
"Synth " + synthUp + " vs Poly " + polyUp + " | " + strength,
"Synth " + synthUp + " vs " + platformLabel + " " + polyUp + " | " + strength,
"Confidence: " + confLabel + " (" + conf + ")",
];
if (data.explanation) {
Expand All @@ -211,6 +215,7 @@ function createEdgeNotification(notifId, item, data) {
// Save to notification history
var historyEntry = {
slug: item.slug,
platform: platform,
label: item.label || item.slug,
title: title,
message: lines.join("\n"),
Expand Down Expand Up @@ -242,29 +247,42 @@ function createEdgeNotification(notifId, item, data) {
});
}

// Focus or open the Polymarket page for the clicked notification
// Focus or open the correct market page for the clicked notification.
// Uses stored platform from watchlist to build the right URL.
chrome.notifications.onClicked.addListener(function (notifId) {
if (notifId.indexOf("synth-edge::") !== 0) return;
var slug = notifId.replace("synth-edge::", "");
if (!slug) { chrome.notifications.clear(notifId); return; }

var targetUrl = "https://polymarket.com/event/" + slug;

chrome.tabs.query({ url: "https://polymarket.com/*" }, function (tabs) {
var match = null;
for (var i = 0; i < tabs.length; i++) {
if (tabs[i].url && tabs[i].url.indexOf(slug) !== -1) {
match = tabs[i];
break;
}
}
if (match) {
chrome.tabs.update(match.id, { active: true });
chrome.windows.update(match.windowId, { focused: true });
} else {
chrome.tabs.create({ url: targetUrl });
// Look up the watchlist to get the platform for this slug
chrome.storage.local.get([STORE_KEYS.watchlist], function (result) {
var watchlist = Array.isArray(result[STORE_KEYS.watchlist]) ? result[STORE_KEYS.watchlist] : [];
var item = null;
for (var i = 0; i < watchlist.length; i++) {
if (watchlist[i].slug === slug) { item = watchlist[i]; break; }
}
chrome.notifications.clear(notifId);
var platform = (item && item.platform) || "polymarket";
var targetUrl = (typeof SynthPlatforms !== "undefined")
? SynthPlatforms.marketUrl(platform, slug)
: "https://polymarket.com/event/" + slug;

// Search across all supported platform tabs
chrome.tabs.query({}, function (tabs) {
var match = null;
for (var j = 0; j < tabs.length; j++) {
if (tabs[j].url && isSupportedUrl(tabs[j].url) && tabs[j].url.indexOf(slug) !== -1) {
match = tabs[j];
break;
}
}
if (match) {
chrome.tabs.update(match.id, { active: true });
chrome.windows.update(match.windowId, { focused: true });
} else {
chrome.tabs.create({ url: targetUrl });
}
chrome.notifications.clear(notifId);
});
});
});

Expand Down
Loading
Loading