Build Your First Poker Bot in Python
Lesson 1 of 13: Connect
Complete a supervised first hand. Python 3.11+, a terminal, and a bot API key for live play. Offline checks need no account.
Start with a small Python client that connects to Open Poker and responds to the server's legal actions. Completing your first hand is the goal of this lesson; strategy comes later.
Course start: Download the lesson checkpoint above to follow the evolving course bot. The short client below explains the connection loop. Keep the complete building guide open as your reference.
What do you actually need to get started?
Use Python 3.11+ for the course. The short client below needs one library:
python -m pip install "websockets>=14,<16"
That's it. No SDK, no framework, no game engine to install. We deliberately kept the protocol simple: your bot connects over WebSocket, receives game state as JSON messages, and sends actions back as JSON. If you can parse a dictionary, you can build a bot.
You'll also need an Open Poker bot API key: sign in, select your bot, and choose Self Host. Store the key in OPEN_POKER_API_KEY in your local process environment. See the registration guide. Keep credentials out of source code and shared screenshots.
This lesson uses WebSocket messages directly so you can see the protocol. Log message types and error codes while learning; avoid publishing raw payloads containing credentials or private cards.
What does the full bot look like?
import asyncio
import json
import os
import uuid
import websockets
API_KEY = os.environ["OPEN_POKER_API_KEY"]
WS_URL = "wss://openpoker.ai/ws"
async def play():
headers = {"Authorization": f"Bearer {API_KEY}"}
async with websockets.connect(WS_URL, additional_headers=headers) as ws:
msg = json.loads(await ws.recv())
print(f"Connected as {msg['name']}")
await ws.send(json.dumps({"type": "set_auto_rebuy", "enabled": True}))
await ws.send(json.dumps({"type": "join_lobby", "buy_in": 2000}))
async for raw in ws:
msg = json.loads(raw)
t = msg.get("type")
if t == "your_turn":
actions = {a["action"]: a for a in msg["valid_actions"]}
if "check" in actions:
act = "check"
elif "call" in actions:
act = "call"
else:
act = "fold"
await ws.send(json.dumps({
"type": "action",
"hand_id": msg["hand_id"],
"action": act,
"client_action_id": str(uuid.uuid4()),
"turn_token": msg["turn_token"],
}))
elif t == "table_closed":
await ws.send(json.dumps({"type": "join_lobby", "buy_in": 2000}))
elif t == "season_ended":
await ws.send(json.dumps({"type": "join_lobby", "buy_in": 2000}))
elif t == "hand_result":
winners = msg.get("winners", [])
if winners:
print(f"Hand won by {winners[0]['name']} (+{winners[0].get('amount', 0)})")
asyncio.run(play())Save it as bot.py, set OPEN_POKER_API_KEY in your local process environment, and run python bot.py. A successful connection prints your bot name. Seating and hand completion depend on other bots being available. This short example stops on disconnect; continue to the reliability lessons before leaving a bot unattended.
What does this bot actually do?
It's a calling station, and it was our first bot too.
When it's your turn: check if you can (free money). If you can't check, call. If you can't call, fold. This loses chips slowly because you're calling every bet without considering hand strength. But it plays legal poker, stays at the table, and gives you a complete event loop to build on.
The four concepts worth understanding:
set_auto_rebuy tells the server to automatically rebuy 1,500 chips when you bust. Without this, your bot stops playing after losing its stack. With it, the server handles rebuys (subject to a cooldown) and your bot keeps going indefinitely.
join_lobby puts you in the matchmaking queue. The buy_in field sets how many chips to bring to the table. Valid range is 1,000 to 5,000; we default to 2,000, which is 100 big blinds at the 10/20 blind structure. When enough players are queued, the matchmaker creates a 6-max table.
turn_token is an anti-replay token. Every your_turn message includes a fresh token. You must echo it back in your action. If you send a stale token from a previous turn, the action is rejected. Always use the token from the most recent your_turn. Never cache it.
hand_id identifies the current hand. Echo it with the token from the same your_turn message.
client_action_id identifies an action attempt. The server echoes it in action_ack; this example uses a UUID. Recovery code must preserve an attempt's identity when resolving an uncertain acknowledgement.
Which WebSocket messages does your bot handle?
Your bot receives a continuous stream of JSON messages. Most are informational; you only need to respond to your_turn. But understanding the others is how you build a smarter bot. Here's the full set you'll encounter:
| Message | What it means | Do you respond? |
|---|---|---|
connected | Auth succeeded, you're online | No |
lobby_joined | You're in the matchmaking queue | No |
table_joined | You're seated at a table | No |
hand_start | New hand beginning, here's your seat and the dealer | No |
hole_cards | Your two private cards (e.g., ["Ah", "Kd"]) | No |
your_turn | Your valid actions, the pot, the board | Yes: send an action |
player_action | Someone (maybe you) acted | No |
community_cards | Flop, turn, or river dealt | No |
hand_result | Hand is over, here's who won | No |
busted | You're out of chips | No (auto-rebuy handles it) |
table_closed | Table shut down | Rejoin lobby |
season_ended | Season transition | Rejoin lobby |
The full message reference is at docs.openpoker.ai/api-reference/message-types. Every field of every message is documented with JSON examples. Worth bookmarking; you'll refer to it constantly.
Making it smarter: three quick wins
The calling station is a connectivity baseline. These are three strategy experiments to try once the connection works; measure their effects rather than assuming a particular win rate.
1. Add a simple preflop filter
Most starting hands in poker are losers. A simple preflop filter that folds the bottom 60% before the flop puts you ahead of every calling station on the platform. Starting hand selection is the single biggest improvement you can make.
def should_play(cards):
"""Illustrative starting range, not a calibrated percentile."""
ranks = "23456789TJQKA"
r1 = ranks.index(cards[0][0])
r2 = ranks.index(cards[1][0])
high, low = max(r1, r2), min(r1, r2)
pair = r1 == r2
suited = cards[0][1] == cards[1][1]
if pair: return True # All pairs
if low >= 8: return True # Both cards ten or higher
if suited and high - low == 1 and low >= 7: return True # 98s+
if high == 12 and low >= 5: return True # A7+
return FalseStore your hole cards when you receive hole_cards, then check should_play() in your your_turn handler. With an excluded hand, check if that action is free and legal; otherwise fold only when fold is in valid_actions.
2. Raise your strong hands
The calling station never raises. This means opponents get to see cheap flops against you every single hand. Fix: raise with your strongest 15% of hands pre-flop.
if "raise" in actions and should_raise(my_cards):
await ws.send(json.dumps({
"type": "action",
"hand_id": msg["hand_id"],
"action": "raise",
"amount": actions["raise"]["min"], # minimum raise
"client_action_id": next_id(),
"turn_token": msg["turn_token"],
}))The raise entry in valid_actions tells you the exact min and max amounts. The amount field is a raise-to amount (total bet size), not an increment. If the big blind is 20 and you want to raise to 60, send "amount": 60.
3. Use pot odds post-flop
After the flop, you have real information. Pot odds tell you whether calling is mathematically correct: if the price you're paying is lower than your probability of winning, call. Otherwise fold. For the full math, the pot odds glossary entry has worked examples and gotchas that trip up beginner bots.
def pot_odds_say_call(pot, call_amount, estimated_win_pct=0.3):
if call_amount == 0:
return True
odds = call_amount / (pot + call_amount)
return estimated_win_pct > oddsEven a rough estimate of your win probability (30% as a default, higher with top pair, lower with nothing) combined with pot odds beats the pure calling station by a wide margin. The your_turn message includes the current pot size, so you have everything you need.
What we learned running this bot
I ran the calling station for over 1,200 hands to get a real baseline. It lost 2.4 big blinds per 100 hands, not catastrophic but a steady drain. The biggest leak wasn't calling too many bets. It was calling river bets with nothing. The calling station has no concept of "I've missed everything and this bet is large relative to pot"; it just calls, every time, and bleeds.
The second thing that surprised me: auto-rebuy cooldowns matter more than you'd think. After busting, there's a 5-minute cooldown on the free tier (2 minutes on Pro) before your next rebuy. A bot that busts frequently spends a lot of time sitting out. Getting stack management right (not busting in the first place) has compounding returns beyond just chip conservation.
Adding should_play() from the section above dropped the loss rate to around 0.8 bb/100 in our testing, a 3x improvement from one function. The bot still loses, but it's now losing like a mediocre player rather than a broken one. That's the starting point for real strategy work.
We're not claiming these are rigorous sample sizes. Variance at 6-max is high, and 1,200 hands is a small window. But directionally, the pattern is consistent: pre-flop selection is the first lever, post-flop aggression is the second.
How can you reproduce the 1,200-hand baseline?
Treat the 1,200-hand calling-station run as an engineering baseline, not a profitability benchmark. The recorded result was -2.4 bb/100. The follow-up preflop-filter result was about -0.8 bb/100, but that follow-up did not preserve enough run metadata for a clean head-to-head claim. We are publishing that limitation because a number without its method is marketing, not evidence.
For a reproducible comparison, pin the bot revision and record these fields for every run:
| Field | Why it belongs in the benchmark |
|---|---|
| Git commit and configuration hash | Proves which policy produced the actions |
| UTC start and end time | Exposes field and uptime differences |
| Completed hand IDs | Makes the sample auditable and prevents double counting |
| Big blinds won or lost per 100 hands | Normalizes results across blind levels |
action_rejected count | Detects protocol errors disguised as strategy losses |
| Turn timeouts and reconnects | Separates decision quality from runtime failures |
| Opponent count and seat distribution | Shows whether one table dominated the result |
Run the baseline and candidate for the same minimum hand count, keep both raw hand-ID lists, and report confidence intervals before calling an improvement real. At 1,200 hands, the result is useful for finding obvious leaks such as unconditional river calls. It is not enough to rank poker strategies.

First-party product screenshot captured March 10, 2026. This is the result-audit interface, not the 1,200-hand calling-station run. Hand IDs and per-hand outcomes are the evidence trail a benchmark should retain.
What to expect on the leaderboard
The base calling station is a connectivity test, not a competitive strategy. Adding the three improvements removes obvious leaks, but no fixed leaderboard placement follows from them. The field changes by season and short samples are noisy. To keep improving, add hand evaluation, opponent modeling, stack management, and position awareness, then measure each change against a pinned baseline.
Your bot needs at least 10 hands to qualify for leaderboard display. The time required depends on table availability and play speed.
The full platform documentation is at docs.openpoker.ai. The actions and strategy guide covers raise semantics, turn tokens, and timeout behavior in detail. The websockets library documentation is worth reading if you want async connection handling beyond the basics shown here.
FAQ
My bot connects but never gets seated. The matchmaker needs 2+ players in the queue. If nobody else is playing, your bot waits. Check the leaderboard to see if others are active; do not register extra independent production agents just to fill a second seat.
I get action_rejected errors.
Check the rejection code and confirm that hand_id and turn_token come from the same current your_turn message. Don't reuse an old turn's authority.
My bot disconnected and lost its seat. You have 120 seconds to reconnect. If you reconnect in time, your seat is preserved. After 120 seconds, your stack is returned to your balance and you'll need to rejoin the lobby.
Can I run this bot 24/7? The short example is for a supervised first session. Unattended play requires reconnect, state recovery, deadline handling, and acknowledgement tracking. Continue through the reliability stage before attempting it.
How much should I buy in for? The valid range is 1,000 to 5,000 chips. We use 2,000 in the examples (100 big blinds at 10/20 blinds), which is a standard deep-stack starting amount. Buying in shorter (1,000) reduces variance but also limits how much you can win in a single hand. Buying in deeper (5,000) is fine once your bot has a basic fold/raise strategy; don't do it with a pure calling station.
After your bot completes a hand, continue to the next lesson below. If the lobby is waiting, keep the session open for opponents and use the offline checkpoint to verify your client in the meantime.
Try the lesson 1 checkpoint
- What changes
- Connect a client and send a legal action.
- Expected output
- completed_hands: 1
- Check your work
- Wait for a real completed hand; an offline fixture does not count as live play.
Extract the lesson download, open that folder in a terminal, and run:
python -m pip install -r requirements.txt
python bot.py --lesson 1 --self-test
python bot.py --lesson 1 --hands 3 --report run.jsonThe self-test uses offline fixtures and prints checkpoint: passed. Live play requires OPEN_POKER_API_KEY in your environment and available opponents. Read the included README for setup and recovery limits.
All lessons in this course
- 1. Build a Poker Bot in Python
- 2. Basic Poker Bot Architecture
- 3. Debug Poker Bot WebSocket Errors
- 4. Why Poker Bots Time Out
- 5. Poker Math for Bots
- 6. Poker Bot Position Ranges
- 7. Poker Bot Betting Strategy
- 8. PokerKit Tutorial
- 9. Monte Carlo Equity Calculator
- 10. Opponent Modeling
- 11. Advanced Poker Bot Architecture
- 12. Leaderboard Scoring
- 13. Pro Poker Bot Architecture