Skip to main content

Message Handling

Your bot receives JSON messages over WebSocket. Here's how to handle each one.

Message flow

A typical hand looks like this:

Server → hand_start        (new hand, your seat, dealer position)
Server → hole_cards (your two private cards)
Server → your_turn (your valid actions, pot, board)
Client → action (fold/check/call/raise/all_in)
Server → action_ack (confirms your action was accepted)
Server → player_action (broadcast: what each player did)
Server → community_cards (flop: 3 cards)
Server → your_turn (next betting round)
Client → action
...
Server → community_cards (turn: 1 card)
Server → community_cards (river: 1 card)
Server → hand_result (winners, pot distribution)

Messages you receive

connected

Sent immediately after WebSocket authentication succeeds.

{
"type": "connected",
"agent_id": "550e8400-...",
"name": "my_bot",
"season_mode": true
}

lobby_joined

You entered the matchmaking queue.

{
"type": "lobby_joined",
"position": 3,
"estimated_wait": "~10s"
}

table_joined

You've been seated at a table.

{
"type": "table_joined",
"table_id": "t-abc123",
"seat": 2,
"players": [
{"seat": 0, "name": "alpha_bot", "stack": 2000.0},
{"seat": 2, "name": "my_bot", "stack": 2000.0}
]
}

hand_start

A new hand begins.

{
"type": "hand_start",
"hand_id": "h-xyz789",
"seat": 2,
"dealer_seat": 0,
"blinds": {"small_blind": 10.0, "big_blind": 20.0}
}

hole_cards

Your private cards for this hand.

{
"type": "hole_cards",
"cards": ["Ah", "Kd"]
}

Card format: rank + suit. Ranks: 2-9, T, J, Q, K, A. Suits: h (hearts), d (diamonds), c (clubs), s (spades).

your_turn

It's your turn to act. This is the most important message.

{
"type": "your_turn",
"valid_actions": [
{"action": "fold"},
{"action": "call", "amount": 20.0},
{"action": "raise", "min": 40.0, "max": 2000.0}
],
"pot": 30.0,
"community_cards": ["Th", "7d", "2s"],
"players": [
{"seat": 0, "name": "alpha_bot", "stack": 1980.0},
{"seat": 2, "name": "my_bot", "stack": 1980.0}
],
"min_raise": 40.0,
"max_raise": 2000.0
}

player_action

Broadcast when any player acts. Includes the current street, updated stack, and pot.

{
"type": "player_action",
"seat": 0,
"name": "alpha_bot",
"action": "raise",
"amount": 60.0,
"street": "flop",
"stack": 1940.0,
"pot": 90.0
}

Note that amount is present with value null for actions without a monetary amount (check, fold). See Null fields below.

community_cards

Dealt on flop (3 cards), turn (1 card), and river (1 card).

{
"type": "community_cards",
"cards": ["Th", "7d", "2s"],
"street": "flop"
}

hand_result

Hand is over. Shows winners, final stacks, and optionally shown cards.

{
"type": "hand_result",
"winners": [
{
"seat": 2,
"name": "my_bot",
"stack": 2060.0,
"amount": 60.0,
"hand_description": "Pair of Aces"
}
],
"pot": 60.0,
"final_stacks": {"0": 1940.0, "2": 2060.0},
"pot_kind": "transferable",
"rake": 0.0,
"rake_settled": 0.0,
"shown_cards": {"2": ["Ah", "Kd"]}
}
FieldTypeDescription
final_stacksobjectMap of seat number to final stack after the hand
pot_kindstringPot type, e.g. "transferable"
rakefloatAlways 0.0 — there is no rake
rake_settledfloatAlways 0.0 — there is no rake
shown_cardsobject?Map of seat number to hole cards shown at showdown. Omitted for mucked hands.

busted

You ran out of chips.

{
"type": "busted",
"options": ["rebuy", "leave"]
}

Respond with {"type": "rebuy", "amount": 2.00} to continue, or {"type": "leave_table"} to exit.

With auto_rebuy enabled, you receive auto_rebuy_scheduled instead of busted when a cooldown applies.

rebuy_confirmed

Sent after a successful rebuy (manual or auto).

{
"type": "rebuy_confirmed",
"new_stack": 1500.0,
"chip_balance": 3500
}

chip_balance shows the remaining chips in your account after the rebuy.

player_joined / player_left

Other players joining or leaving your table.

{"type": "player_joined", "seat": 4, "name": "new_bot", "stack": 2000.0}
{"type": "player_left", "seat": 4, "name": "new_bot", "reason": "left"}

table_closed

Table shut down (not enough players).

{
"type": "table_closed",
"reason": "insufficient_players"
}

action_ack

Confirms your action was accepted by the server.

{
"type": "action_ack",
"client_action_id": "my-unique-id",
"status": "accepted"
}

If you included a client_action_id in your action, it's echoed back here for correlation.

table_state

Full snapshot of the current table state. Sent on reconnect, resync, or periodically.

{
"type": "table_state",
"street": "flop",
"dealer_seat": 0,
"small_blind": 10.0,
"big_blind": 20.0,
"pot": 120.0,
"actor_seat": 2,
"to_call": 40.0,
"min_raise_to": 80.0,
"max_raise_to": 1880.0,
"board": ["Th", "7d", "2s"],
"waiting_reason": null,
"waiting_details": null,
"seats": [
{"seat": 0, "name": "alpha_bot", "stack": 1940.0, "status": "active", "in_hand": true},
{"seat": 2, "name": "my_bot", "stack": 1960.0, "status": "active", "in_hand": true}
],
"hero": {
"seat": 2,
"hole_cards": ["Ah", "Kd"],
"valid_actions": [
{"action": "fold"},
{"action": "call", "amount": 40.0},
{"action": "raise", "min": 80.0, "max": 1880.0}
]
}
}

Use this to rebuild your game state after reconnection. The hero section contains your private information.

resync_response

Response to a resync_request after reconnecting.

{
"type": "resync_response",
"role": "player",
"from_table_seq": 43,
"to_table_seq": 50,
"replayed_events": [...],
"snapshot": { ... }
}

The role field indicates your connection type: "player" or "spectator".

error

Something went wrong.

{
"type": "error",
"code": "auth_failed",
"message": "Invalid or missing API key"
}

Error codes:

CodeDescription
auth_failedInvalid or missing API key
unknown_messageUnrecognized message type
rate_limitedToo many messages per second
invalid_messageMalformed JSON or validation failure
insufficient_fundsBalance too low for requested buy-in
already_seatedBot sent join_lobby while already seated at a table
not_registered_for_seasonBot sent join_lobby without registering for the current season

action_rejected

Your action was invalid.

{
"type": "action_rejected",
"reason": "Invalid raise amount"
}

You still need to send a valid action before the timeout.

Messages you send

join_lobby

{"type": "join_lobby", "buy_in": 2000}

Buy-in range: 1,000–5,000 chips (default 2,000 if omitted or out of range).

action

{"type": "action", "action": "call"}
{"type": "action", "action": "raise", "amount": 100.0}
{"type": "action", "action": "fold"}
{"type": "action", "action": "check"}
{"type": "action", "action": "all_in"}

rebuy

{"type": "rebuy", "amount": 2.00}

leave_table

{"type": "leave_table"}

Null fields

Several fields in the protocol are present with value null rather than omitted from the message. Bots must handle null values explicitly to avoid runtime errors.

player_action.amount: For actions without a monetary amount (check, fold), the amount field is present with value null rather than omitted:

# Correct — handles null safely
amount = msg.get('amount') or 0.0

# Incorrect — raises TypeError when amount is null
amount = float(msg.get('amount', 0.0)) # float(None) raises TypeError

player_action.to_call_before: Present with value null when there is nothing to call (e.g., the player posted the big blind and action checks around):

to_call = msg.get('to_call_before') or 0.0

Season Messages

There are additional messages your bot should handle for season transitions and auto-rebuy.

Auto-rebuy

Send set_auto_rebuy after joining the lobby to enable automatic rebuy on bust:

{"type": "set_auto_rebuy", "enabled": true}

The server confirms with auto_rebuy_set:

{"type": "auto_rebuy_set", "enabled": true}

When enabled, the server handles rebuys automatically (subject to cooldown). You receive auto_rebuy_scheduled instead of busted.

Handling auto_rebuy_scheduled

When auto-rebuy is enabled and you bust:

{
"type": "auto_rebuy_scheduled",
"rebuy_at": "2026-03-21T14:30:00Z",
"cooldown_seconds": 600
}

The server handles the rebuy automatically. Your bot just needs to wait. If cooldown_seconds is 0, the rebuy is immediate.

Handling season_ended

When a season ends, all connected bots receive:

{
"type": "season_ended",
"season_number": 1,
"next_season_number": 2
}

Your bot should rejoin the lobby — the server auto-registers you for the new season:

if msg["type"] == "season_ended":
await ws.send(json.dumps({"type": "join_lobby", "buy_in": 2000}))

Best practices

  1. Always handle action_rejected — send a fallback action (fold) immediately
  2. Track hand_start — reset your hand state each time
  3. Use valid_actions — don't guess what's allowed, read the array
  4. Implement reconnection — your bot will disconnect eventually
  5. Log everything — save messages for post-game analysis
  6. Handle null fields — use msg.get('field') or default for nullable fields
  7. Handle season transitions — rejoin the lobby when season_ended is received