Skip to content

Commit 4cf35d4

Browse files
authored
Merge pull request #41 from MkDev11/feat/issue-19-kalshi-support
feat: add Kalshi support with grammar-based ticker parser (#19)
2 parents dec3e35 + 3d6e155 commit 4cf35d4

12 files changed

Lines changed: 1812 additions & 178 deletions

File tree

tools/synth-overlay/README.md

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# Synth Overlay — Polymarket Edge & Position Sizing Extension
1+
# Synth Overlay — Polymarket & Kalshi Edge Extension
22

3-
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**.
3+
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**.
44

55
## What it does
66

@@ -10,16 +10,25 @@ Chrome extension that uses Chrome's **native Side Panel** to show Synth market c
1010
- **Synth-sourced prices only**: Displays prices from the Synth API to avoid sync issues with DOM-scraped market data.
1111
- **Manual + auto refresh**: Refresh button in panel plus automatic 15s refresh. "Data as of" timestamp shows when the Synth data was generated.
1212
- **Clear confidence colors**: red (<40%), amber (40–70%), green (≥70%).
13-
- **Contextual only**: Enabled on Polymarket pages; panel shows guidance when page/slug is unsupported.
13+
- **Multi-platform**: Works on both Polymarket and Kalshi. Platform is auto-detected from URL; the UI dynamically shows "Poly" or "Kalshi" labels.
14+
- **Contextual only**: Enabled on supported platform pages; panel shows guidance when page/slug is unsupported.
15+
16+
## Architecture
17+
18+
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.
19+
20+
- **`matcher.py`** — Platform registry (Python, server-side). Polymarket checked first to prevent slug collisions with Kalshi legacy tickers.
21+
- **`extension/platforms.js`** — Platform registry (JS, extension-side). Single source of truth for origins, domain hints, URL templates.
22+
- **`extension/content.js`** — Strategy pattern: platform detected once at init, scrapers dispatched per platform.
1423

1524
## How it works
1625

17-
1. **Content script** (on `polymarket.com`) reads the market slug from the page URL and scrapes **live prices** and **balance** from the DOM.
18-
2. **Side panel page** requests context from the content script and fetches Synth edge data from local API (`GET /api/edge?slug=...`).
19-
3. **Panel rendering** displays Synth forecast data (prices, edge, signal, confidence, analysis, invalidation) and updates every 15s or on manual refresh.
20-
4. **Position Sizing card** combines Synth probabilities, Polymarket-implied odds, forecast confidence, and user balance into a Kelly-based recommendation.
21-
5. **Background service worker** enables/disables side panel per-tab based on URL and runs the alert polling engine.
22-
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.
26+
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.
27+
2. **Side panel page** requests context from the content script and fetches Synth edge data from local API (`GET /api/edge?slug=...&platform=...`).
28+
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.
29+
4. **Position Sizing card** combines Synth probabilities, market-implied odds, forecast confidence, and user balance into a Kelly-based recommendation.
30+
5. **Background service worker** enables/disables side panel per-tab based on URL (any supported platform) and runs the alert polling engine.
31+
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.
2332

2433
## Synth API usage
2534

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

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

115124
1. Install: `pip install -r requirements.txt` (from repo root: `pip install -r tools/synth-overlay/requirements.txt`).
116-
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`.
125+
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.
117126
3. Load extension: Chrome → Extensions → Load unpacked → select `tools/synth-overlay/extension`.
118-
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.
127+
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.
119128

120129
## Verify the side panel (before recording)
121130

@@ -126,9 +135,11 @@ The UI shows:
126135
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.
127136

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

134145
3. **Interaction:**

tools/synth-overlay/extension/alerts.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* Storage keys (chrome.storage.local):
88
* synth_alerts_enabled : boolean
99
* synth_alerts_threshold : number (edge pp, default 3.0)
10-
* synth_alerts_watchlist : Array<{ slug, asset, label, addedAt }>
10+
* synth_alerts_watchlist : Array<{ slug, asset, label, platform, addedAt }>
1111
* synth_alerts_cooldowns : Object { slug: timestamp }
1212
* synth_alerts_history : Array<{ slug, label, title, message, edgePct, signal, timestamp }>
1313
* synth_alerts_auto_dismiss : boolean (default false)
@@ -72,8 +72,10 @@ var SynthAlerts = (function () {
7272

7373
// ---- Watchlist ----
7474

75-
function addToWatchlist(slug, asset, label, callback) {
75+
function addToWatchlist(slug, asset, label, platform, callback) {
7676
if (!slug) { if (callback) callback([]); return; }
77+
// Backward compat: if platform is a function, it's the old callback signature
78+
if (typeof platform === "function") { callback = platform; platform = "polymarket"; }
7779
load(function (settings) {
7880
var exists = settings.watchlist.some(function (w) { return w.slug === slug; });
7981
if (exists) { if (callback) callback(settings.watchlist); return; }
@@ -82,6 +84,7 @@ var SynthAlerts = (function () {
8284
slug: slug,
8385
asset: asset || "BTC",
8486
label: label || slug,
87+
platform: platform || "polymarket",
8588
addedAt: Date.now(),
8689
});
8790
saveWatchlist(settings.watchlist);
@@ -180,9 +183,14 @@ var SynthAlerts = (function () {
180183

181184
// ---- Format ----
182185

183-
function formatMarketLabel(asset, marketType) {
186+
function formatMarketLabel(asset, marketType, platform) {
184187
var typeMap = { daily: "24h", hourly: "1h", "15min": "15m", "5min": "5m" };
185-
return (asset || "BTC") + " " + (typeMap[marketType] || marketType || "daily");
188+
var prefix = "";
189+
if (platform && platform !== "polymarket" && typeof SynthPlatforms !== "undefined") {
190+
var p = SynthPlatforms.get(platform);
191+
if (p) prefix = p.label + " ";
192+
}
193+
return prefix + (asset || "BTC") + " " + (typeMap[marketType] || marketType || "daily");
186194
}
187195

188196
return {

tools/synth-overlay/extension/background.js

Lines changed: 47 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
var SUPPORTED_ORIGINS = [
2-
"https://polymarket.com/"
3-
];
1+
// ---- Platform-aware background service worker ----
2+
// Uses SynthPlatforms (loaded via importScripts) as single source of truth
3+
// for supported origins and market URLs.
4+
5+
try { importScripts("platforms.js"); } catch (_e) {}
46

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

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

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

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

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

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

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

202206
var title = (item.label || item.slug) + " — " + direction + " " + sign + edge + "pp";
203207
var lines = [
204-
"Synth " + synthUp + " vs Poly " + polyUp + " | " + strength,
208+
"Synth " + synthUp + " vs " + platformLabel + " " + polyUp + " | " + strength,
205209
"Confidence: " + confLabel + " (" + conf + ")",
206210
];
207211
if (data.explanation) {
@@ -211,6 +215,7 @@ function createEdgeNotification(notifId, item, data) {
211215
// Save to notification history
212216
var historyEntry = {
213217
slug: item.slug,
218+
platform: platform,
214219
label: item.label || item.slug,
215220
title: title,
216221
message: lines.join("\n"),
@@ -242,29 +247,42 @@ function createEdgeNotification(notifId, item, data) {
242247
});
243248
}
244249

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

251-
var targetUrl = "https://polymarket.com/event/" + slug;
252-
253-
chrome.tabs.query({ url: "https://polymarket.com/*" }, function (tabs) {
254-
var match = null;
255-
for (var i = 0; i < tabs.length; i++) {
256-
if (tabs[i].url && tabs[i].url.indexOf(slug) !== -1) {
257-
match = tabs[i];
258-
break;
259-
}
260-
}
261-
if (match) {
262-
chrome.tabs.update(match.id, { active: true });
263-
chrome.windows.update(match.windowId, { focused: true });
264-
} else {
265-
chrome.tabs.create({ url: targetUrl });
257+
// Look up the watchlist to get the platform for this slug
258+
chrome.storage.local.get([STORE_KEYS.watchlist], function (result) {
259+
var watchlist = Array.isArray(result[STORE_KEYS.watchlist]) ? result[STORE_KEYS.watchlist] : [];
260+
var item = null;
261+
for (var i = 0; i < watchlist.length; i++) {
262+
if (watchlist[i].slug === slug) { item = watchlist[i]; break; }
266263
}
267-
chrome.notifications.clear(notifId);
264+
var platform = (item && item.platform) || "polymarket";
265+
var targetUrl = (typeof SynthPlatforms !== "undefined")
266+
? SynthPlatforms.marketUrl(platform, slug)
267+
: "https://polymarket.com/event/" + slug;
268+
269+
// Search across all supported platform tabs
270+
chrome.tabs.query({}, function (tabs) {
271+
var match = null;
272+
for (var j = 0; j < tabs.length; j++) {
273+
if (tabs[j].url && isSupportedUrl(tabs[j].url) && tabs[j].url.indexOf(slug) !== -1) {
274+
match = tabs[j];
275+
break;
276+
}
277+
}
278+
if (match) {
279+
chrome.tabs.update(match.id, { active: true });
280+
chrome.windows.update(match.windowId, { focused: true });
281+
} else {
282+
chrome.tabs.create({ url: targetUrl });
283+
}
284+
chrome.notifications.clear(notifId);
285+
});
268286
});
269287
});
270288

0 commit comments

Comments
 (0)