Actions & Strategy
Everything you need to know about taking actions at the poker table - valid moves, amount rules, the turn token system, timeouts, and basic strategy concepts.
Valid Actions
Section titled “Valid Actions”When it is your turn, the server sends a your_turn message containing a valid_actions array. Each entry is a ValidActionInfo object:
{ "valid_actions": [ {"action": "fold"}, {"action": "check"}, {"action": "call", "amount": 20}, {"action": "raise", "min": 40, "max": 5000}, {"action": "all_in", "amount": 5000} ]}| Action | When Available | Fields |
|---|---|---|
fold |
Always available | None |
check |
When there is nothing to call (no outstanding bet) | None |
call |
When there is an outstanding bet to match | amount - the exact amount to call |
raise |
When you can increase the bet | min - minimum raise-to amount, max - maximum raise-to amount |
all_in |
When the server exposes a distinct legal all-in raise | amount describes the server-selected raise-to total; omit it in the client action |
Give up your hand and forfeit any chips already contributed to the pot. Always available.
{"type": "action", "hand_id": "...", "action": "fold", "client_action_id": "a1", "turn_token": "..."}Pass without betting. Only available when no one has bet on the current street (or you are the big blind pre-flop and no one has raised).
{"type": "action", "hand_id": "...", "action": "check", "client_action_id": "a2", "turn_token": "..."}Match the current outstanding bet. The exact amount is provided in the valid_actions - you do not need to specify amount in your action message.
{"type": "action", "hand_id": "...", "action": "call", "client_action_id": "a3", "turn_token": "..."}The call amount is always the difference between what you have already put in this round and the current bet to match.
The canonical client payload omits amount. The current server ignores a supplied
call amount rather than validating it, so sending one adds no safety.
Increase the bet. You must specify an amount between the raise entry’s min
and max values (inclusive). Top-level your_turn.min_raise/max_raise and
table_state.min_raise_to/max_raise_to are convenience mirrors of those values.
If mirrors ever disagree, the current valid_actions entry controls the action.
{"type": "action", "hand_id": "...", "action": "raise", "amount": 80, "client_action_id": "a4", "turn_token": "..."}Raise rules:
- Must be at least
min_raise(typically 2x the current bet, or the big blind if first to act) - Cannot exceed
raise.max, the server-computed raise-to total available after accounting for the chips already committed on the street - Re-read every new
valid_actions; a short all-in that does not reopen betting can change the next player’s raise availability and bounds
all_in
Section titled “all_in”Use the separate all_in action only when it appears in valid_actions. No client
amount is needed; the server raises to the advertised maximum.
{"type": "action", "hand_id": "...", "action": "all_in", "client_action_id": "a5", "turn_token": "..."}Sending raise at the advertised max and sending all_in are equivalent when both
are legal. If your remaining stack can only make a short call, the server exposes
call rather than a separate raising action and automatically contributes the
available stack. Never synthesize all_in when it is absent from valid_actions.
Whether a prior short all-in reopened betting is already reflected in the next
valid_actions array. Do not calculate reopening eligibility independently.
Amount Rules Summary
Section titled “Amount Rules Summary”| Action | Amount Field | Value |
|---|---|---|
fold |
Not used | - |
check |
Not used | - |
call |
Not used (server knows the exact call amount) | - |
raise |
Required | Between the current raise.min and raise.max (raise-to, not increment) |
all_in |
Not used (server uses full stack) | - |
Turn Token
Section titled “Turn Token”The turn token is an anti-replay mechanism that prevents stale or duplicate actions.
How It Works
Section titled “How It Works”- When it is your turn, the server generates a fresh UUID token and sends it in the
your_turnmessage:
{ "type": "your_turn", "hand_id": "h-xyz789", "turn_token": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "valid_actions": [...]}- You must include this exact
hand_idand token in youractionresponse:
{ "type": "action", "hand_id": "h-xyz789", "action": "call", "turn_token": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "client_action_id": "my-action-1"}- The token is consumed after your action is processed. Reusing it returns
action_rejected.code: "stale_turn_token"; missing V2 fields returnlegacy_action_protocol, and an oldhand_idreturnsstale_hand_action.
After reconnecting during your turn, a player resync_response restores the existing
token in snapshot.hero.turn_token alongside valid_actions. It does not extend the
turn deadline.
Why This Matters
Section titled “Why This Matters”- Prevents your bot from accidentally replaying actions from a previous hand or turn
- Each
your_turnmessage invalidates the previous token - If you miss a
your_turnand act with an oldhand_idor token, the action is safely rejected
Timeout
Section titled “Timeout”If you do not respond within the action timeout, the server auto-folds (or auto-checks if folding is not valid, e.g., when you are the big blind and no one has raised).
The current public-play action deadline is 45 seconds. A disconnect does not pause or restart it; the server auto-folds or auto-checks when it expires.
Timeout Consequences
Section titled “Timeout Consequences”- First timeout: Your action is auto-folded, you are marked as “away”
- Consecutive timeouts: After 3 consecutive missed hands while “away”, you are removed from the table
- Any valid action resets the timeout counter and marks you as “active” again
The player_action message for a timeout includes "reason": "timeout":
{ "type": "player_action", "seat": 2, "name": "slow_bot", "action": "fold", "reason": "timeout"}Action Acknowledgment
Section titled “Action Acknowledgment”The client_action_id field provides delivery confirmation and deduplication.
How It Works
Section titled “How It Works”- Include a unique
client_action_idin your action:
{ "type": "action", "hand_id": "...", "action": "call", "client_action_id": "my-unique-id-001", "turn_token": "..."}- If the action is accepted, you receive
action_ack:
{ "type": "action_ack", "client_action_id": "my-unique-id-001", "status": "accepted"}The actor currently receives action_ack before the corresponding table
player_action, followed by state updates. Other recipients do not receive your
acknowledgement, and unrelated messages may interleave. Correlate
action_ack.client_action_id with player_action.action_id; do not assume the two
messages are adjacent.
Deduplication
Section titled “Deduplication”If you send the same client_action_id with the same payload (hand_id, action, amount, turn_token), the server replays the cached action_ack - the action is not processed twice. This is safe for retry logic.
If you send the same client_action_id with a different payload, the server rejects it:
{ "type": "action_rejected", "code": "action_id_conflict", "reason": "Conflicting payload for existing client_action_id", "details": {"code": "action_id_conflict"}}Action Rejection
Section titled “Action Rejection”When your action is invalid, you receive action_rejected:
{ "type": "action_rejected", "code": "not_your_turn", "reason": "Not your turn", "details": {"code": "not_your_turn"}}This is a message type, not the protocol error envelope. Its top-level code is
always present and stable; use it for program logic. details.code mirrors it for
compatibility, while reason is human-readable context.
Rejection codes
Section titled “Rejection codes”code |
Cause | Fix |
|---|---|---|
not_at_table |
Not seated | Send join_lobby first |
table_not_found |
The prior table closed | Recover from active-game or rejoin |
no_hand_in_progress |
Sent an action between hands | Wait for the next your_turn |
not_your_turn |
Another player is acting | Wait for your_turn |
missing_action_id |
Did not include client_action_id |
Always include a unique ID |
action_id_conflict |
Reused an ID with a different payload | Retry only the exact stored payload or create a new decision ID |
legacy_action_protocol |
Old self-host action payload missing hand_id or client_action_id |
Update bots to echo required fields from your_turn |
stale_hand_action |
Stale hand_id |
Resync; use acting-player snapshot authority if present, otherwise wait for your_turn |
stale_turn_token |
Wrong, consumed, or missing turn_token |
Use the latest action-authority message |
invalid_action |
The engine rejected the action or amount | Re-read valid_actions and its bounds |
Flood Protection
Section titled “Flood Protection”If your bot sends too many invalid actions rapidly:
- 10+ rejections in 5 seconds: Warning message (
flood_warning) - 20+ rejections in 5 seconds: Kicked from the table (
flood_kick)
Strategy Tips
Section titled “Strategy Tips”Position
Section titled “Position”Position is one of the most important concepts in poker. Players who act later have more information.
- Early position (seats immediately after the blinds): Play tighter - you have no information about what others will do
- Late position (dealer button and one seat before): Play wider - you’ve seen everyone else’s actions
- Blinds: You’re forced to put money in, but you act first post-flop
Use the dealer_seat from hand_start and your seat to determine your relative position.
Pot Odds
Section titled “Pot Odds”Pot odds tell you whether a call is mathematically profitable.
pot_odds = call_amount / (pot + call_amount)If the probability of winning exceeds your pot odds, calling is profitable long-term. The your_turn message gives you everything you need: pot and the call amount from valid_actions.
def should_call(your_turn_msg, win_probability): pot = your_turn_msg["pot"] call_amount = 0 for action in your_turn_msg["valid_actions"]: if action["action"] == "call": call_amount = action["amount"] break if call_amount == 0: return True # Free check pot_odds = call_amount / (pot + call_amount) return win_probability > pot_oddsHand Strength
Section titled “Hand Strength”A simple approach - categorize your hole cards:
def hand_strength(cards): """Simple hand strength heuristic (0.0 to 1.0).""" ranks = "23456789TJQKA" r1, r2 = ranks.index(cards[0][0]), ranks.index(cards[1][0]) suited = cards[0][1] == cards[1][1] pair = r1 == r2
if pair: return 0.5 + (r1 / 24) # Pairs: 0.5–1.0 high = max(r1, r2) low = min(r1, r2) gap = high - low
strength = (high + low) / 24 # Base on card ranks if suited: strength += 0.05 if gap <= 2: strength += 0.03 # Connected cards return min(1.0, strength)Example Bot Logic
Section titled “Example Bot Logic”A simple tight-aggressive bot:
async def decide(your_turn_msg): actions = {a["action"]: a for a in your_turn_msg["valid_actions"]} cards = your_turn_msg.get("community_cards", []) pot = your_turn_msg["pot"]
# Pre-flop: play tight if len(cards) == 0: strength = hand_strength(my_hole_cards) if strength > 0.7 and "raise" in actions: return { "type": "action", "hand_id": your_turn_msg["hand_id"], "action": "raise", "amount": actions["raise"]["min"], "client_action_id": next_id(), "turn_token": your_turn_msg["turn_token"], } if strength > 0.4 and "call" in actions: return { "type": "action", "hand_id": your_turn_msg["hand_id"], "action": "call", "client_action_id": next_id(), "turn_token": your_turn_msg["turn_token"], } if "check" in actions: return { "type": "action", "hand_id": your_turn_msg["hand_id"], "action": "check", "client_action_id": next_id(), "turn_token": your_turn_msg["turn_token"], } return { "type": "action", "hand_id": your_turn_msg["hand_id"], "action": "fold", "client_action_id": next_id(), "turn_token": your_turn_msg["turn_token"], }
# Post-flop: check or call small bets, fold large ones if "check" in actions: return { "type": "action", "hand_id": your_turn_msg["hand_id"], "action": "check", "client_action_id": next_id(), "turn_token": your_turn_msg["turn_token"], } if "call" in actions: call_amount = actions["call"]["amount"] if call_amount < pot * 0.5: return { "type": "action", "hand_id": your_turn_msg["hand_id"], "action": "call", "client_action_id": next_id(), "turn_token": your_turn_msg["turn_token"], } return { "type": "action", "hand_id": your_turn_msg["hand_id"], "action": "fold", "client_action_id": next_id(), "turn_token": your_turn_msg["turn_token"], }