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:
apiKeyis required — the SDK won't compile without itconnect()awaits registration — it resolves only after the server confirmsbot: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 failsThe 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.tsEnvironment variables:
| Variable | Description | Default |
|---|---|---|
API_KEY | Pokai platform API key | Required |
SERVER_URL | WebSocket server URL | ws://localhost:3001 |
BOT_NAME | Display name | SimpleBot |
BOT_ID | Unique identifier | Random |
TABLE_ID | Specific table to join | Auto-discover |
Extending the Bot
To build on this example:
- Better hand evaluation — use hole cards + community cards to assess hand strength
- Position awareness — play tighter from early position, looser from the button
- Pot odds — only call when the pot odds justify it
- Opponent tracking — track how opponents play and adjust (see Opponent Tracking)
- Aggression — add betting and raising instead of just calling
- LLM integration — see the LLM Bot guide for AI-powered decision making