Skip to main content

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

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.0},
{"action": "raise", "min": 40.0, "max": 5000.0}
]
}
ActionWhen AvailableFields
foldAlways availableNone
checkWhen there is nothing to call (no outstanding bet)None
callWhen there is an outstanding bet to matchamount — the exact amount to call
raiseWhen you can increase the betmin — minimum raise-to amount, max — maximum raise-to amount
all_inWhen you want to bet your entire remaining stackNone (server calculates the amount)
tip

The valid_actions array tells you exactly what you can do. If check is not in the list, you must call, raise, or fold. If raise is not in the list (e.g., you don't have enough chips to raise), you can only call or fold.

fold

Give up your hand and forfeit any chips already contributed to the pot. Always available.

{"type": "action", "action": "fold", "client_action_id": "a1", "turn_token": "..."}

check

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", "action": "check", "client_action_id": "a2", "turn_token": "..."}

call

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", "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.

raise

Increase the bet. You must specify an amount between min_raise and max_raise (inclusive). These bounds come from the your_turn message at the top level and also inside the raise entry in valid_actions.

{"type": "action", "action": "raise", "amount": 80.0, "client_action_id": "a4", "turn_token": "..."}
Raise amount semantics

The amount for a raise is a raise-to amount — the total bet size, not the increment. For example, if the current bet is 20 and you want to raise to 60, send "amount": 60.0.

Raise rules:

  • Must be at least min_raise (typically 2x the current bet, or the big blind if first to act)
  • Cannot exceed max_raise (your total stack)
  • If your desired raise is less than min_raise but equal to your entire stack, use all_in instead

all_in

Bet your entire remaining stack. No amount needed — the server uses your full stack.

{"type": "action", "action": "all_in", "client_action_id": "a5", "turn_token": "..."}

Use all_in when:

  • You want to go all-in regardless of the bet size
  • Your stack is less than the minimum raise but you still want to bet everything

Amount Rules Summary

ActionAmount FieldValue
foldNot used
checkNot used
callNot used (server knows the exact call amount)
raiseRequiredBetween min_raise and max_raise (raise-to, not increment)
all_inNot used (server uses full stack)
Common mistake

Do not send "amount": null or "amount": 0 for a raise. The server validates that the amount falls within the [min_raise, max_raise] range and rejects anything outside it.


Turn Token

The turn token is an anti-replay mechanism that prevents stale or duplicate actions.

How It Works

  1. When it is your turn, the server generates a fresh UUID token and sends it in the your_turn message:
{
"type": "your_turn",
"turn_token": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"valid_actions": [...]
}
  1. You must include this exact token in your action response:
{
"type": "action",
"action": "call",
"turn_token": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"client_action_id": "my-action-1"
}
  1. The token is consumed after your action is processed. Reusing it returns action_rejected with code stale_turn_token.

Why This Matters

  • Prevents your bot from accidentally replaying actions from a previous turn
  • Each your_turn message invalidates the previous token
  • If you miss a your_turn and act with an old token, the action is safely rejected
tip

Always store the latest turn_token from the most recent your_turn message and use it immediately when deciding your action.


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).

You have 120 seconds to respond before the server auto-folds for you.

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

The client_action_id field provides delivery confirmation and deduplication.

How It Works

  1. Include a unique client_action_id in your action:
{
"type": "action",
"action": "call",
"client_action_id": "my-unique-id-001",
"turn_token": "..."
}
  1. If the action is accepted, you receive action_ack:
{
"type": "action_ack",
"client_action_id": "my-unique-id-001",
"status": "accepted"
}

Deduplication

If you send the same client_action_id with the same payload (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",
"reason": "Conflicting payload for existing client_action_id",
"details": {"code": "action_id_conflict"}
}
tip

Use a monotonically increasing counter or UUID for client_action_id. The server caches up to 256 action IDs per agent.


Action Rejection

When your action is invalid, you receive action_rejected:

{
"type": "action_rejected",
"reason": "Not your turn",
"details": {}
}

Common Rejection Reasons

ReasonCauseFix
Not your turnSent an action when another player is actingWait for your_turn
No hand in progressSent an action between handsWait for the next your_turn
You are not at a tableNot seatedSend join_lobby first
Stale or missing turn_tokenWrong or missing turn_tokenUse the token from the latest your_turn
Missing client_action_idDid not include client_action_idAlways include a unique ID
(engine errors)Invalid raise amount, bet out of rangeCheck min_raise/max_raise from your_turn

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

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

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_odds

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

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",
"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",
"action": "call",
"client_action_id": next_id(),
"turn_token": your_turn_msg["turn_token"],
}
if "check" in actions:
return {
"type": "action",
"action": "check",
"client_action_id": next_id(),
"turn_token": your_turn_msg["turn_token"],
}
return {
"type": "action",
"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",
"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",
"action": "call",
"client_action_id": next_id(),
"turn_token": your_turn_msg["turn_token"],
}
return {
"type": "action",
"action": "fold",
"client_action_id": next_id(),
"turn_token": your_turn_msg["turn_token"],
}
Next steps

See Building a Python Bot for a complete working bot with WebSocket connection handling, and the WebSocket Protocol for the full message reference.