# The Real Cost of Running a Prediction Market Bot ($7/Month Infrastructure Deep Dive)

## The Setup

We run 5 trading bots that collectively cover 8 sports on Polymarket: NBA, NCAAMB, MLB, NHL, CS2, LoL, Tennis, and Soccer. The bots execute trades, the website displays live results, and the entire system runs 24/7.

Total infrastructure cost: about $7 per month for the VPS, plus $5/month for the odds API. Everything else is free.

Here's exactly how it works, what runs where, and why we made each decision.

## The Two-Machine Architecture

We split our system between a local Mac and a remote VPS. This isn't arbitrary — there's a security reason.

**Local Mac (trading execution):** All 5 trading bots run locally. They hold the Polymarket private key (which controls the wallet) and place real orders. We never put the private key on a remote server. If the VPS gets compromised, the attacker can deface the website but can't steal funds.

**VPS (everything else):** The website, API, signal engine, data collection, trade resolution, and monitoring all run on a Hetzner CX22 ($7/month, 2 vCPUs, 4GB RAM, located in Ashburn, Virginia). This machine serves the public website, polls ESPN for live scores, collects sportsbook odds, resolves trades, and runs health monitoring.

The trade log (trades.jsonl) syncs from the Mac to the VPS via the deploy script, so the website always has the latest trading results.

## What Runs on the VPS

### The API Server (FastAPI + Uvicorn)

The main application is a single FastAPI process running under systemd. It handles:

- The public website (course page, results, pricing, blog, docs)
- User authentication (email + password, session cookies)
- The prediction API (real-time win probabilities, fair lines, edge signals)
- The dashboard (live edge scanner, game monitor, course progress tracking)
- Stripe payment processing (course purchases, API subscriptions)
- WebSocket connections for real-time price updates

FastAPI was chosen because it's async-native (important for WebSocket handling), has excellent performance for a Python framework, and the typing system makes the API self-documenting.

Uvicorn runs as a single worker. We don't need multiple workers because the VPS only handles ~100 requests per hour from actual users. The real-time components (ESPN polling, Polymarket WebSocket) are async tasks within the same process.

### The Signal Engine

The signal engine is an async background task that runs inside the API process. Every 5 seconds, it:

1. Polls ESPN for live scores across all 7 sports
2. Receives Polymarket price updates via WebSocket (real-time, sub-second)
3. Runs the win probability model on every live game
4. Compares model predictions to market prices
5. Emits edge signals when disagreements exceed the threshold

The engine maintains a live game state for every active game: score, time remaining, period, home/away Elo, model prediction, and current market price. This state is what the dashboard displays.

### Multi-Venue Odds Collection

We poll The Odds API every 2 minutes for sportsbook prices from DraftKings, FanDuel, BetMGM, Caesars, and others. These prices are devigged (the bookmaker's profit margin is removed) and compared to both our model and Polymarket prices.

The odds data is saved to daily Parquet files for historical analysis. We're building a dataset of cross-venue price disagreements that will eventually become a product.

The Odds API costs $5/month for the 100K request tier, which gives us about 3,000 requests per day — enough to poll 6 sports every 2 minutes during game hours.

### Trade Resolution

A cron job runs every 15 minutes that checks all pending trades against the Polymarket settlement API. When a market closes, it looks up the winning token and marks our trade as won or lost with the correct P&L.

This is important because our bots log trades at entry time with `resolved=false`. The resolution cron is what turns "we bought this position" into "we won $1.20" or "we lost $0.55." Without it, the results page would show all trades as pending forever.

The resolution logic: query the Polymarket CLOB client for the market's condition ID, check if the market is closed, identify the winning token, and compare it to our position's token ID. If they match, we won. If not, we lost. P&L is calculated as (100 - entry_price) for wins and (-entry_price) for losses.

### HTTPS and Reverse Proxy

Caddy handles HTTPS termination and reverse proxying. It automatically provisions and renews Let's Encrypt certificates for zenhodl.net and www.zenhodl.net. A legacy `api.zenhodl.net` subdomain still 301-redirects to zenhodl.net for backwards compatibility.

Caddy was chosen over Nginx because the configuration is 15 lines instead of 50, and it handles SSL automatically with zero configuration. The entire Caddyfile:

```
zenhodl.net, www.zenhodl.net {
    reverse_proxy localhost:8000
    encode gzip
}

api.zenhodl.net {
    redir https://zenhodl.net{uri} permanent
}
```

### Data Collection Crons

Several cron jobs handle periodic data tasks:

- **Cache refresh (every 4 hours):** Rebuilds the Polymarket token cache (14,000+ tokens across all sports). Maps event titles to token IDs so the signal engine can find the right market for each game.

- **ESPN scraping (daily at 6 AM UTC):** Scrapes the previous day's completed games and adds them to the training parquets. This keeps the model's training data current without running a full multi-year scrape.

- **Health check (every 5 minutes):** Curls the /v1/health endpoint and sends a Discord webhook alert if the server is down or the ESPN poller has stalled.

- **Database backup (daily at 4 AM):** Copies users.db and trades.jsonl to a timestamped backup directory.

- **Trade resolution (every 15 minutes):** The resolver described above.

### Monitoring

A bot heartbeat script runs every 30 minutes and checks:

- Is the API server responding?
- How long since the last ESPN poll?
- How long since the last WebSocket price update?
- How many active games are being tracked?
- Are any trades stuck in pending state for more than 24 hours?

If any check fails, a Discord webhook fires with the details. We've caught several issues this way: a WebSocket reconnection bug that silently dropped the connection, an ESPN rate limit that paused polling for 2 hours, and a trade resolution failure on a cancelled market.

## What Runs on the Local Mac

### The 5 Trading Bots

Each bot is started manually from the terminal:

```bash
python3 moneyline_wp_bot.py --mode live --sports NBA,NCAAMB,MLB --duration 480
python3 cs2_wp_bot.py --mode live --duration 480
python3 lol_wp_bot.py --mode live --duration 480
python3 soccer_wp_bot.py --mode live --leagues EPL,LALIGA,LIGUE1 --duration 480
python3 tennis_wp_bot.py --mode live --tours ATP,WTA,WTT --duration 480
```

Each bot connects to its own data sources (ESPN, HLTV/bo3.gg, lolesports, etc.), runs the model, and executes trades through the Polymarket CLOB API. They write fills to trades.jsonl, which syncs to the VPS.

The bots run for 8 hours (480 minutes) and then exit. Systemd would restart them, but we run them manually because we want to review the day's trades before starting the next session.

### The Hedge Overlay

A separate process monitors all open positions from the WP bots and looks for hedging opportunities. If a game swings and the opposing side drops to a price where buying it locks in guaranteed profit (pair cost below $1.00), the overlay buys the hedge.

Example: We bought Team A at 68 cents. Team A falls behind and Team B drops to 25 cents. Pair cost: 68 + 25 + 2 (fee) = 95 cents. Guaranteed 5-cent profit per share regardless of outcome.

## The Tech Stack in Full

**Languages:** Python 3.13 (everything), Jinja2 (templates), Alpine.js (frontend interactivity)

**Web framework:** FastAPI + Uvicorn

**Database:** SQLite with WAL mode (users, course progress, ratings, usage tracking)

**Storage:** JSONL for trade logs, Parquet for training data and odds snapshots

**Authentication:** Email + bcrypt passwords, session cookies, HMAC-signed download tokens

**Payments:** Stripe Checkout (18 products, webhooks for fulfillment)

**Email:** Resend SMTP (transactional emails, drip campaigns)

**Monitoring:** Discord webhooks (alerts), Caddy logs, systemd journal

**CSS:** Pre-built Tailwind (37KB, no CDN — compiled at build time)

**CDN:** Cloudflare (free tier, DNS proxy for caching and DDoS protection)

**Hosting:** Hetzner CX22 ($7/month)

**Odds data:** The Odds API ($5/month for 100K requests)

## Performance Optimization

The website scores 79 on Google PageSpeed (mobile) after optimization:

- **GZip middleware:** 54KB page compresses to 11KB
- **1-year immutable cache:** Static assets (CSS, JS, images) cached aggressively
- **Dashboard mockup as WebP image:** Replaced 90 HTML table nodes with a 17KB image
- **Deferred Google Tag Manager:** Moved to window.onload to reduce main thread blocking
- **CSS and image preloading:** Browser starts downloading critical resources immediately

The remaining bottleneck is LCP (Largest Contentful Paint) at 5.3 seconds on simulated slow 4G. This is network-limited, not server-limited — our server TTFB is 23ms.

## What This Costs

Monthly recurring:
- Hetzner VPS: $7
- The Odds API: $5
- Cloudflare: $0 (free tier)
- Domain: ~$1/month (annual billing)
- Caddy SSL: $0 (Let's Encrypt, automatic)
- Total: **~$13/month**

One-time costs absorbed into development:
- Mac (already owned, development machine doubles as trading server)
- Python, FastAPI, SQLite, Caddy, systemd: all free/open-source

The bot P&L needs to cover $13/month to break even on infrastructure. At current performance (~$60-80/month across all bots on small position sizes), we're profitable after infrastructure costs.

## What We'd Change

**If starting over:** We'd use a VPS in the same region as Polymarket's matching engine (likely US-East) from day one. Our early deployments were on European servers, adding 200ms of latency to every trade.

**If scaling:** We'd add a second VPS for redundancy and move from SQLite to PostgreSQL. SQLite handles our current load fine but would struggle with concurrent writes from multiple processes.

**If budget allowed:** A co-located server near Polymarket's infrastructure would cut trade latency from 6 seconds to under 1 second. At our current trade volume, this doesn't justify the cost, but it would become the bottleneck if we scaled position sizes significantly.

---

*The complete deployment process is covered in Module 6 of our course at zenhodl.net/course. From setting up the VPS to configuring systemd services to monitoring with Discord — everything described here is in the notebooks.*
