A read-only terminal dashboard that tracks crypto prediction markets across multiple venues, surfaces price disagreement between venues for the same market, and alerts when that disagreement crosses a configurable threshold.
MonitoBlitz is a personal trading-research tool — not a trading client. There are no order endpoints, no wallet integration, and no authenticated write paths. It only reads public market data.
Markets — group period — sort spread ↓ — filter ""
ASSET TF TTR RESOLVES STRIKE POLYMARKET HYPERLIQUID KALSHI SPREAD
── 15m ───────────────────────────────────────────────────────────────────────────────────────────────
BTC 15m 13m 11:30 UTC UP / $67,210 33 · 68 — 48 · 53 @67,210 ⚠ 15.0%
ETH 15m 13m 11:30 UTC UP / $1,884 30 · 71 — 44 · 56 @1,884 ⚠ 14.5%
── 1d ────────────────────────────────────────────────────────────────────────────────────────────────
BTC 1d 18h 06:00 UTC UP / $67,297 — 48 · 52 @67,297 — —
- What it does
- Supported venues
- How markets are matched
- Reading the table
- Install & run
- Configuration
- Key bindings
- Architecture
- Project layout
- Development
- Known limitations
- Aggregates the same market across venues into one row, side by side.
- Computes the spread — the gap between the highest and lowest YES odds for a matched market — and colours / alerts when it exceeds your threshold, with hysteresis to avoid flapping.
- Shows each venue's "target to beat" for direction (up/down-from-open) markets — the reference price the market resolves against — so you can see what each venue is actually pricing.
- Flags divergence (
⚠ mixed) when two venues anchor a direction market to meaningfully different reference prices, so an apples-to-oranges spread doesn't read as a real edge. - Runs entirely in your terminal, in memory, with no database and no API keys.
| Venue | Transport | Auth | Notes |
|---|---|---|---|
| Polymarket | Gamma REST API (polled) | None | Discovers active crypto strike & up/down markets. |
| Hyperliquid | /info HTTP API (polled) |
None | Direction markets; exposes a reference (target) price. |
| Kalshi | Public market-data REST (polled) | None | Series-driven; configured via event_series (e.g. KXBTC15M). |
All three use public, unauthenticated endpoints. Hyperliquid and Kalshi poll cadence is configurable (default 3s); Polymarket currently polls at a fixed 3s.
Two markets from different venues are treated as the same market if and only if these fields are equal:
- Asset (canonical upper-case ticker, e.g.
BTC) - Resolution time (truncated to the minute, UTC)
- Strike — the resolution condition:
above/belowa fixed price (e.g.> $67,199.99), orup/downfrom the window's open price (direction markets)
- Timeframe label (
5m,15m,1h,1d,weekly, …)
For direction markets, the fixed strike price is zeroed so that the same up/down market merges across venues even though each venue anchors to its own reference/open price. Markets with no cross-venue counterpart (singletons) are still shown — they just have no spread.
| Column | Meaning |
|---|---|
| ASSET | Crypto ticker. |
| TF | Timeframe of the market window. |
| TTR | Time-to-resolution (live countdown). |
| RESOLVES | Resolution time (UTC). |
| STRIKE | The resolution condition. > $X / < $X for fixed strikes; UP / $X / DOWN / $X for direction markets (consensus reference), or UP ⚠ mixed when venues disagree on the reference. |
YES · NO odds (0–100). For direction markets, also shows that venue's own reference as @price when available. |
|
| SPREAD | Highest − lowest YES odds across venues. Coloured by threshold; a trailing ⚠ marks a row whose venue references diverge (spread may be misleading). |
- A bold/red
⚠ <spread>%means the spread is at/above your alert threshold. —means no data for that venue/row (e.g. a venue doesn't list that market).
Requires Go 1.24+.
# clone, then from the repo root:
make run # build & run against live public APIs
# or build a binary:
make build
./monitoblitzOn first run, MonitoBlitz writes a default config.toml to the working
directory if none exists, then starts the TUI in the alternate screen. Quit with
q (or Ctrl-C).
Optional flags:
./monitoblitz --config /path/to/config.toml # use a specific config file
./monitoblitz --alert-pct 2.5 # override the alert threshold
./monitoblitz --theme dark # auto | light | dark
./monitoblitz --log /tmp/monitoblitz.log # write errors to a log fileConfiguration is resolved with this precedence (highest wins):
CLI flags → environment variables → config file → built-in defaults.
- Config file:
config.tomlin the working directory by default. Override with--config <path>or theMONITOBLITZ_CONFIGenv var. - Env overrides:
MONITOBLITZ_ALERT_PCT,MONITOBLITZ_THEME.
Example config.toml (abridged from the defaults written on first run):
[app]
assets = ["BTC", "ETH"]
providers = ["polymarket", "hyperliquid", "kalshi"]
ring_size = 200 # in-memory history depth per (market, venue)
[ui]
sort = "spread_desc" # spread_desc | asset | resolution_asc
filter = "" # asset prefix filter
show_alerts = true
theme = "auto" # auto | light | dark
[alerts]
alert_pct = "2" # spread % that triggers an alert
clear_pct = "1.5" # spread % at which an active alert clears (hysteresis)
bell = true # terminal bell on alert
notify = "auto" # auto | terminal-notifier | notify-send | off
target_mismatch_tolerance = "0.001" # relative gap (0.001 = 0.1%) above which venue references are "mixed"
[providers]
[providers.polymarket]
rest_url = "https://gamma-api.polymarket.com"
# Polymarket polls at a fixed 3s. The first-run file also writes ws_url
# and discover_every fields, but they are not wired up yet.
[providers.hyperliquid]
poll_every = "3s"
[providers.kalshi]
rest_url = "https://api.elections.kalshi.com/trade-api/v2"
poll_every = "3s"
event_series = ["KXBTC", "KXBTCD", "KXBTC15M", "KXETH", "KXETHD", "KXETH15M",
"KXXRPD", "KXXRP15M", "KXDOGED", "KXDOGE15M",
"KXSOL15M", "KXBNB15M", "KXHYPE15M"]Validation rejects: ring_size <= 0, alert_pct <= 0, clear_pct < 0 or
clear_pct > alert_pct, negative target_mismatch_tolerance, a negative
Hyperliquid poll_every, a non-positive Kalshi poll_every, and unknown
theme / sort / notify values. When clear_pct is 0, it derives to
0.75 × alert_pct.
| Key | Action |
|---|---|
q / Ctrl-C |
Quit |
↑ / k, ↓ / j |
Navigate rows |
enter |
Drill into the selected market |
esc |
Close drill-down |
s |
Cycle sort (spread ↓ / asset / resolves ↑) |
g |
Toggle grouping (period ↔ venue) |
a |
Toggle the alerts pane |
c |
Clear the alerts log |
/ |
Filter by asset prefix |
r |
Retry providers |
? |
Toggle help |
Approach: a pub/sub in-memory store with a Bubble Tea bridge. Providers push snapshots into a shared store; matchers subscribe and emit match updates; an alerter and the TUI each consume those updates independently.
provider PM ─┐
provider HL ─┼─► snapshots ─► store ─► subscribe ─► Matcher ─┬─► TUI bridge ─► tea.Msg
provider KAL ─┘ (ring buffer per │ └─► Alerter ─► events ─► bell / OS notify
market+venue) └─► (second Matcher for the alerter)
- Providers normalise each venue's raw markets into a common
Snapshot(domain.Snapshot) and push them onto a channel. - Store (
internal/store) keeps a bounded ring buffer per(MarketKey, Venue)and fans out to subscribers. History feeds the drill-down view. - Matcher (
internal/matcher) groups snapshots byMarketKeyand emits aMatchUpdate(quotes per venue + computed spread). Two independent matchers are wired — one for the TUI, one for the alerter — so consumers don't contend. - Alerter (
internal/alerter) applies the threshold with hysteresis, rings the terminal bell, and best-effort OS notifications (terminal-notifieron macOS /notify-sendon Linux when present). - TUI (
internal/tui) is the Bubble Tea model: table, grouping/sorting, filtering, drill-down, alerts pane, and live countdowns.
All identity types live in internal/domain and import nothing outside the
standard library and shopspring/decimal, keeping the core decoupled.
MonitoBlitz/
├── cmd/monitoblitz/main.go # entrypoint — wires providers, store, matchers, alerter, TUI
├── internal/
│ ├── domain/ # MarketKey, Snapshot, Strike, MatchUpdate (no internal imports)
│ ├── provider/ # Provider interface + retry helpers
│ │ ├── polymarket/ # Gamma REST client + normalisation
│ │ ├── hyperliquid/ # /info client + market description parsing
│ │ └── kalshi/ # public REST poll, series-driven discovery
│ ├── netutil/ # shared HTTP client + retry/backoff helpers
│ ├── store/ # in-memory snapshot store, ring buffer per (market, venue)
│ ├── matcher/ # groups snapshots by MarketKey, emits MatchUpdate
│ ├── alerter/ # threshold + hysteresis; bell + OS notify
│ ├── config/ # TOML load + env + flag overrides, with validation
│ ├── clock/ # Clock interface (real + fake for tests)
│ ├── logging/ # optional file logger
│ └── tui/ # Bubble Tea model, views, table, styles, bridge
├── docs/superpowers/ # design specs & implementation plans
├── Makefile
└── config.toml # written on first run if absent
The Makefile is the entrypoint for all common tasks:
make build # go build ./...
make test # go test -race ./...
make lint # golangci-lint run (skipped if not installed)
make run # go run ./cmd/monitoblitz
make tui-snapshot # regenerate TUI golden snapshots (-update)
make fmt # gofmt -w .
make tidy # go mod tidyConventions followed in this codebase:
- TDD — tests are written before implementation.
- DRY / KISS — small, focused, single-responsibility files.
- Granular commits; feature work happens on dedicated branches.
Built with Bubble Tea /
Lip Gloss,
shopspring/decimal, and
BurntSushi/toml.
- In-memory only. No persistence — all state is lost on restart.
- Polymarket has no published reference price. PM direction markets resolve
on a Chainlink price stream whose open price isn't exposed in the market data,
so PM rows don't show an
@-target (deriving one from another source is under discussion). Kalshi (floor_strike) and Hyperliquid (market description) do publish theirs. - Read-only. No trading, no order placement, no authenticated endpoints.
- No CI.
make testmust be green locally before each commit.