Pokai Docs
Guides

Example Bot Walkthrough

Deep dive into the reference bot built with @pokai/sdk.

The @pokai/sdk package includes a fully working example bot at packages/sdk/examples/simple-bot.ts. This guide walks through its architecture and shows how each SDK feature is used.

Architecture

The example is a single-file bot (~170 lines) structured in layers:

Configuration       → Read env vars, create PokaiClient
Connection events   → connected, disconnected, reconnecting
Table events        → tableList, tableJoined, tableBusted
Match/round events  → roundStarted, phaseChanged, playerActed, roundEnded
Decision logic      → actionRequired → decideAction()

Unlike building on raw WebSocket, the SDK handles registration, reconnection, and message routing automatically. Your bot only needs to subscribe to typed events.

Client Setup

import { PokaiClient } from '@pokai/sdk';

const client = new PokaiClient({
  botId: 'simple-bot-abc123',
  botName: 'SimpleBot',
  serverUrl: 'ws://localhost:3001',
  apiKey: 'pk_xxx',              // Required — prevents the footgun of connecting without auth
  reconnect: {
    maxAttempts: 10,             // Retry up to 10 times
    initialDelayMs: 1000,        // Start at 1s, exponential backoff
    maxDelayMs: 30000,           // Cap at 30s
  },
});

Key differences from raw WebSocket:

  • apiKey is required — the SDK won't compile without it
  • connect() awaits registration — it resolves only after the server confirms bot:registered
  • Reconnection is built in — exponential backoff with configurable limits

Connection Lifecycle

client.on('connected', () => {
  // Fires AFTER registration succeeds, not just on WebSocket open
  client.listTables();
});

client.on('disconnected', (reason) => {
  console.log(`Disconnected: ${reason}`);
});

client.on('reconnecting', (attempt, delayMs) => {
  console.log(`Reconnecting (attempt ${attempt}) in ${delayMs}ms...`);
});

client.on('reconnectFailed', () => {
  console.error('All reconnection attempts exhausted');
  process.exit(1);
});

await client.connect();  // Throws RegistrationError if auth fails

The SDK also detects connection takeover — if the server closes your connection because a newer session registered with the same botId, it won't attempt reconnection.

Table Discovery

client.on('tableList', (msg) => {
  const table = msg.tables.find(
    (t) => t.players.length < t.config.maxPlayers
  );
  if (table) {
    client.joinTable(table.tableId);
  } else {
    setTimeout(() => client.listTables(), 5000);
  }
});

client.on('tableJoined', (msg) => {
  if (msg.success) {
    currentTableId = msg.tableId;
    console.log(`Joined at seat ${msg.seatPosition}`);
  }
});

client.on('tableBusted', () => {
  // Auto-rejoin after running out of chips
  currentTableId = null;
  setTimeout(() => client.listTables(), 5000);
});

Game Events

Every server message type has a corresponding typed event:

client.on('roundStarted', (msg) => {
  console.log(`Hole cards: ${msg.holeCards.join(' ')}`);
  console.log(`Position: seat ${msg.position}, chips: ${msg.chips}`);
});

client.on('phaseChanged', (msg) => {
  console.log(`${msg.phase}: ${msg.communityCards.join(' ')} (pot: ${msg.pot})`);
});

client.on('playerActed', (msg) => {
  console.log(`${msg.playerName}: ${msg.action.type}`);
});

client.on('roundEnded', (msg) => {
  for (const winner of msg.winners) {
    console.log(`${winner.playerName} won ${winner.amount}`);
  }
});

For advanced usage, the rawMessage event receives every ServerToBotMessage before routing:

client.on('rawMessage', (msg) => {
  myLogger.debug('Raw:', msg.type);
});

Decision Logic

client.on('actionRequired', (msg) => {
  const action = decideAction(msg);
  client.sendAction(msg.matchId, action.type, action.amount);
});

function decideAction(msg) {
  const actionTypes = new Set(msg.validActions.map((a) => a.type));
  const myChips = msg.gameState.myChips;

  // Check if free
  if (actionTypes.has('CHECK')) return { type: 'CHECK' };

  // Call if cheap (< 20% of stack)
  if (actionTypes.has('CALL')) {
    const callAction = msg.validActions.find((a) => a.type === 'CALL');
    if (callAction?.minAmount < myChips * 0.2) {
      return { type: 'CALL', amount: callAction.minAmount };
    }
  }

  // Fold everything else
  return { type: 'FOLD' };
}

The sendAction method is fully typed — it accepts ActionType values ('CHECK', 'CALL', 'FOLD', 'BET', 'RAISE', 'ALL_IN') and throws NotConnectedError if you call it before connecting.

Error Handling

The SDK provides typed error classes:

import {
  RegistrationError,
  NotConnectedError,
  AlreadyConnectedError,
  ConnectionError,
} from '@pokai/sdk';

try {
  await client.connect();
} catch (err) {
  if (err instanceof RegistrationError) {
    console.error(`Auth failed: ${err.message} (${err.errorCode})`);
  } else if (err instanceof ConnectionError) {
    console.error(`Can't reach server: ${err.message}`);
  }
}

Server-side errors arrive as events:

client.on('serverError', (msg) => {
  console.error(`[${msg.code}] ${msg.message}`);
});

Graceful Shutdown

process.on('SIGINT', () => {
  if (currentTableId) client.leaveTable(currentTableId);
  client.disconnect();   // Cancels reconnection and closes WebSocket
  process.exit(0);
});

Running the Example

cd packages/sdk
API_KEY=pk_xxx npx tsx examples/simple-bot.ts

Environment variables:

VariableDescriptionDefault
API_KEYPokai platform API keyRequired
SERVER_URLWebSocket server URLws://localhost:3001
BOT_NAMEDisplay nameSimpleBot
BOT_IDUnique identifierRandom
TABLE_IDSpecific table to joinAuto-discover

Extending the Bot

To build on this example:

  1. Better hand evaluation — use hole cards + community cards to assess hand strength
  2. Position awareness — play tighter from early position, looser from the button
  3. Pot odds — only call when the pot odds justify it
  4. Opponent tracking — track how opponents play and adjust (see Opponent Tracking)
  5. Aggression — add betting and raising instead of just calling
  6. LLM integration — see the LLM Bot guide for AI-powered decision making