First month 25% off for new traders — code

How to Set Up a Polymarket Bot on a VPS

Written by TradoxVPS Engineering Team
|
How to set up a Polymarket bot on a VPS — diagram of a Dublin VPS bot exchanging WebSocket book deltas at 13 to 15 milliseconds median and signed orders at 21 to 23 milliseconds with Polymarket's CLOB.

The case for automating Polymarket isn’t a war story — it’s the on-chain record. The academic analysis of a year of Polymarket data (arXiv:2508.03474) measured roughly $40 million in realized arbitrage profit, with the top wallet earning about $2.0 million across 4,049 trades — roughly $500 per trade, thousands of times, on schedule. Independent wallet-profitability studies show the winning minority skews heavily toward systematic, automated strategies while losing manual traders enter after prices have already moved. Edges on Polymarket are small, repeatable, and harvested by software that never sleeps.

This guide is the full build: understanding Polymarket’s API architecture, provisioning a server, installing py-clob-client, authenticating a wallet, writing strategy logic with real position sizing, surviving rate limits and disconnects, deploying for 24/7 operation, and hardening the thing so a leaked key doesn’t end your week. Python 3.9+, a Polygon wallet, USDC for capital, and a small amount of POL for gas are the prerequisites. (Never traded manually yet? Start with the beginner guide first — a bot amplifies a process, including a bad one.)

Understanding Polymarket’s Architecture Before You Write a Line of Code

Bots fail most often at the authentication layer and the API layer, not the strategy layer. Knowing how the pieces fit saves hours of debugging.

The Three APIs and What Each One Does

Polymarket splits functionality across three APIs plus WebSocket streams. A complete bot touches all of them.

The CLOB API (https://clob.polymarket.com) is the trading engine: order placement, cancellation, live order book data, fill confirmations. Matching happens off-chain for speed; settlement lands on-chain on Polygon. Your network path determines how fast your orders arrive after you send them — and on the venue side, our benchmark observed the API root’s own p99 stretching to 250–650 ms across all providers tested, a reminder that “simultaneous” execution logic must tolerate server-side tails too.

The Gamma API (https://gamma-api.polymarket.com) is the discovery layer: available markets, event metadata, outcome descriptions, token IDs. Indexing lags roughly a second, so use it to find markets, never to price them.

The Data API (https://data-api.polymarket.com) is account-level: positions, trade history, P&L. Monitoring, not execution.

APIEndpointPrimary useLatency sensitivity
CLOB APIclob.polymarket.comOrders, cancels, order bookHigh — the execution path
Gamma APIgamma-api.polymarket.comMarket discovery, token IDsLow — poll every 60s
Data APIdata-api.polymarket.comPositions, history, P&LLow — monitoring only
CLOB WebSocketws-subscriptions-clob.polymarket.com/ws/Live book deltas, fillsCritical — all real-time data
Polymarket bot API architecture — the bot connects to the CLOB WebSocket for real-time data, the CLOB API for orders, the Gamma API for market discovery, the Data API for monitoring, with settlement on Polygon.

Endpoints, channels, and message schemas evolve — treat docs.polymarket.com as the source of truth for the current contract, and our V2 migration guide for what changed and why your older snippets broke.

How the CLOB Matches Orders — and What It Costs in 2026

Makers post resting limit orders; takers fill against them. Prices run $0.01–$0.99 as implied probability; winning contracts pay $1.00 at resolution.

The fee structure (V2, effective March 30, 2026) is the single most bot-relevant fact on this page: makers pay 0% on all markets. Takers pay a category formula — rate × P × (1−P) — peaking at 50¢: roughly 1.80% on crypto, 1.00% on politics/finance/tech, 0.75% on sports, with geopolitics fee-free, and maker rebate programs on top (official schedulewhy the curve is shaped that way). The implication is mechanical: a pure market-order bot in crypto markets needs a reliable edge above 1.80% per trade before slippage just to break even, while a maker bot trades free and collects rebates. Order-type choice is fee strategy.

Where Latency Actually Comes From (Measured, Not Guessed)

You’ll find confident claims online about which AWS region Polymarket “runs in” — earlier versions of this site’s own pages managed to name two different regions, which tells you what those claims are worth. Here’s what we actually know, because we measured it: the CLOB API is CDN-fronted (our probes terminate at Cloudflare edges), and in our June 2026 four-provider benchmark — one purchased box per provider, raw JSON downloadable — Dublin machines saw the live feed at ~13–15 ms median and round-tripped the order book warm at ~21–23 ms median. Nobody measured single-digit milliseconds, including us. A US-based box pays the Atlantic crossing on top — tens of milliseconds of physics no provider removes — which matters in proportion to how contested your edge is. Between well-placed bots, the race is decided in the tails: order-path p99 ranged 37–55 ms across the four boxes we tested, and the box with the smallest worst-case wins the ties.

So the honest location guidance: for Polymarket-focused bots, a European box with a short, measured path is the right call — our Dublin location is built for exactly this, on Ryzen 9 9950X hosts with DDR5 and NVMe and a 99.999% uptime target — and the way to verify that sentence is the same 20-minute probe we publish, run on a free 3-day demo before you pay us or anyone.

Choosing and Provisioning Your VPS

The server is part of your execution quality, but the sizing logic is simpler than vendors imply. A Polymarket bot is network-bound, not compute-bound: the hot loop (read delta → decide → sign → submit) runs on one core, so single-core speed beats core count, and more cores buy you more bots, not a faster bot. A single bot watching 10–20 markets runs comfortably on 2 cores and a few GB of RAM — our $44.90 Starter (2 cores, 6 GB, NVMe) is sized for exactly that — and the full reasoning about what each resource does and what breaks first (OOM kills, swap death, log-blocked event loops) lives in the VPS specs guide. Scale tiers as you add bots or platforms; current plans are on the pricing page. NVMe matters more than it looks: continuous log writes on slow storage can block Python’s thread mid-flush, which is a real cause of missed fills.

Operating System: Linux vs Windows

Both work. Linux gives you systemd — the cleanest 24/7 process management with auto-restart — and Ubuntu 22.04/24.04 are the standard picks. Windows Server handles Python bots fine via Task Scheduler and makes life easier if NinjaTrader or another Windows platform shares the box. Everything below shows the Linux path; the Windows equivalents are noted where they differ.

First Commands After Connecting

SSH in (RDP on Windows), then before anything else:

sudo apt update && sudo apt upgrade -y
sudo timedatectl set-timezone UTC

UTC is not cosmetic: Polymarket’s authenticated requests expire after a short window, and meaningful clock drift fails every signed call with errors that won’t mention the clock. Your firewall posture is equally simple — the bot makes only outbound connections, so block all inbound traffic except SSH/RDP from your own IP (full commands in the security section).

Setting Up Python and py-clob-client

# Verify Python 3.9+
python3 --version

# Virtual environment for the bot
python3 -m venv polybot-env
source polybot-env/bin/activate

# Core dependencies
pip install py-clob-client
pip install web3==6.14.0
pip install python-dotenv
pip install websockets

Pin web3 to 6.14.0. The SDK leans on web3 internals that break on newer versions without warning — setup looks fine, then the first order signing fails with errors that point everywhere except the version mismatch. Pin it and skip the afternoon of debugging.

Environment Variables, Not Hardcoded Keys

A committed private key is compromised the moment it touches a remote repository, even if deleted seconds later. All secrets live in a .env file:

# .env — never in version control
PRIVATE_KEY=your_polygon_wallet_private_key
POLYMARKET_API_KEY=your_api_key
POLYMARKET_API_SECRET=your_api_secret
POLYMARKET_PASSPHRASE=your_api_passphrase
WALLET_ADDRESS=your_polygon_wallet_address
CHAIN_ID=137
chmod 600 .env
import os
from dotenv import load_dotenv

load_dotenv()
PRIVATE_KEY = os.getenv("PRIVATE_KEY")
API_KEY = os.getenv("POLYMARKET_API_KEY")
API_SECRET = os.getenv("POLYMARKET_API_SECRET")
PASSPHRASE = os.getenv("POLYMARKET_PASSPHRASE")
WALLET = os.getenv("WALLET_ADDRESS")

Authenticating Your Wallet and Setting Token Allowances

Authentication is the most common point of failure for first-time builders. Two layers, both required: L1 uses your Polygon private key to prove ownership and mint API credentials; L2 uses HMAC-signed headers (key, secret, passphrase) for trading operations.

The parameter that bites people is signature_type:

  • 0 — standard EOA wallet (MetaMask, Rabby, hardware)
  • 1 — Polymarket email / Magic wallet
  • 2 — Gnosis Safe or browser proxy wallet

A mismatch doesn’t always throw clearly; test in dry-run mode (below) before risking an order.

from py_clob_client.client import ClobClient

HOST = "https://clob.polymarket.com"
CHAIN_ID = 137  # Polygon mainnet

client = ClobClient(
    HOST,
    key=PRIVATE_KEY,
    chain_id=CHAIN_ID,
    signature_type=0   # match how your wallet was created
)

api_creds = client.create_or_derive_api_creds()
client.set_api_creds(api_creds)

print(client.get_ok())           # "OK"
print(client.get_server_time())  # current UTC time

Token Allowances — Once Per Wallet

Before the first order, Polymarket’s contracts need authorization to move your USDC and conditional tokens. Run the official allowance script from Polymarket’s py-clob-client repository once per wallet; it approves the USDC, CTF, and exchange contracts. You’ll need a little POL for the approval gas — 1–2 POL covers it, and 5–10 POL is a sensible running buffer for an active bot.

Writing Your Bot’s Strategy Logic

Real-Time Data: WebSocket, Never REST Polling

REST polling makes your bot permanently as stale as its polling interval. The WebSocket pushes book deltas as they happen:

import asyncio
import websockets
import json

async def stream_orderbook(token_id: str):
    uri = "wss://ws-subscriptions-clob.polymarket.com/ws/"
    async with websockets.connect(uri) as ws:
        # Subscription message — confirm the current field names
        # against docs.polymarket.com before deploying.
        await ws.send(json.dumps({
            "type": "subscribe",
            "channel": "market",
            "assets_id": token_id
        }))
        async for message in ws:
            data = json.loads(message)
            process_update(data)

def process_update(data):
    # Rebuild top-of-book, run signal logic
    pass

Market discovery goes through Gamma:

import requests

def get_active_markets(category: str = "crypto", limit: int = 50):
    url = "https://gamma-api.polymarket.com/markets"
    params = {"active": True, "category": category, "limit": limit}
    return requests.get(url, params=params).json()

Position Sizing: Fractional Kelly with a Hard Cap

Sizing is risk management, not strategy. Full Kelly is aggressive; production bots typically run 0.15–0.25 of the Kelly output to absorb model error:

def kelly_criterion(true_prob: float, market_price: float,
                    bankroll: float, fraction: float = 0.25) -> float:
    """Fractional Kelly position size in USDC."""
    if market_price <= 0 or market_price >= 1:
        return 0.0

    b = (1.0 / market_price) - 1.0   # decimal odds
    p = true_prob
    q = 1.0 - p

    full_kelly = (b * p - q) / b
    if full_kelly <= 0:
        return 0.0   # no edge — no trade

    position = fraction * full_kelly * bankroll
    return min(position, bankroll * 0.10)   # hard 10% cap

size = kelly_criterion(0.72, 0.58, 1000.0)   # → $35.00

(If you copied this function from an earlier version of this page and it wouldn’t run: the comparison operators had been HTML-escaped in publishing. The block above is correct.) The zero-output case is the entire point — a bot that passes on 90% of markets and trades only positive-Kelly signals beats one that forces marginal trades, and the profitability data says exactly that about who wins here.

Placing Orders: GTC for Making, FOK for Taking

GTC limit orders rest as makers — zero fees, rebate-eligible. FOK market orders fill now or cancel — taker fees apply. One classic mistake: a $500 FOK in a market with $200 of depth at your price cancels entirely; use FAK for partial fills in thin books, or slice into GTC orders.

from py_clob_client.clob_types import OrderArgs, MarketOrderArgs, OrderType

def place_limit_order(client, token_id, price, size, side):
    """GTC maker order — zero fees."""
    order_args = OrderArgs(token_id=token_id, price=price,
                           size=size, side=side)
    return client.post_order(client.create_order(order_args),
                             OrderType.GTC)

def place_market_order(client, token_id, amount, side):
    """FOK taker order — fees apply."""
    order_args = MarketOrderArgs(token_id=token_id, amount=amount,
                                 side=side, order_type=OrderType.FOK)
    return client.post_order(client.create_market_order(order_args),
                             OrderType.FOK)

Handling Rate Limits, Errors, and Reconnections

A bot that crashes on its first API error breaks at 3 AM during the event you built it for. As of this writing the commonly documented limits are on the order of 60 orders/minute per key and ~10 REST requests/second — verify current numbers in the official docs — and the architecture answer is the same regardless: real-time data on WebSockets, REST reserved for account data on a slow cadence.

import time
from functools import wraps

def rate_limited(max_per_second: float):
    min_interval = 1.0 / max_per_second
    last_called = [0.0]
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            wait = min_interval - (time.time() - last_called[0])
            if wait > 0:
                time.sleep(wait)
            result = func(*args, **kwargs)
            last_called[0] = time.time()
            return result
        return wrapper
    return decorator

Retry 5xx errors with exponential backoff (1s, doubling, cap 30s, give up after 5); never auto-retry 400/401 — those mean fix the request or the auth. And reconnect WebSockets unconditionally:

async def subscribe_with_reconnect(token_ids: list, handler):
    uri = "wss://ws-subscriptions-clob.polymarket.com/ws/"
    while True:
        try:
            async with websockets.connect(uri) as ws:
                for token_id in token_ids:
                    await ws.send(json.dumps({
                        "type": "subscribe",
                        "channel": "market",
                        "assets_id": token_id
                    }))
                async for message in ws:
                    await handler(json.loads(message))
        except (websockets.exceptions.ConnectionClosed,
                OSError, asyncio.TimeoutError) as e:
            print(f"WS disconnected: {e}. Reconnecting in 3s.")
            await asyncio.sleep(3)

For a market-making bot, a missed update is a stale quote on the book — which informed flow will happily fill. On reconnect, resync state before quoting again; never trade on a book you mirrored before the gap.

Three Bot Strategies and Their Infrastructure Requirements

Strategy 1 — Intra-market arbitrage (YES + NO under $1.00). When both sides of one market sum below $1, filling both locks the difference. The academic record shows this lane is real (~$10.6M of the study’s $40M) and industrialized — the authors only counted gaps above 5¢ because execution reality eats thinner ones. Windows close in seconds in liquid markets; this is the most latency-sensitive lane, both legs FOK, tails deciding the race. Full mechanics, fee math, and the one-leg risk rule: the arbitrage guide.

Strategy 2 — Cross-signal trading (the Binance lag). Polymarket’s short-horizon crypto markets reprice after the spot venue moves, and the repricing lag is the edge window — measure it, don’t trust folklore numbers about it. Two measured cautions from our own benchmark: this strategy pays taker fees in the most expensive category (1.80% peak — your per-trade edge must clear it), and routing to your signal venue is per-box: our Dublin machine’s Binance probe hit a far CDN edge at ~313 ms while three competitor boxes saw ~208 ms — same city, different routes. Before deploying this strategy, probe both your Polymarket path and your signal-source path from the exact box that will trade; the test script covers it, and yes, it caught our own routing weakness — published, on our roadmap, re-verified next benchmark.

Strategy 3 — Market making. Quote both sides, earn the spread plus rebates, pay zero fees — in exchange for the strictest uptime requirement on this page. Offline means stale quotes; stale quotes mean adverse fills. Continuous WebSocket, fast cancels, instant reconnect-and-resync, and a process manager that restarts you in seconds (next section) are the job description. The liquidity-rewards program is the same lane with the platform paying you to be there — and the adverse-selection economics are covered honestly in How to Win on Polymarket.

Deploying for 24/7 Operation

Copy code up with rsync, excluding secrets (create .env fresh on the box):

rsync -avz --exclude '.env' ./polybot/ user@your-vps-ip:/home/user/polybot/

Then systemd:

# /etc/systemd/system/polybot.service
[Unit]
Description=Polymarket Trading Bot
After=network.target

[Service]
Type=simple
User=your-username
WorkingDirectory=/home/your-username/polybot
EnvironmentFile=/home/your-username/polybot/.env
ExecStart=/home/your-username/polybot-env/bin/python main.py
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable polybot
sudo systemctl start polybot
sudo journalctl -u polybot -f   # live logs

Restart=always brings the bot back five seconds after any crash and survives reboots. Log every significant event with UTC timestamps to a rotating file (50MB × 10 keeps weeks of history), and on Windows, Task Scheduler with “restart on failure” plays the systemd role.

The Circuit Breaker Is Not Optional

Strategies break in production in ways backtests never predicted. A hard daily-loss stop that cancels all open orders and halts is the difference between a bad day and a bad month:

class CircuitBreaker:
    def __init__(self, max_daily_loss_usdc: float):
        self.max_loss = max_daily_loss_usdc
        self.daily_loss = 0.0
        self.triggered = False

    def record_loss(self, amount: float):
        self.daily_loss += amount
        if self.daily_loss >= self.max_loss:
            self.triggered = True

    def is_ok(self) -> bool:
        return not self.triggered

breaker = CircuitBreaker(max_daily_loss_usdc=100.0)

# in the main loop:
if not breaker.is_ok():
    client.cancel_all()
    break

Security Hardening

The bot holds a private key controlling real USDC; the attack surface is narrow but the failure is irreversible.

# Inbound: SSH from your IP only. The bot needs no inbound ports.
sudo ufw allow from YOUR.IP.ADDRESS to any port 22
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw enable
# /etc/ssh/sshd_config → PasswordAuthentication no, PermitRootLogin no

Fund the trading wallet with this week’s risk budget only; the rest stays in cold storage — a compromised key drains a float, not your holdings. Cap position size in code (the 10% hard cap above) regardless of what Kelly outputs until the strategy has 200+ live trades behind it. And keep secrets in the chmod 600 .env loaded via EnvironmentFile= — never in the unit file’s Environment= line, which is world-readable through systemctl show.

Testing Before Going Live

Add a DRY_RUN flag and gate every submission on it:

DRY_RUN = os.getenv("DRY_RUN", "true").lower() == "true"

def submit_order(client, order_args, order_type):
    if DRY_RUN:
        logger.info(f"[DRY RUN] {order_args.side} {order_args.size}"
                    f" @ {order_args.price} on {order_args.token_id}")
        return {"status": "simulated"}
    return client.post_order(client.create_order(order_args), order_type)

Run dry for at least 48 hours across different market conditions; check signal frequency, sizing outputs, and error patterns. Then go live with $50–100 for a week — enough for real fills, slippage, and gas while bugs are still cheap — and only scale when live results match the dry-run logs. Diverging? Investigate before adding capital, not after.

Frequently Asked Questions

What VPS specs do I need for a Polymarket bot?

A single bot watching 10–20 markets runs comfortably on 2 cores and a few GB of RAM — the hot loop is single-threaded, so per-core speed matters more than core count, and extra cores buy more bots rather than a faster bot. NVMe storage keeps log writes off the critical path. The full sizing logic and failure modes are in the specs guide.

Where should my Polymarket bot be located, and what latency is realistic?

Europe, for Polymarket-focused bots — and the realistic numbers are tens of milliseconds, not the single-digit claims you’ll see around the internet. In our published Dublin benchmark the live feed ran ~13–15 ms median and warm order round-trips ~21–23 ms, with bot-vs-bot races decided in the 37–55 ms p99 tails. The API is CDN-fronted, so ignore confident region trivia and measure from your actual box with the free probe.

What are the CLOB API rate limits?

Commonly documented as ~60 orders/minute per key and ~10 REST requests/second, with throttling before hard rejection — but verify current limits in the official docs before architecting around them. Either way, the design answer is fixed: WebSockets for all real-time data, REST for slow account polling.

What signature type should my bot use?

0 for a standard EOA wallet (MetaMask), 1 for Polymarket email/Magic accounts, 2 for Gnosis Safe or proxy wallets. Match it to how the wallet was created; mismatches fail in unhelpful ways, so verify with a dry-run before trading.

How do I keep the bot running 24/7?

systemd with Restart=always and RestartSec=5 on Linux (Task Scheduler’s restart-on-failure on Windows), rotating logs, a WebSocket reconnect loop that resyncs state before quoting, and a circuit breaker that cancels everything past a daily loss limit. The box itself should be always-on with an uptime target worthy of resting orders — a home PC is the weak link here, not the code.

Do I need USDC and POL?

Both. USDC is trading capital; POL pays Polygon gas for allowance approvals and on-chain operations. 5–10 POL is a comfortable running buffer.

What are token allowances?

One-time on-chain approvals letting Polymarket’s contracts move your USDC and conditional tokens. Without them every order fails at the contract level no matter how correct your API auth is. Run the official script from the py-clob-client repo once per wallet.

Can I run this on Windows Server?

Yes — Python 3.9+, pip, and the SDK install cleanly; Task Scheduler replaces systemd. Windows is the convenient choice when the bot shares a box with NinjaTrader or another Windows-native platform.


A Polymarket bot is only as good as its worst minute: the dropped WebSocket during a Fed print, the reboot with resting orders on the book, the log flush that blocked the loop. Build for those minutes — and verify every infrastructure claim, ours included, with measurements. Our Dublin boxes’ current numbers are public in the benchmark, the probe is free, and the 3-day demo exists so the first thing your bot does on our hardware is check whether we told you the truth.

Fee figures and API details reference official Polymarket documentation as of June 2026 and change — verify before deploying. We operate TradoxVPS and provide infrastructure, not financial advice; automated trading involves substantial risk, including rapid total loss.

Share this article:
Facebook
X
LinkedIn

TradoxVPS Engineering Team

Infrastructure specialists focused on low-latency trading VPS and CME-proximal hosting.
Published:
Discover how TradoxVPS can power your trading with speed, stability, and 24/7 uptime to stay ahead in the markets.