Repository for a Polymarket + Kalshi cross-exchange trading research and arbitrage workflow.
- Keep work incremental and explain-first.
- Use PMXT as the exchange interface layer for both market data and trading.
- Avoid getting lost in over-complex agent-driven implementations; build one small step at a time.
- Track changes in:
agent_journal.mdfor high-level agent-facing notes
- User will track their own changes in:
PMXT details are tracked in TODO.md.
The core contract remains:
- PMXT is used as a Python SDK + local sidecar abstraction.
- For outcome-level calls (orderbook/trades/ohlcv), use
outcome_id. - For setup/sanity steps (startup path, health checks, caveats), see the TODO task list.
The full Python SDK API reference is in docs/PMXT_PYTHON_API_REFERENCE.md.
The repo now includes a Phase 1 web skeleton:
- FastAPI backend (
app/main.py) - Server-rendered UI template (
templates/index.html) - Local SQLite bootstrap (
app/db.py)
python3available on your PATH- Internet access for first-time dependency install
make setupThis creates .venv, upgrades pip, and installs pinned dependencies from requirements.txt.
make init-dbThis creates data/pair_manager.db.
make run-webDefault URL:
http://127.0.0.1:8011/
Health endpoint:
http://127.0.0.1:8011/healthz
Use saved pair outcome links from data/pair_manager.db and stream orderbooks via PMXT:
.venv/bin/python scripts/ws_orderbook_smoke.py --max-streams 10 --updates-per-stream 1Notes:
- Uses
active_only=true,include_expired=falseby default. - Add
--include-inactiveand/or--include-expiredto widen selection. - Kalshi streaming requires auth; this repo supports
KALSHI_API_KEYplusKALSHI_PRIVATE_KEY=./key.pem.
Run the API server first (pair discovery source), then run the continuous alert watcher:
make run-webmake run-arb-alertsDirect script usage with explicit thresholds:
.venv/bin/python scripts/ws_arb_alerts.py \
--api-base-url http://127.0.0.1:8011 \
--arb-threshold 0.02 \
--deviation-threshold 0.03What it alerts on:
[ALERT_ARB_CROSS]:1 - (YES_ask + NO_ask)across markets, both directions.[ALERT_ARB_WITHIN]:1 - (YES_ask + NO_ask)within Kalshi and within Polymarket.[ALERT_DEVIATION]:abs(ask_kalshi - ask_polymarket)for the same semantic token (Pandnot_P).
Example threshold trigger:
- If
YES_ask = 0.60andNO_ask = 0.38, edge =1 - (0.60 + 0.38) = 0.02, so it alerts at default--arb-threshold 0.02.
Notes:
- Uses active, non-expired mappings by default (
--include-inactive/--include-expiredare debug overrides). - Uses top-of-book asks only in v1 (no depth simulation, fees, or slippage).
- Uses per-opportunity cooldown (
--cooldown-seconds, default30) and staleness filtering (--book-stale-seconds, default15).
Base URL (local):
http://127.0.0.1:8011
General notes:
- Request/response format is JSON unless noted.
- Most endpoints currently return HTTP
200with a predictable error payload for validation/runtime failures:{"ok": false, "error": "...", "error_code": "..."}.
Common error_code values:
INVALID_URLINVALID_STATUS_FILTERPMXT_PREVIEW_FAILEDPAIR_SAVE_FAILEDPAIR_UPDATE_FAILEDPAIR_NOT_FOUNDMONITORING_QUERY_FAILED
Lightweight service health check.
Example response:
{
"status": "ok",
"db_path": "data/pair_manager.db"
}Normalize one Kalshi URL and one Polymarket URL into lookup identifiers.
Request:
{
"kalshi_url": "https://kalshi.com/markets/kxsenateild/ild/kxsenateild-26",
"polymarket_url": "https://polymarket.com/event/illinois-democratic-senate-primary-winner"
}Success response:
{
"ok": true,
"normalized": {
"kalshi": {"lookup_value": "kxsenateild-26"},
"polymarket": {"lookup_value": "illinois-democratic-senate-primary-winner"}
}
}Fetch market/event details from PMXT for both exchanges and return normalized preview payload.
Request:
{
"kalshi_url": "https://kalshi.com/markets/kxsenateild/ild/kxsenateild-26",
"polymarket_url": "https://polymarket.com/event/illinois-democratic-senate-primary-winner"
}Success response includes:
normalizedpreview.kalshipreview.polymarket
Create and persist one pair set plus selected mappings.
Request fields:
kalshi_url,polymarket_urlnormalized(from normalize/preview)preview(from preview)matches[]with:kalshi_market_idpolymarket_market_idrelation_type(same_directionorinverse)active(true/false)
recurrence_intent(nullable)expires_at(nullableYYYY-MM-DD)
Success response:
{
"ok": true,
"pair_id": 12,
"saved_link_rows": 4
}List saved pair sets for the list/edit UI.
Load one saved pair set, including stored market snapshots and outcome links.
Update an existing pair set using the same request body shape as POST /api/pairs.
Delete a pair set and cascaded child rows.
Use this endpoint for downstream monitoring services:
GET /api/monitoring/pairs?active_only=true&include_expired=false
Default behavior:
- Includes only active links (
active_only=true). - Excludes expired pairs (
include_expired=false).
Response shape (stable contract):
{
"ok": true,
"active_only": true,
"include_expired": false,
"error": null,
"error_code": null,
"pairs": [
{
"pair_id": 12,
"recurrence_intent": "weekly",
"expires_at": "2026-12-31",
"mappings": [
{
"relation_type": "same_direction",
"legs": [
{"exchange": "kalshi", "market_id": "KX...", "outcome_id": "KX..."},
{"exchange": "polymarket", "market_id": "12345", "outcome_id": "67890"}
]
}
]
}
]
}Notes:
pairs[].mappings[]is outcome-link level (forsame_direction, typically YES and NO rows).- Each mapping has exactly two legs: one Kalshi leg and one Polymarket leg.
- If duplicate mappings exist across multiple pair sets, the monitoring endpoint keeps only the latest pair-set version (highest
pair_id).
make check-pmxtThis validates PMXT + sidecar connectivity with a minimal fetch on Polymarket and Kalshi.
If you want a different port temporarily:
PORT=8020 make run-web