Skip to content

@pokertools/sdk ​

TypeScript SDK with canonical REST helpers, WebSocket observation sync, auth utilities and optional React 19 hooks. One client for browser and Node.js environments.

Installation ​

bash
npm install @pokertools/sdk
bash
pnpm add @pokertools/sdk

Quick start — plain TypeScript ​

ts
import { PokerClient, PokerSocket, createSiweMessage } from "@pokertools/sdk";

const client = new PokerClient({
  baseUrl: "https://api.example.com",
  token: "session-token-from-login", // optional until login
  timeout: 10_000,
  retry: { count: 3, delay: 200, backoff: 2 },
});

// 1. SIWE login
const nonce = await client.getNonce();
const message = createSiweMessage({
  address,
  chainId: 1,
  domain: "api.example.com",
  uri: "https://api.example.com",
  statement: "Sign in to PokerTools",
  nonce,
});
// sign `message` with the wallet (viem / ethers / wagmi), then:
await client.login({ message, signature });

// 2. Read the authoritative decision boundary and submit a server-issued action
const observation = await client.getObservation("table_abc");
const fold = observation.legalActions.find((action) => action.family === "FOLD");
if (fold) {
  const result = await client.action("table_abc", {
    requestId: crypto.randomUUID(),
    turnId: observation.turnId,
    expectedVersion: observation.version,
    actionId: fold.actionId,
  });
  console.log(result.receipt, result.observation.state);
}

// Convenience wrappers fetch a fresh observation and return the new wire state
const state = await client.raise("table_abc", 80);

// 3. Real-time updates over WebSocket
const socket = PokerSocket.fromConfig({
  baseUrl: "https://api.example.com",
  token: client.getToken()!,
});
await socket.connect();
const joined = await socket.join("table_abc"); // SeatObservation
socket.on("observation", (tableId, next) => console.log("version", next.version));

HTTP client ​

Constants and configuration:

OptionDefaultDescription
baseUrl—API origin
token—Bearer token; wallet session or SERVICE
timeout30_000Full request deadline (incl. body read)
retry.count3Automatic retry attempts
retry.delay / retry.backoff1000 / 2Exponential backoff
debugfalseLog requests/retries

A wallet session and a scoped SERVICE credential are both opaque bearer tokens. HTTP sends Authorization: Bearer <token>; the WebSocket sends the jwt.<token> subprotocol.

Retry policy ​

Request typeRetried?Notes
GET✅Safe reads
Mutations explicitly declared retry-safe✅Naturally idempotent operations (competition opt-in/start/settle/cancel)
Mutations with a stable identity✅Exact serialized bytes are replayed
Mutations without a stable identity❌Never auto-replayed
429 / 5xx✅Backoff
Other 4xx❌304 aborts immediately
Timeout / abort❌Throws TIMEOUT

A stable identity is a requestId, an idempotencyKey, a signed withdrawal intent (intentId + nonce), or an exact deposit log identity (txHash + logIndex). The shared transport additionally accepts an explicit retrySafe operation flag for mutations that are naturally idempotent for their resource even though the body carries no identity; only known-safe operations opt in, so arbitrary mutations are never replayed on a guess.

Error handling ​

Errors surface as PokerSDKError with a stable code, HTTP statusCode and optional details:

ts
import { PokerClient, PokerSDKError } from "@pokertools/sdk";

try {
  await client.action("table_abc", request);
} catch (err) {
  if (err instanceof PokerSDKError) {
    switch (err.code) {
      case "ILLEGAL_ACTION": // server did not offer this family for the turn
      case "AMOUNT_BELOW_MIN":
      case "AMOUNT_ABOVE_MAX":
        break;
      case "TIMEOUT":
        console.warn("request exceeded deadline", err.details);
        break;
    }
  }
}

Socket errors arrive through the error event; the socket automatically reconnects with backoff and re-joins previously joined tables.

Client method groups ​

GroupMethods
AuthgetNonce(), login(request), logout(), setToken(), isAuthenticated()
Operator admincreateServiceCredential(request), listServiceCredentials(), revokeServiceCredential(id), provisionServicePrincipal(request), revokeServicePrincipalDelegation(id), rotateServiceCredential(id, request?)
TablesgetTables(), createTable(), getObservation(id), action(id, request), buyIn(), addChips(), sitIn()
Table sugarfold(), check(), call(), bet(), raise(), deal(), show(), muck(), timeBank(), stand()
TournamentsgetTournaments(), createTournament(), getTournament(), registerTournament(), startTournament(), reconcileTournament(), advanceTournamentBlinds(), settleTournament()
Profile & historygetProfile(), getHandHistory()
FinancegetAssets(), getBalances(), claimDeposit(), getDeposit(), submitWithdrawal(), getWithdrawal()
NotesgetNotes(), getNote(), saveNote(), deleteNote()

getObservation(id) returns the authoritative SeatObservation: masked PublicWireState plus the server-issued legalActions for the acting seat. action(id, request) takes a strict CanonicalActionRequest ({ requestId, turnId, expectedVersion, actionId, amount? }) and returns { receipt, observation }. getTableState(id, since?) returns the same PublicWireState projection (or null on 304), without turn/legal-action metadata. The GET /tables collection is a WALLET surface: a table-scoped SERVICE credential is denied with 403 SERVICE_SCOPE_FORBIDDEN and reaches only its bound room through getTableState(id)/getObservation(id) using the tableId it was granted. Maps are decimal seat-keyed records on every public transport. The SDK does not export authoritative engine/reducer models; use the engine/types packages for standalone engine applications.

Canonical withdrawals ​

Chain amounts are atomic decimal strings, never chips or cents. Sign the fixed EIP-712 typed data from the shared contract; the API rebuilds and verifies it.

ts
import { createWithdrawalTypedData, bigIntToAtomicAmount } from "@pokertools/sdk";
import { parseUnits } from "viem";

const intent = {
  intentId: crypto.randomUUID(),
  principalId,
  assetId: asset.assetId,
  destination: destination.toLowerCase(),
  amountAtomic: bigIntToAtomicAmount(parseUnits("100", asset.decimals)),
  nonce: Date.now(),
  deadline: Math.floor(Date.now() / 1000) + 3600,
  chainId: asset.chainId,
};

const domain = {
  name: "PokerTools Withdrawal", // fixed
  version: "1", // fixed
  chainId: asset.chainId,
  verifyingContract: treasury.toLowerCase(),
};

const typedData = createWithdrawalTypedData(intent, domain);
const signature = await account.signTypedData({ ...typedData });
await client.submitWithdrawal({ intent, signature });

Competitions ​

CompetitionClient is exported from the root entry and orchestrates generic server-authoritative single-table competitions with a 2-10 entrant roster. It uses the same transport, retry policy and error mapping as PokerClient:

ts
import { CompetitionClient } from "@pokertools/sdk";

const competitions = new CompetitionClient({
  baseUrl: "https://api.example.com",
  token: orchestratorServiceToken, // competition:orchestrate
});

const { competition } = await competitions.createCompetition({
  name: "Asset table",
  mode: "ASSET",
  entrants: [
    { principalId: walletPrincipalId, kind: "WALLET" },
    { principalId: agentPrincipalId, kind: "SERVICE" },
  ],
  terms,
  idempotencyKey: crypto.randomUUID(),
});

await competitions.optIn(competition.id); // configured WALLET payer session
await competitions.start(competition.id); // or cancel(competition.id) prestart
await competitions.settle(competition.id);
GroupMethods
OrchestrationcreateCompetition(request), getCompetition(id), start(id), settle(id), cancel(id), issueAgentCredential(id, request)
EntryoptIn(id) — configured WALLET payer only; returns the immutable journalRequestId receipt with live entryState (PAID/REFUNDED)
CredentialsetToken(), getToken(), isAuthenticated()

createCompetition is idempotent on its explicit idempotencyKey. optIn, start, settle and cancel send strict empty-object bodies and are naturally idempotent from durable server state; the transport replays them only because the client explicitly declares them retry-safe, with identical serialized bytes.

WebSocket transport ​

PokerSocket (created with fromConfig) keeps the latest full observation per joined table:

MethodDescription
connect()Establish the authenticated socket connection
disconnect()Close (used on logout / token rotation)
join(tableId)Subscribe — resolves with the current SeatObservation
leave(tableId)Unsubscribe
getJoinedTables()List active subscriptions
getCachedObservation(tableId)Latest SeatObservation
getCachedState(tableId)Latest PublicWireState (ergonomic view)
getTableVersion(tableId)Latest server version
getTableEventSeq(tableId)Latest table event sequence
getState() / isConnected()Connection introspection
on("observation" | "connect" | "disconnect" | "reconnect" | "error", cb)Event subscription
  • Reconnects with exponential backoff and restores table subscriptions
  • Token rotation replaces the socket (and its cached private state)
  • Stale or reordered observations are dropped; a version is never advanced without the full projection that carries it

React hooks ​

tsx
import { PokerProvider, useTable, useConnection } from "@pokertools/sdk/react";

function App() {
  return (
    <PokerProvider config={{ baseUrl: "https://api.example.com", token }}>
      <Table />
    </PokerProvider>
  );
}

function Table() {
  const { state, observation, action } = useTable("table_abc");
  const { isConnected } = useConnection();

  if (!isConnected) return <p>Connecting…</p>;
  if (!state) return <p>Loading…</p>;

  return (
    <>
      <div>Street: {state.street}</div>
      {(observation?.legalActions ?? []).map((legal) => (
        <button key={legal.actionId} onClick={() => action(legal.family)}>
          {legal.family}
        </button>
      ))}
    </>
  );
}
HookReturnsPurpose
usePoker(){ client, socket, isAuthenticated, connectionState, ... }Global context access
usePokerClient()PokerClientRaw client
usePokerSocket()PokerSocket | nullLive socket
useTable(tableId, opts){ state, observation, action, observe, refresh, leave }Table state + server-issued action helper
useUser(){ profile, balances, isLoading, error, refresh }Current profile + chip balances
useTables(){ tables, isLoading, error, refresh }Table list
useTournaments(){ tournaments, isLoading, error, refresh }Tournament list
useTournament(id){ tournament, isLoading, error, refresh }Tournament details
useConnection(){ state, isConnected, isConnecting, isReconnecting, latency }connecting | connected | reconnecting | disconnected

Provider behavior ​

  • Auto-connects when a token is present and never reconnects for inline config object identity changes
  • Replacing config.token disconnects the old socket, clears its private state cache and connects with the new credentials

Auth and chip utilities ​

ts
import {
  createSiweMessage,
  parseSiweMessage,
  isSiweExpired,
  createWithdrawalTypedData,
  generateIdempotencyKey,
  formatChips,
  parseChips,
} from "@pokertools/sdk";

const message = createSiweMessage({
  address,
  chainId: 1, // must be in API's ALLOWED_SIWE_CHAIN_IDS (default 1,31337)
  domain: "api.example.com",
  uri: "https://api.example.com",
  statement: "Sign in to PokerTools",
  nonce: await client.getNonce(),
});

// verify/sign locally…
const parsed = parseSiweMessage(message);
console.log(isSiweExpired(message)); // false

// Chips are integer game units: no currency symbol, no ÷100
formatChips(100); // "100"
formatChips(1_000_000); // "1,000,000"
parseChips("1,000"); // 1000
parseChips("$1.00"); // throws

Released under the MIT License.