Prediction Market Terminal: Build a CLI Trading Monitor with Python
Not everything needs a browser. If you’re monitoring prediction market odds from a headless VPS, an SSH session, or just prefer terminals over GUIs, a CLI monitor does the job in 50 lines of Python.
In this tutorial, you’ll build a prediction market terminal that displays live odds from Polymarket, Kalshi, Pinnacle, and DraftKings in a color-coded table — with edge detection when prediction markets diverge from sharp sportsbooks. No browser, no Streamlit, no JavaScript.
Why CLI Over a Dashboard
The Streamlit dashboard we built previously is great for visual analysis. But a terminal monitor has its own advantages:
| Feature | Streamlit Dashboard | CLI Terminal |
|---|---|---|
| Runs on headless VPS | Requires browser or tunneling | ✅ SSH-native |
| Resource usage | ~100MB+ (browser + Python) | ~20MB (Python only) |
| Composable | Standalone web app | ✅ Pipe to files, cron, alerts |
| Setup | pip install streamlit plotly | pip install rich requests |
| Best for | Visual analysis, sharing | Monitoring, logging, automation |
If you’re running a trading bot on a $5/month VPS, you don’t want to spin up a browser to check odds. You want python monitor.py and a clean table in your terminal.
What We’re Building
A Rich-powered terminal app that:
- Fetches upcoming fixtures from OddsPapi
- Displays Polymarket vs Pinnacle vs Bet365 vs DraftKings implied probabilities
- Color-codes edges (green = Polymarket lower than sharp, red = Polymarket higher)
- Auto-refreshes with Rich Live display
- Runs in any terminal — local, SSH, tmux, screen
Step 1: Install Dependencies
pip install rich requests
Two packages. That’s it. Rich handles the colored tables and live refresh. Requests handles the API calls.
Step 2: Fetch Prediction Market Odds
Same API calls as the dashboard tutorial — OddsPapi returns Polymarket, Kalshi, Pinnacle, and 350+ other bookmakers in a single response.
import requests
API_KEY = "YOUR_API_KEY"
BASE = "https://api.oddspapi.io/v4"
def fetch_fixtures(sport_id=10, tournament_id=7, limit=20):
"""Fetch upcoming fixtures. Default: Champions League soccer."""
params = {"apiKey": API_KEY, "sportId": sport_id,
"tournamentId": tournament_id, "limit": limit}
r = requests.get(f"{BASE}/fixtures", params=params, timeout=15)
r.raise_for_status()
return r.json()
def fetch_odds(fixture_id):
"""Fetch odds from all bookmakers for a fixture."""
r = requests.get(f"{BASE}/odds",
params={"apiKey": API_KEY, "fixtureId": fixture_id},
timeout=15)
r.raise_for_status()
return r.json().get("bookmakerOdds", {})
Step 3: Build the Edge Detection Logic
The edge calculation is simple: compare Polymarket’s implied probability against Pinnacle’s sharp line. A positive difference means Polymarket thinks the outcome is more likely than the sharps do.
BOOKS = ["polymarket", "pinnacle", "bet365", "draftkings"]
MARKET_ID = "101" # Full Time Result (1X2) for soccer
OUTCOMES = {"101": "Home", "102": "Draw", "103": "Away"}
def implied(price):
"""Convert decimal odds to implied probability."""
return round(1 / price * 100, 1) if price and price > 0 else None
def get_probs(bo, slug):
"""Extract implied probabilities for a bookmaker."""
if slug not in bo:
return {}
market = bo[slug].get("markets", {}).get(MARKET_ID, {})
outs = market.get("outcomes", {})
probs = {}
for oid, label in OUTCOMES.items():
price = (outs.get(oid, {}).get("players", {})
.get("0", {}).get("price"))
p = implied(price)
if p:
probs[label] = p
return probs
def calc_edge(poly_prob, pin_prob):
"""Calculate edge: Polymarket vs Pinnacle divergence."""
if poly_prob is None or pin_prob is None:
return None
return round(poly_prob - pin_prob, 1)
Step 4: Build the Rich Table Display
Rich tables support per-cell styling, which makes edge detection visual — green for opportunities where Polymarket underprices vs sharps, red for where it overprices.
from rich.table import Table
from rich.text import Text
EDGE_THRESHOLD = 5.0
def build_table(fixtures_data):
"""Build a Rich table comparing prediction market vs sportsbook odds."""
table = Table(title="Prediction Market Monitor", show_lines=True)
table.add_column("Match", style="bold white", min_width=30)
table.add_column("Outcome", style="cyan")
table.add_column("Polymarket", justify="right")
table.add_column("Pinnacle", justify="right")
table.add_column("Bet365", justify="right")
table.add_column("DraftKings", justify="right")
table.add_column("Edge (pp)", justify="right")
for match_name, bo in fixtures_data:
poly = get_probs(bo, "polymarket")
pin = get_probs(bo, "pinnacle")
b365 = get_probs(bo, "bet365")
dk = get_probs(bo, "draftkings")
if not poly or not pin:
continue
for label in ["Home", "Draw", "Away"]:
edge = calc_edge(poly.get(label), pin.get(label))
edge_text = Text(f"{edge:+.1f}" if edge else "—")
if edge and abs(edge) >= EDGE_THRESHOLD:
edge_text.stylize("bold red" if edge > 0 else "bold green")
table.add_row(
match_name if label == "Home" else "",
label,
f"{poly.get(label, '—')}%",
f"{pin.get(label, '—')}%",
f"{b365.get(label, '—')}%",
f"{dk.get(label, '—')}%",
edge_text,
)
return table
Step 5: Complete Monitor with Auto-Refresh
Here’s the complete monitor. Save this as monitor.py:
import time, requests
from rich.live import Live
from rich.table import Table
from rich.text import Text
from rich.console import Console
API_KEY = "YOUR_API_KEY"
BASE = "https://api.oddspapi.io/v4"
MARKET_ID = "101" # 1X2 for soccer, use "111" for NBA moneyline
OUTCOMES = {"101": "Home", "102": "Draw", "103": "Away"}
EDGE_THRESHOLD = 5.0
REFRESH = 60 # seconds
def implied(price):
return round(1 / price * 100, 1) if price and price > 0 else None
def get_probs(bo, slug):
if slug not in bo:
return {}
market = bo[slug].get("markets", {}).get(MARKET_ID, {})
outs = market.get("outcomes", {})
return {label: implied(outs.get(oid, {}).get("players", {})
.get("0", {}).get("price"))
for oid, label in OUTCOMES.items()
if implied(outs.get(oid, {}).get("players", {})
.get("0", {}).get("price"))}
def fetch_data():
params = {"apiKey": API_KEY, "sportId": 10,
"tournamentId": 7, "limit": 300}
fxs = requests.get(f"{BASE}/fixtures", params=params, timeout=15).json()
now = time.strftime("%Y-%m-%dT%H:%M:%S")
fxs = [f for f in fxs if f.get("startTime", "") >= now][:10]
data = []
for fx in fxs:
bo = requests.get(f"{BASE}/odds",
params={"apiKey": API_KEY,
"fixtureId": fx["fixtureId"]},
timeout=15).json().get("bookmakerOdds", {})
if "polymarket" in bo:
name = f"{fx['participant1Name']} vs {fx['participant2Name']}"
data.append((name, bo))
return data
def build_table(data):
t = Table(title=f"Prediction Market Monitor — "
f"{time.strftime('%H:%M:%S')}", show_lines=True)
t.add_column("Match", style="bold white", min_width=28)
t.add_column("Outcome", style="cyan")
t.add_column("Polymarket", justify="right")
t.add_column("Pinnacle", justify="right")
t.add_column("Bet365", justify="right")
t.add_column("DraftKings", justify="right")
t.add_column("Edge", justify="right")
for name, bo in data:
poly = get_probs(bo, "polymarket")
pin = get_probs(bo, "pinnacle")
b365 = get_probs(bo, "bet365")
dk = get_probs(bo, "draftkings")
if not poly or not pin:
continue
for label in OUTCOMES.values():
pp, pn = poly.get(label), pin.get(label)
edge = round(pp - pn, 1) if pp and pn else None
et = Text(f"{edge:+.1f}pp" if edge else "—")
if edge and abs(edge) >= EDGE_THRESHOLD:
et.stylize("bold red" if edge > 0 else "bold green")
t.add_row(
name if label == list(OUTCOMES.values())[0] else "",
label,
f"{pp}%" if pp else "—",
f"{pn}%" if pn else "—",
f"{b365.get(label, '—') if b365 else '—'}%"
if b365.get(label) else "—",
f"{dk.get(label, '—') if dk else '—'}%"
if dk.get(label) else "—",
et,
)
return t
console = Console()
console.print("[bold]Starting prediction market monitor...[/]")
console.print(f"[dim]Refresh: {REFRESH}s | Edge threshold: "
f"{EDGE_THRESHOLD}pp | Ctrl+C to quit[/]\n")
with Live(console=console, refresh_per_second=1) as live:
while True:
try:
data = fetch_data()
live.update(build_table(data))
time.sleep(REFRESH)
except KeyboardInterrupt:
break
except Exception as e:
console.print(f"[red]Error: {e}[/]")
time.sleep(10)
Running the Monitor
python monitor.py
You’ll see a table like this in your terminal:
┌──────────────────────────────┬─────────┬─────────────┬──────────┬────────┬────────────┬──────────┐
│ Match │ Outcome │ Polymarket │ Pinnacle │ Bet365 │ DraftKings │ Edge │
├──────────────────────────────┼─────────┼─────────────┼──────────┼────────┼────────────┼──────────┤
│ Galatasaray vs Liverpool │ Home │ 31.5% │ 21.7% │ 22.2% │ 22.0% │ +9.8pp │
│ │ Draw │ 19.5% │ 24.0% │ 23.8% │ 24.4% │ -4.5pp │
│ │ Away │ 56.0% │ 57.6% │ 55.6% │ 58.8% │ -1.6pp │
├──────────────────────────────┼─────────┼─────────────┼──────────┼────────┼────────────┼──────────┤
│ Real Madrid vs Man City │ Home │ 36.0% │ 28.6% │ 29.4% │ 28.6% │ +7.4pp │
│ │ Draw │ 26.0% │ 25.3% │ 24.4% │ 25.6% │ +0.7pp │
│ │ Away │ 41.0% │ 49.5% │ 47.6% │ 50.0% │ -8.5pp │
└──────────────────────────────┴─────────┴─────────────┴──────────┴────────┴────────────┴──────────┘
Red edges mean Polymarket overprices the outcome vs Pinnacle. Green edges mean Polymarket underprices it. The table refreshes every 60 seconds without clearing your terminal history.
Bonus: Pipe to a Log File
Since this is a CLI tool, you can pipe the output to a file for historical tracking:
# Log edges to a file every minute
python monitor.py 2>&1 | tee -a prediction_edges.log
Or run it in a tmux session on your VPS:
tmux new -s monitor
python monitor.py
# Ctrl+B, D to detach — it keeps running
Adapting for NBA
To monitor NBA moneylines instead of soccer 1X2, change the config at the top:
# NBA configuration
MARKET_ID = "111" # Moneyline
OUTCOMES = {"111": "Home", "112": "Away"}
# In fetch_data(), change:
params = {"apiKey": API_KEY, "sportId": 11, # Basketball
"tournamentId": 132, # NBA
"limit": 300}
Polymarket covers NBA moneylines on every game — you’ll see the same kind of divergences between crowd pricing and sharp books.
Dashboard vs Terminal: When to Use Each
| Scenario | Use Dashboard | Use Terminal |
|---|---|---|
| Visual analysis of odds | ✅ Plotly charts | |
| Sharing with team | ✅ Browser-based | |
| Headless VPS monitoring | ✅ SSH-native | |
| Cron job / automation | ✅ No browser needed | |
| Low resource usage | ✅ 20MB vs 100MB+ | |
| Piping to log files | ✅ stdout friendly |
Both tools hit the same OddsPapi API. The data is identical — it’s just how you view it.
Why OddsPapi for Terminal Monitoring
This monitor works because OddsPapi aggregates 350+ bookmakers — including prediction market exchanges like Polymarket and Kalshi — into a single API. You don’t need separate integrations for each source. One API key, one request, and you get every price from every bookmaker on the fixture.
The free tier includes historical data for backtesting, and when you need real-time feeds with sub-second latency, upgrade to WebSockets.
Build Your Monitor in 10 Minutes
50 lines of Python, two pip packages, and one API key. That’s all you need to monitor prediction market edges from any terminal in the world.
Get your free API key and run python monitor.py. If you prefer charts over tables, check out our Streamlit dashboard tutorial instead.