Prediction Market Terminal: Build a CLI Trading Monitor with Python

Prediction Market Terminal - OddsPapi API Blog
How To Guides March 13, 2026

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.