pokerai.bet API documentation

Two poker APIs, one key & quota: the GTO strategy API (/v1/gto/* — every street preflop/flop/turn/river, returns each action's mixed frequency) and the Poker engine / semantics API (/v1/pokerkit/* — board/hand semantics, eval, draws, range equity, game simulation). Both live in the interactive reference.

GTO strategy API · two ways to get a strategy: (1) Presolved solutions — preflop / flop tap a library of presolved GTO solutions, returned in milliseconds; (2) Real-time solver computation — flop / turn / river are computed live by the solver (starting from the flop takes a while). There is also range conversion to update ranges along an action line.

Scope: currently only 6-handed tables (6-max) and a 100BB starting effective stack are supported. More table sizes and stack depths are coming soon.

Stateless / self-contained requests: each request is independent, the server keeps no hand state, and you are not required to call earlier streets first — you can query any spot directly, just supply all the information that spot needs.

Two APIs, one key: this page is the GTO strategy API (/v1/gto/*). There is also a Poker engine & semantics API (/v1/pokerkit/*: board texture, made-hand/draws/outs/blockers, range equity, hand eval, ICM, game simulation…), reusing the same API key and quota (cheap endpoints charge the general bucket, Monte-Carlo the solve bucket). Both live in the interactive reference (switch in the top bar); its own docs/spec are at /v1/pokerkit/docs · openapi.json (auto-synced with the code).

Board / hand format (unified): across all endpoints the board and hole_cards inputs use an unseparated string (each card is 2 chars: rank AKQJT98765432 + suit c d h s), e.g. "board": "2c2h2s", "hole_cards": "AdKd". In responses board is always returned as an array, e.g. ["2c","2h","2s"].

Range notation: range strings such as oop_range / ip_range are comma-separated hand:weight. Hands use class notation — pairs AA, suited AKs, offsuit AKo (higher rank first); weight is 0–1, omitted defaults to 1. Example: "AA:1,KK,AKs:0.5,72o:0.1". Note: the keys of range_strategy and range_*_new_raw use specific combo notation (e.g. "AhKh").

Machine-readable spec: OpenAPI 3.0 spec — importable into Postman / Insomnia, or use openapi-generator to generate clients in any language. Interactive reference: online API reference (try it live) — enter your Key and fire requests endpoint by endpoint (this consumes real quota).

Auth

Every request must carry an API Key. Get a Key in 60 seconds:

  1. Open the console, enter your email → receive a verification code by email (passwordless login, no signup needed).
  2. Enter the code to log in → create an API Key → copy it (shown only once, store it safely).
  3. Set it as an environment variable: export POKERAI_API_KEY=gto_xxxxxxxx, and you can run the Quickstart below.

From then on, send the same Key (the gto_xxx you just copied — this is the “Bearer token”) in a request header on every request. Pick either form below — it is one key, not two:

Authorization: Bearer gto_xxxxxxxx     # standard (recommended; matches OpenAPI scheme BearerApiKey)
# or (fully equivalent, pick one)
X-API-Key: gto_xxxxxxxx                 # equivalent (OpenAPI scheme XApiKey); handy for some gateways / SDKs / quick tests

Quota

Two kinds of monthly quota, metered separately and reset on the 1st of each month; current usage is in the console:

Error model

All errors return a uniform JSON: { "error": "<code>", "message": "<description>" }. A few variants: some 502s use reason instead of message; the 429 for all solvers being busy looks like { "status": "busy", "message": ... }. The endpoint-specific error values are in each endpoint's "Possible errors" table; the common status codes are below:

HTTPerror / meaningretry?
400Invalid input or missing field (see each endpoint's table for the specific error).No, fix the input
401missing_api_key / invalid_api_key: missing or invalid Key.No, check the Key
403invalid_node_token / invalid_solve: token/handle invalid or not yours.No, re-fetch the tree / reschedule first
404no_solution: no GTO data for this spot yet.No, change the spot
429quota_exceeded (general) / solve_quota_exceeded (solve): monthly quota used up.No, resets on the 1st or upgrade your quota
429status: busy: all solvers busy (only /v1/gto/solver), not charged.Yes, back off and retry
502no_result (reason: timeout / no_worker_available) / auth_unavailable / solver_unreachable: backend transiently unavailable.Yes, back off and retry 2–3 times

Error response body examples

400 (invalid input):

{
  "error": "invalid_board",
  "message": "board must be 3 cards, e.g. \"2c2h2s\""
}

401 (missing Key) / 404 (no data) / 429 (quota used up) — single field or short message:

{ "error": "missing_api_key" }
{ "error": "no_solution", "message": "no GTO data for this spot/board" }
{ "error": "quota_exceeded" }

502 (backend transiently unavailable, retryable):

{
  "error": "no_result",
  "reason": "timeout"
}

Retry: only 429 busy and 502 are worth retrying — use exponential backoff (start ~1s, double, at most 2–3 times). The rest (400/401/403/404/quota exhausted) are terminal and retrying is useless: fix the input / change the Key / re-fetch the tree, or wait for the quota to reset on the 1st of the month. There is no per-second rate limit, so there is no Retry-After header.

Quickstart

Once you have a Key (see above), copy this curl (replace $POKERAI_API_KEY with yours) — the simplest successful call: Hero has an opening opportunity in UTG (RFI), and the preflop presolved solution returns in milliseconds:

curl -s https://pokerai.bet/v1/gto/preflop \
  -H "Authorization: Bearer $POKERAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"hole_cards":"AhKh","positions":{"hero":"UTG"},"preflop_actions":[{"position":"SB","action":"small blind","amount":0.5},{"position":"BB","action":"big blind","amount":1}]}'

Real response (UTG with AKs, opening opportunity → opens 100% to 3BB):

{
  "hole_cards": "AhKh",
  "situation": "RFI",
  "strategy": [
    { "action": "raise", "frequency": 1, "amount_bb": 3, "sizing_pot": 0.8 }
  ],
  "quota": { "used": 6, "limit": 100 }
}

Facing a raise? Just add the opponent's action to preflop_actions (append {"position":"UTG","action":"raise","amount":3} and change Hero to MP → it becomes a 3bet spot, and situation returns Raise). Each action's mixed frequency is returned (no recommended action; choose yourself based on frequency). The sections below are organized in three parts: Presolved solutions (preflop / flop), real-time solver computation, and range conversion.

Client SDKs Python · TypeScript · MCP

Don't want to hand-write HTTP? The official clients are auto-generated from the OpenAPI spec and fully typed, so they always track the API. Auth is just your API key.

Python pip

pip install pokerai-bet   # distribution name pokerai-bet, import as pokerai

The same Quickstart spot (Hero UTG open), typed:

from pokerai import AuthenticatedClient
from pokerai.api.lookup import preflop_strategy
from pokerai.models import (
    PreflopRequest, PreflopRequestPositions, PreflopRequestPreflopActionsItem,
)
from pokerai.models.preflop_request_preflop_actions_item_action import (
    PreflopRequestPreflopActionsItemAction as Act,
)
from pokerai.models.position import Position

client = AuthenticatedClient(base_url="https://pokerai.bet", token="gto_...")

resp = preflop_strategy.sync(client=client, body=PreflopRequest(
    hole_cards="AhKh",
    positions=PreflopRequestPositions(hero=Position.UTG),
    preflop_actions=[
        PreflopRequestPreflopActionsItem(position=Position.SB, action=Act.SMALL_BLIND, amount=0.5),
        PreflopRequestPreflopActionsItem(position=Position.BB, action=Act.BIG_BLIND, amount=1.0),
    ],
))
print(resp.to_dict())

Real output (same spot as the Quickstart):

{"hole_cards": "AhKh", "situation": "RFI",
 "strategy": [{"action": "raise", "frequency": 1, "sizing_pot": 0.8, "amount_bb": 3}],
 "quota": {"used": 2702, "limit": 100000}}

TypeScript / JavaScript npm

npm install @pokerai/client
import { createPokeraiClient } from "@pokerai/client";

const client = createPokeraiClient({ apiKey: "gto_..." });

const { data, error } = await client.POST("/v1/gto/preflop", {
  body: {
    hole_cards: "AhKh",
    positions: { hero: "UTG" },
    preflop_actions: [
      { position: "SB", action: "small blind", amount: 0.5 },
      { position: "BB", action: "big blind", amount: 1 },
    ],
  },
});
if (error) throw new Error(JSON.stringify(error));
console.log(data.situation, data.strategy);
// "RFI" [{ action: "raise", frequency: 1, amount_bb: 3, sizing_pot: 0.8 }]

Paths, request bodies, and response fields are all type-checked — your editor autocompletes the whole API. Types are generated from the spec by openapi-typescript; the runtime is openapi-fetch.

MCP (for LLM agents) npm

Let Claude / Cursor and friends call the Pokerai API as tools (@pokerai/mcp):

// mcp.json
{ "mcpServers": { "pokerai": {
  "command": "npx", "args": ["-y", "@pokerai/mcp"],
  "env": { "POKERAI_API_KEY": "gto_..." }
}}}

5 presolved-lookup tools by default; add "POKERAI_ENABLE_SOLVE": "1" to enable the real-time solver tools (consumes solve quota).

Concepts

The overall mental model — read this once and the per-endpoint sections below will go more smoothly.

Two ways to get a strategy

No recommended action is returned

Every strategy returns each action's mixed frequency (0–1) and does not pick the action for you — you implement it yourself from frequency (take the max probability, or sample randomly by frequency).

Bet sizing: amount_bb and sizing_pot

Each bet/raise carries two sizing fields: amount_bb (the absolute amount, i.e. the BB you raise to) and sizing_pot (the value relative to the pot, the standard % of pot convention):

Example: 3bet to 9 facing an open of 3 — pot is 4.5, after the call 4.5+3=7.5, the raise exceeds by 9−3=6, so sizing_pot = 6/7.5 = 0.8. When all-in it also carries allin: true.

tree → node (fetch tree → fetch node)

Both the flop decision tree and the real-time solver are two steps: first fetch the whole tree (each decision node carries a token), then use the Hero node's token to fetch that step's strategy.

fetch tree   /flop/tree   or   /solver (+ poll /solver/tree)
         └─→ nodes[]: each node carries is_hero + token
               └─→ pick the node with is_hero:true
fetch node   /flop/node  or   /solver/node    (body carries that node's token)
               └─→ the mixed strategy of this step

Node path notation (two conventions, depending on the tree's source): the flop decision tree uses BET_8 (underscore, integer BB); the solver tree uses BET 8.000000 (space, 6 decimals). When passing node_id / navigating, it must match the labels in that tree (or in solver_results) character for character.

Node state machine (real-time solving)

After scheduling, poll the spot_status of /solver/tree: available (not scheduled) → computing (solving, keep polling) → queryable (can fetch nodes) → expired (cache reclaimed by TTL, must reschedule). You neither need to nor can manually release compute resources.

Presolved solutions presolved · milliseconds

preflop and flop tap presolved GTO solutions, served instantly, suited to use cases that need a fast response. The effective stack is fixed at 100BB (presolved, not an input). To compute the flop live with the real solver, see the next section.

Preflop

POST https://pokerai.bet/v1/gto/preflop charges 1 general

Full parameter / response schema, and try it live → interactive reference.

You don't need to judge "how many bets the pot is" — give Hero's preceding preflop actions in order, and the server derives the spot automatically (unopened / facing a raise / 3bet / 4bet…).

Notes · request body

fieldtypedescription
hole_cardsstringHero's 2 hole cards, e.g. "AdKd".
positions.herostringHero's position, one of SB BB UTG MP CO BTN (placed under positions).
preflop_actionsarrayThe complete, explicit action sequence from the small blind up to the player just before Hero (Hero is not in the sequence; Hero's position is given by positions.hero, and the sequence stops at the player before Hero). Each item is { position, action, amount, allin? }, see the table below.
preflop_versionstringOptional. Which 6max preflop chart set to use: 6max (default) / 6max_RC_100bb_200NL / 6max_RC_100bb_100NL / 6max_RC_40bb. Omit for the platform default (6max); an unknown value -> 400 unsupported_preflop_version. Different versions give different frequencies for the same spot.

Fields of each preflop_actions item

fieldtypedescription
positionstringThe position of this action, one of SB BB UTG MP CO BTN.
actionstring"small blind" / "big blind" / "raise" / "call" / "fold" (note the blinds are two-word strings).
amountnumberThe incremental amount newly invested by this action (BB, not the cumulative total). Examples: small blind 0.5; big blind 1; an opening raise to 3 → amount 3 (from 0); a player who already invested 1 reraising to 9 → amount 8. fold omits it (counts as 0). Pot = sum of all amount.
allinbooleanOptional. Marks a short-stack all-in (bet/call amount below the minimum raise); when true, the minimum-raise check is skipped.

Validation (violation → 400 invalid_actions): the sequence must start with small blind (0.5), then big blind (1); each raise/call needs a positive amount; a raise's cumulative total must exceed the current bet and meet the minimum raise (= current bet + the size of the previous raise; so opening ≥ 2BB, and a 3bet over an open-to-3 must be ≥ 5BB) — unless allin:true; a call's cumulative total must exactly equal the current bet — unless allin:true.

Exact amounts only affect the pot / sizing_pot, not the frequencies: giving exact amount values only makes the pot / sizing_pot exact; GTO frequencies are determined by the spot (RFI / 3bet / 4bet + position) and do not change with bet size.

Call example

curl -s https://pokerai.bet/v1/gto/preflop \
  -H "Authorization: Bearer $POKERAI_API_KEY" -H "Content-Type: application/json" \
  -d '{"hole_cards":"AhKh","positions":{"hero":"MP"},"preflop_version":"6max_RC_100bb_200NL",
       "preflop_actions":[{"position":"SB","action":"small blind","amount":0.5},
                          {"position":"BB","action":"big blind","amount":1},
                          {"position":"UTG","action":"raise","amount":3}]}'

Omit preflop_version for the default (6max). Available versions are listed below, or query them live with GET /v1/gto/preflop/versions.

Response (200)

{"hole_cards": "AhKh", "situation": "Raise", "strategy": [{"action": "raise", "frequency": 1, "amount_bb": 9, "sizing_pot": 0.8}], "quota": {"used": 7, "limit": 100}}
fielddescription
hole_cardsThe echoed hand.
situationThe state the table faces when it's Hero's turn: RFI (no one in the pot, Hero has an opening opportunity) / Limp (someone limped, no raise) / Raise (facing one opening raise) / 3-Bet / 4-Bet / 5-Bet. Note: BB always lands on Limp (someone must have entered the pot before it acts).
strategy[]The mixed strategy of each action. raise carries amount_bb (the absolute amount raised to, in BB) and sizing_pot (see bet sizing). Preflop amount_bb is set by the number of raises before Hero, see the table below. No recommended action is returned; choose yourself based on frequency.
quotaThis month's general quota usage (used / limit).

Preflop amount_bb (derived from the presolved flop pot, raised to):

number of raises before Herospotamount_bb
0open3
13bet9
24bet25
≥35bet+all-in 100 (allin: true)

Strategy versions preflop_version

Same 6max, different chart sets (frequencies differ for the same spot); omit for the default 6max. The authoritative list is the discovery endpoint GET /v1/gto/preflop/versions (free; returns id + label + default):

id (pass as preflop_version)description
6max (default)6 max 100bb Deepsolver
6max_RC_100bb_200NL6 max 100bb GG 200NL 3b/f 2.2x - 2.5x
6max_RC_100bb_100NL6 max 100bb GG 100NL 3b/f
6max_RC_40bb6 max 40bb GG 100NL
curl -s https://pokerai.bet/v1/gto/preflop/versions -H "Authorization: Bearer $POKERAI_API_KEY"
// {"versions": [{"id": "6max", "label": "6 max 100bb Deepsolver", "default": true}, …], "default": "6max"}

Possible errors

HTTPerrortrigger / how to fix
400invalid_hole_cardshole_cards is not 2 cards (e.g. "AdKd").
400unsupported_table_size / invalid_positions / invalid_actionsTable type (currently only 6max), hero/positions, or preflop_actions is invalid.
400unsupported_preflop_versionpreflop_version is not in the allowed set (6max / 6max_RC_100bb_200NL / 6max_RC_100bb_100NL / 6max_RC_40bb).
404no_solutionNo presolved solution for this preflop spot; change the spot.

Download: request.jsonresponse.json

Whole range (13×13) charges 1 general

POST https://pokerai.bet/v1/gto/preflop/range · the full 13×13 range for a spot (position + action line), returning all 169 hand types' fold/call/raise in one call. No hole_cards (the spot comes from positions + preflop_actions). Charges 1 general quota (one call, not 169). For rendering a range grid.

// Request (no hole_cards)
{"table_size": "6max", "positions": {"hero": "BTN"}, "preflop_actions": [{"position": "SB", "action": "small blind", "amount": 0.5}, {"position": "BB", "action": "big blind", "amount": 1}, {"position": "UTG", "action": "fold"}, {"position": "MP", "action": "fold"}, {"position": "CO", "action": "fold"}]}

// Response
{ "range": {"22": {"fold": 0.69, "call": 0, "raise": 0.31}, "33": {"fold": 0, "call": 0, "raise": 1}, "ATs": {"fold": 0, "call": 0, "raise": 1}, "AKs": {"fold": 0, "call": 0, "raise": 1}, "AKo": {"fold": 0, "call": 0, "raise": 1}, "KQs": {"fold": 0, "call": 0, "raise": 1}, "72o": {"fold": 1, "call": 0, "raise": 0}, "JJ": {"fold": 0, "call": 0, "raise": 1} , …169 hands total… },
  "quota": {"used": 7, "limit": 100} }

Hand notation: pair AA, suited AKs, offsuit AKo (high card first); each entry's fold+call+raise≈1. Full schema / try it live → interactive reference.

Flop decision tree

The flop strategy is a decision tree: first fetch the tree (getting all decision nodes + each node's token), your system walks the path on the tree by the actual bets, and at Hero's node fetches that step's strategy. Two steps, same as the solver (treenode).

1) Fetch the decision tree charges 1 general

POST https://pokerai.bet/v1/gto/flop/tree · input board + pot_type + positions (no hole_cards needed, the tree is independent of the hand).

Full parameter / response schema, and try it live → interactive reference.

boardThe 3 flop cards, e.g. "2c2h2s".
pot_type"SRP" single-raised / "3BET" / "4BET" / "LIMP" limped.
positionsThe position of each role (SB BB UTG MP CO BTN), required per pot_type as below.
flop_versionOptional. Which flop dataset (one solved per preflop version): 6max (default) / 6max_RC_100bb_200NL / 6max_RC_100bb_100NL / 6max_RC_40bb. Omit for the default (6max); if that version has no data for the spot it degrades gracefully to 6max; an unknown value -> 400 unsupported_flop_version. Independent of preflop_version. The node tokens carry this version, so /v1/gto/flop/node stays on the same dataset.
pot_typerequired positionsHero value
SRPhero, raiser, callerraiser or caller
3BET / 4BEThero, raiser, three_bettorraiser or three_bettor
LIMPhero, limperone of Hero or limper must be BB
// Request (no hole_cards)
{"board": "2c2h2s", "pot_type": "SRP", "positions": {"hero": "UTG", "raiser": "UTG", "caller": "BTN"}}

// Response (36 nodes total; first 5 shown)
{"board": ["2c", "2h", "2s"], "pot_type": "SRP", "pot": 7.5, "effective_stack": 97, "oop_range": "AA:1,AKs:1,AQs:1,AJs:1,ATs:1,A9s:1,A8s:1,A7s:1,A6s:1,A5s:1,…", "ip_range": "AQs:0.05,AJs:0.86,ATs:0.436,A9s:0.356,A8s:0.36,A7s:0.196,…", "node_count": 36, "nodes": [{"node": "root", "is_hero": true, "token": "eyJ…"}, {"node": "root/BET_4", "is_hero": false, "token": "eyJ…"}, {"node": "root/BET_8", "is_hero": false, "token": "eyJ…"}, {"node": "root/BET_97", "is_hero": false, "token": "eyJ…"}, {"node": "root/CHECK", "is_hero": false, "token": "eyJ…"}, …], "quota": {"used": 7, "limit": 100}}
fielddescription
oop_range / ip_rangeThe starting range of this spot (weighted combo string), used as the starting weights for range conversion.
nodes[]All decision nodes: node (action path, e.g. "root/CHECK/BET_8"), is_hero (whether it's a Hero decision point), token (the credential to fetch that node's strategy, passed to step 2; bound to account + spot, cannot be forged).
pot / effective_stack / node_countPot, effective stack (BB), and total number of decision nodes.
quotaThis month's general quota usage (used / limit).

2) Fetch a node's strategy free

POST https://pokerai.bet/v1/gto/flop/node · carries node (the token of some node from step 1). With hole_cards → the mixed strategy for that hand; without hole_cards → the full range strategy.

Full parameter / response schema, and try it live → interactive reference.

Every Hero decision goes through here: Hero's first action = the root node (OOP acts first, check/bet); when Hero is IP, facing a bet, or acting a second time, pick the corresponding is_hero:true node (e.g. root/CHECK/BET_8 = I checked and now face a bet → fold/call/raise). The flop tree is single-street; for cross-street (turn/river) use /v1/gto/solver/*.

// Request (Hero node, with hole_cards) -> Hero strategy
{"node": "eyJ…", "hole_cards": "AdKd"}
{"hole_cards": "AdKd", "node": "root", "is_hero": true, "strategy": [{"action": "check", "frequency": 0.7208}, {"action": "bet", "amount_bb": 4, "sizing_pot": 0.5333, "frequency": 0.0533}, {"action": "bet", "amount_bb": 8, "sizing_pot": 1.0667, "frequency": 0.2259}, {"action": "bet", "amount_bb": 97, "sizing_pot": 12.9333, "allin": true, "frequency": 0}]}

// Request (no hole_cards) -> whole-range strategy
{"node": "eyJ…"}
{"node": "root/BET_4", "is_hero": false, "actions": [{"action": "call"}, {"action": "raise", "amount_bb": 12, "sizing_pot": 0.5161}, {"action": "raise", "amount_bb": 20, "sizing_pot": 1.0323}, {"action": "raise", "amount_bb": 97, "sizing_pot": 6, "allin": true}, {"action": "fold"}], "range_strategy": {"3d3c": [0.93067, 4e-05, 0.06922, 1e-05, 7e-05], "3h3c": [0.92172, 6e-05, 0.07812, 1e-05, 0.0001], "3h3d": [0.94334, 4e-05, 0.05655, 1e-05, 7e-05], "…": "188 hands total"}, "range_hand_count": 188}
fielddescription
is_heroWhether this node is Hero's decision.
strategy[]Hero node (with hole_cards): the mixed strategy for this hand. actioncheck / bet / call / raise / fold (bet = first bet, raise = a raise facing a bet); bet/raise carry amount_bb and sizing_pot (see bet sizing), and all-in also carries allin: true; frequency is a 0–1 probability. No recommended action is returned; choose yourself based on frequency.
actions[] + range_strategyOpponent node (or no hole_cards): the full range strategy, with each hand's frequencies aligned to actions; includes range_hand_count. Can be used to assemble solver_results for "range conversion".

The BET_8 / RAISE_20 in a node id is the absolute bet amount (BB).

Possible errors

HTTPerrortrigger / how to fix
4001) invalid_board / invalid_positions; 2) missing_node / invalid_hole_cards1) board is not 3 cards, or hero/positions invalid; 2) missing node or hole_cards is not 2 cards.
403invalid_node_token(step 2) node token invalid or not yours — call /v1/gto/flop/tree to fetch the tree first.
404no_solutionNo presolved solution for this spot/board.

Download: tree_request.jsontree_response.jsonnode_request.jsonnode_response.json

Solver (real-time) solver

POST https://pokerai.bet/v1/gto/solver family. Pure postflop solver, computed in real time, using a separate solve quota. The essence is input-to-solve: board + oop/ip ranges + pot + behind stack + who is Hero, with no history needed, so you can enter from any spot. Three steps: schedule → poll for the tree → fetch a node's strategy.

Full parameter / response schema, and try it live → interactive reference (includes /solver/tree, /solver/node).

board length sets the street: 3=flop / 4=turn / 5=river. ⚠ Real-time solving starting from the flop takes a while (measured SRP flop ~70 seconds), and is not suited to use cases that need a fast response — if you want a fast flop, use "Presolved solutions" above (served instantly in milliseconds); turn / river are faster (seconds to tens of seconds).

Call example (three steps: 1) schedule → 2) poll for the tree → 3) fetch a node)

# 1) schedule (board 3/4/5 = flop/turn/river; flop ~70 seconds)
curl -s https://pokerai.bet/v1/gto/solver -H "Authorization: Bearer $POKERAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"board":"2c2h2s","oop_range":"AA,KK,QQ,AKs","ip_range":"JJ,TT,AQs,KQs","pot":7.5,"effective_stack":97,"hero":"OOP"}'
# 2) poll for the tree until spot_status=queryable; 3) fetch the node
curl -s https://pokerai.bet/v1/gto/solver/tree -H "Authorization: Bearer $POKERAI_API_KEY" \
  -H "Content-Type: application/json" -d '{"solve":"eyJ..."}'

1) Schedule a solve charges 1 solve

board3=flop / 4=turn / 5=river, e.g. "2c2h2s9d" (turn).
oop_range / ip_rangeRequired. The two players' ranges entering this street, weighted combo strings, e.g. "AsKs:1,QQ:0.75,...".
pot / effective_stackRequired. The pot and behind effective stack (BB) entering this street; they determine the bet sizes and must be real.
heroRequired: "OOP" or "IP".
bet_sizesOptional: override the bet sizes per street, e.g. {"flop":[33,75],"turn":[33,67,120],"river":[33,75,150]} (% of pot); if flop is omitted it defaults to 50%.
// Request (flop)
{"board": "2c2h2s", "oop_range": "AA,KK,QQ,AKs", "ip_range": "JJ,TT,AQs,KQs", "pot": 7.5, "effective_stack": 97, "hero": "OOP"}

// Response (first trigger, charges 1 solve)
{"status": "computing", "solve": "eyJ0Ijoic2x2XzJmMjk2MWU3Y2Y0ODU3OGUiLCJ1IjoiYjk4Y2Y5NzAtZTVjMS00Njk5LTg4ODQtOGQzYzcwMGIyNGJlIiwidHMiOjE3ODIxMjkxMTUyMDJ9.rtmeNcZoBNWJRhkAGkTSSR58ZafpAh35mi510bgPU6c", "solve_quota": {"used": 1, "limit": 100}}
field / casedescription
solveThe solve handle, used by the subsequent /tree and /node; the range is given only once in this step.
status = computingA new solve was triggered, charges 1 solve quota (see solve_quota).
status = queryableA cached solve already exists for this spot, not charged (no solve_quota field), poll /tree directly.
429 status = busyAll solvers are busy, not charged, retry later.

Cache hit (not charged again): re-scheduling the same spot (same board/range/pot/stack/hero/bet_sizes) no longer charges solve quota — if the response has no solve_quota field it means not charged; just use the returned solve handle to poll /tree. Examples: request / response.

2) Fetch the decision tree + node status free

POST https://pokerai.bet/v1/gto/solver/tree · carries the solve handle, poll until spot_status = queryable. To reach a later street, pass the dealt cards: turn_card (a flop solve → a specific turn) and/or river_card. A river spot from a flop solve needs BOTH turn_card + river_card (river alone isn't unique); a turn solve needs only river_card; omit both for the solve's own street.

// (2) fetch this street (flop) tree
{"solve": "eyJ…"}
{"street": "flop", "spot_status": "queryable", "pot": 7.5, "effective_stack": 97, "solve_seconds": 25.84, "node_count": 14, "nodes": [{"node": "root", "is_hero": true, "status": "queryable", "token": "eyJ…"}, {"node": "root/BET 4.000000", "is_hero": false, "status": "queryable", "token": "eyJ…"}, {"node": "root/BET 4.000000/RAISE 12.000000", "is_hero": true, "status": "queryable", "token": "eyJ…"}, …]}

// turn tree (flop solve + turn_card)
{"solve": "eyJ…", "turn_card": "9d"}
{"street": "turn", "spot_status": "queryable", "pot": 7.5, "effective_stack": 97, "solve_seconds": 25.84, "node_count": 104, "nodes": [{"node": "root/BET 4.000000/CALL/9d", "is_hero": false, "status": "queryable", "token": "eyJ…"}, {"node": "root/BET 4.000000/CALL/9d/BET 10.000000", "is_hero": true, "status": "queryable", "token": "eyJ…"}, {"node": "root/BET 4.000000/CALL/9d/BET 10.000000/RAISE 34.000000", "is_hero": false, "status": "queryable", "token": "eyJ…"}, …]}

// river tree (flop solve + turn_card + river_card)
{"solve": "eyJ…", "turn_card": "9d", "river_card": "Qh"}
{"street": "river", "spot_status": "queryable", "pot": 7.5, "effective_stack": 97, "solve_seconds": 25.84, "node_count": 308, "nodes": [{"node": "root/BET 4.000000/CALL/9d/BET 10.000000/CALL/Qh", "is_hero": true, "status": "queryable", "token": "eyJ…"}, {"node": "root/BET 4.000000/CALL/9d/BET 10.000000/CALL/Qh/BET 27.000000", "is_hero": false, "status": "queryable", "token": "eyJ…"}, {"node": "root/BET 4.000000/CALL/9d/BET 10.000000/CALL/Qh/BET 27.000000/RAISE 83.000000", "is_hero": true, "status": "queryable", "token": "eyJ…"}, …]}
fielddescription
spot_statusSolve status: available (not scheduled) / computing (solving, keep polling) / queryable (fetchable) / expired (cache reclaimed, reschedule at step 1) / no_nodes (solve converged but the queried runout/street has no decision nodes in the tree — terminal, stop polling).
nodes[]Each decision node: node (action path), is_hero, status, token (the credential to fetch that node's strategy, passed to step 3). The river runout is a chance node, navigated by river card.
street / pot / effective_stack / node_countStreet, pot, effective stack, and total number of decision nodes.
solve_secondsWall-clock time of this real-time solve, from schedule to convergence (seconds). Returned only when queryable; all runouts of the same solve share this value.

3) Fetch a node's strategy free

POST https://pokerai.bet/v1/gto/solver/node · carries the node token. A Hero node returns the Hero strategy; an opponent node (or no hole_cards) returns the range strategy.

{"node": "eyJ…", "hole_cards": "AhKh"}
{"hole_cards": "AhKh", "node": "root", "is_hero": true, "strategy": [{"action": "check", "frequency": 0}, {"action": "bet", "amount_bb": 4, "sizing_pot": 0.5333, "frequency": 0.2666}, {"action": "bet", "amount_bb": 97, "sizing_pot": 12.9333, "allin": true, "frequency": 0.7334}]}
fielddescription
is_heroWhether this node is Hero's decision.
strategy[]Hero node: Hero's mixed strategy for this hand (action / amount_bb / sizing_pot (see bet sizing) / frequency), with allin: true added when all-in.
actions[] + range_strategyOpponent node (or no hole_cards): in range_strategy each hand's frequencies align to the actions order; also includes range_hand_count.

You neither need to nor can manually release compute resources; the system reclaims them automatically by TTL. When the cache has been reclaimed or replaced by a newer solve, this step returns { "node_status": "expired" }; just reschedule at step 1. A per-node solve error returns { "node_status": "error", "message": … } (terminal — stop polling).

Possible errors

HTTPerrortrigger / how to fix
400invalid_board / missing_range / invalid_pot / invalid_effective_stack / invalid_hero(scheduling) the board, oop/ip range, pot, behind stack, Hero, or other solve input is invalid.
400missing_solve / missing_node(fetch tree/node) missing the solve handle or node token.
403invalid_solve / invalid_node_tokenHandle/token invalid or not yours — reschedule / re-fetch the tree.
429status: busyAll solvers are busy, not charged, back off and retry.
502solve_failedTriggering the solve failed; retry later.
503upstream_unavailable(tree/node) the solver is unreachable after the service's own retries (transport error / 5xx) — retryable.

Download (turn, board=4): schedule_reqschedule_restree_reqtree_resnode_reqnode_res

Download (flop, board=3): schedule_reqschedule_restree_reqtree_resnode_reqnode_res

Download (river, board=5): schedule_reqschedule_restree_reqtree_resnode_reqnode_res

End-to-end polling example (Python)

import requests, time
H = {"Authorization": "Bearer $POKERAI_API_KEY"}
BASE = "https://pokerai.bet/v1/gto"

# 1) schedule (charges 1 solve quota; free on cache hit)
solve = requests.post(f"{BASE}/solver", headers=H, json={
    "board": "2c2h2s9d", "oop_range": "AA,KK,QQ,AKs", "ip_range": "JJ,TT,AQs,KQs",
    "pot": 20, "effective_stack": 90, "hero": "OOP"}).json()["solve"]

# 2) poll for the tree until queryable
while True:
    tree = requests.post(f"{BASE}/solver/tree", headers=H, json={"solve": solve}).json()
    if tree["spot_status"] == "queryable": break
    time.sleep(2)                       # computing -> back off and retry

# 3) fetch Hero's root node strategy
root = next(n for n in tree["nodes"] if n["node"] == "root")
strat = requests.post(f"{BASE}/solver/node", headers=H,
                      json={"node": root["token"], "hole_cards": "AhKh"}).json()
print(strat["strategy"])

Range conversion range

POST https://pokerai.bet/v1/gto/range charges 1 general

Full parameter / response schema, and try it live → interactive reference.

Update the OOP/IP ranges along an action line. Mainly used to get the range that enters the next street (flop→turn / turn→river), then feed it to /v1/gto/solver. All of the range-update computation (including normalize and bluff discount) is done by the solver, and is consistent with the solver's results.

This is a pass-through proxy: you assemble all of the input yourself, including solver_results (the decision tree). The decision tree can be one of your own solves, or assembled from this platform's flop decision tree: call /v1/gto/flop/tree for the starting range, then for each node call /v1/gto/flop/node without hole_cards to fetch the full range_strategy, nested as {node_type,player,strategy,childrens}. See the script at the end.

Request body

{
  "range_oop": "AQs:1,AJs:0.48,...",            // required, starting OOP range
  "range_ip":  "AA:1,AKs:1,...",                 // required, starting IP range
  "solver_results": { /* required: the decision tree */ },
  "node_id": "root/CHECK",          // required, the action line
  "board": "2c2h2s",                             // optional, unseparated string (same as other endpoints)
  "normalize": true,                             // optional, default true
  "explain": false,                              // optional, per-hand change explanation
  "track_hands": ["AA"],                         // optional, track only these hands
  "bluff_discount_ratio": 0.8,                   // optional, bluff discount
  "hero_position": "oop",                        // optional, "oop" / "ip" — which player is Hero (for card blocking)
  "hero_hand": "AsKs"                            // optional, remove combos that contain Hero's cards (blockers); echoed back
}

Card blocking (optional): set hero_position ("oop"/"ip") + hero_hand to remove every combo that contains one of Hero's cards from the ranges — useful for range-vs-hand analysis. Both are echoed back in the response. (This pair is also honored by the projected-range wrappers; /v1/gto/flop/projected-range additionally supports partner_hands.)

Call example

# solver_results is large, put it in a file and use -d @
curl -s https://pokerai.bet/v1/gto/range -H "Authorization: Bearer $POKERAI_API_KEY" \
  -H "Content-Type: application/json" -d @range_request.json

Response (200, real response, long strings truncated)

{"bluff_combos_ratio": 0.33, "bluff_discount_ratio": 1, "board": ["2c", "2h", "2s"], "hand_bottom_ranks_ip": "KsQh:2413,KsQd:2413,KsQc:2413,KhQs:2413,…", "hand_bottom_ranks_oop": "KcJh:2414,KcTs:2415,KsTs:2415,KsTh:2415,…", "hand_ranks_ip": "QcQh:313,QdQh:313,QdQs:313,QhQs:313,QcQs:313,…", "hand_ranks_oop": "Ad2d:155,AdAc:311,AhAc:311,AhAd:311,AsAc:311,…", "node_id": "root/CHECK", "path_length": 1, "range_ip_new": "33:0.193023,44:0.216279,54s:0.65814,…", "range_ip_new_raw": "3c3d:0.193023,3c3h:0.193023,3c3s:0.193023,…", "range_ip_new_raw_before_normalization": "3c3d:0.166,3c3h:0.166,3c3s:0.166,3d3h:0.166,…", "range_oop_new": "33:0.245724,44:0.272975,54s:0.420008,…", "range_oop_new_raw": "3d3c:0.245857,3h3c:0.245847,3h3d:0.24586,…", "range_oop_new_raw_before_normalization": "3d3c:0.245843,3h3c:0.245833,3h3d:0.245845,…", "quota": {"used": 7, "limit": 100}}
fielddescription
range_oop_new / range_ip_newThe normalized updated range (class notation), used directly as the next street's oop_range / ip_range fed to the solver.
range_oop_new_raw / range_ip_new_rawThe intermediate value after bluff discount, not normalized (combo notation); use as needed.
range_oop_new_raw_before_normalization / range_ip_new_raw_before_normalizationThe raw value narrowed only along the action line, without bluff discount or normalization.
hand_ranks_oop / hand_ranks_ipHand strength ranking (combo:rank, smaller value is stronger).
hand_bottom_ranks_oop / hand_bottom_ranks_ipBottom-of-range ranking.
node_id / board / path_lengthThe echoed action line, board, and number of action-line steps.
bluff_discount_ratio / bluff_combos_ratioThe bluff discount parameters actually used this time.
quotaThis month's general quota usage (used / limit).

Possible errors

HTTPerrortrigger / how to fix
400missing_fieldMissing one of range_oop / range_ip / solver_results / node_id.
400bad_requestThe solver side rejected it (e.g. the node_id does not lead anywhere in the decision tree you supplied).

Download: request.json (~1MB, includes the decision tree)response.jsonassemble_solver_results.py (end-to-end script)

Flop projected range projected-range

POST https://pokerai.bet/v1/gto/flop/projected-range charges 1 general

Full parameter / response schema, and try it live → interactive reference.

Narrow the OOP/IP ranges along a flop action line to directly get the starting range that enters the turn. Supply the full spot (board/pot_type/positions) and one action line (node_id), and the platform assembles the decision tree on the server automatically and completes the range update, returning the same fields as range conversion, plus an echoed pot_type.

This is a convenience wrapper over /v1/gto/range: it spares you the manual steps of calling /v1/gto/flop/tree + per-node /v1/gto/flop/node to assemble solver_results. It only applies to flop action lines (the decision tree is assembled from this platform's presolved flop results); for turn→river etc. where you must bring your own decision tree, still use /v1/gto/range.

Request body

{
  "board": "2c2h2s",
  "pot_type": "SRP",
  "positions": { "hero": "UTG", "raiser": "UTG", "caller": "BTN" },
  "node_id": "root/BET_4",
  "normalize": true,
  "bluff_discount_ratio": 0.8,
  "bluff_combos_ratio": 0.5,
  "hero_position": "oop",
  "hero_hand": "AsKs",
  "partner_hands": ["Ac9c"]
}
fielddescription
boardRequired. The flop (unseparated string, same as other endpoints), e.g. "2c2h2s".
pot_typeRequired. The pot type, e.g. "SRP".
positionsRequired. { hero, raiser, caller } (same as flop); 3bet/limp spots may carry three_bettor / limper.
node_idOptional, default "root". The flop action line, using the api notation returned by /v1/gto/flop/tree (absolute bet amount in BB), e.g. "root/BET_4", "root/CHECK/BET_8/CALL".
normalizeOptional, default true. Whether to normalize the updated range.
bluff_discount_ratio / bluff_combos_ratioOptional, each in [0,1] (out of range → 400). bluff_discount_ratio weights the bottom-of-range bluff combos; bluff_combos_ratio is the fraction of the range treated as bluffs. Omit to use the server's turn/river defaults. Both echoed back.
hero_position / hero_handOptional card blocking. Set hero_position ("oop"/"ip") + hero_hand (e.g. "AsKs"): removes every combo containing one of Hero's cards from the villain's updated range (range_*_new_raw), and ensures Hero's own hand is present in Hero's own range. Both echoed back. Only range_*_new_raw is affected — hand_ranks / hand_bottom_ranks are board-filtered only.
partner_handsOptional array of 4-char combos (e.g. ["Ac9c"]), requires hero_position. Removes every combo containing one of those cards from the villain's range_*_new_raw — model known dead cards (folded hands, exposed cards). Input only (not echoed). Also supported on /v1/gto/turn/projected-range.
flop_versionOptional. Which flop dataset (one solved per preflop version): 6max (default) / 6max_RC_100bb_200NL / 6max_RC_100bb_100NL / 6max_RC_40bb. Omit for the default (6max); if that version has no data for the spot it degrades gracefully to 6max; an unknown value -> 400 unsupported_flop_version. Independent of preflop_version.

Call example

curl -s https://pokerai.bet/v1/gto/flop/projected-range -H "Authorization: Bearer $POKERAI_API_KEY" \
  -H "Content-Type: application/json" -d @projected_range_request.json

Response (200, all fields listed, long strings truncated)

{"board": ["2c", "2h", "2s"], "pot_type": "SRP", "node_id": "root/BET_4", "bluff_combos_ratio": 0.5, "bluff_discount_ratio": 0.8, "hero_position": "oop", "hero_hand": "AsKs", "hand_bottom_ranks_ip": "AhTh:2405,AsTs:2405,Ac9c:2406,Ad9d:2406,…", "hand_bottom_ranks_oop": "AhJs:2404,AsJc:2404,AsJd:2404,AsJh:2404,…", "hand_ranks_ip": "QcQd:313,QcQh:313,QcQs:313,QdQh:313,…", "hand_ranks_oop": "Ad2d:155,AdAc:311,AdAh:311,AdAs:311,…", "path_length": 1, "range_ip_new": "33:0.193023,44:0.216279,54s:0.65814,55:0.344186,…", "range_ip_new_raw": "3c3d:0.193023,3c3h:0.193023,3c3s:0.193023,3d3h:0.193023,…", "range_ip_new_raw_before_normalization": "3c3d:0.166,3c3h:0.166,3c3s:0.166,3d3h:0.166,…", "range_oop_new": "44:0.0120032,55:0.0422217,66:0.181192,77:0.327323,…", "range_oop_new_raw": "4s4c:0.0666554,4s4h:0.0666554,5d5c:0.005,5d5h:0.005,…", "range_oop_new_raw_before_normalization": "4s4c:0.011816,4s4h:0.011816,5s5c:0.0392591,5s5h:0.0392591,…", "quota": {"used": 7, "limit": 100}}
fielddescription
range_oop_new / range_ip_newThe normalized updated range (class notation), used directly as the next street's oop_range / ip_range fed to the solver.
range_oop_new_raw / range_ip_new_rawThe intermediate value after bluff discount, not normalized (combo notation); use as needed.
range_oop_new_raw_before_normalization / range_ip_new_raw_before_normalizationThe raw value narrowed only along the action line, without bluff discount or normalization.
hand_ranks_oop / hand_ranks_ipHand strength ranking (combo:rank, smaller value is stronger).
hand_bottom_ranks_oop / hand_bottom_ranks_ipBottom-of-range ranking.
node_id / board / pot_type / path_lengthThe echoed action line, board, pot type, and number of action-line steps.
bluff_discount_ratio / bluff_combos_ratioThe bluff discount parameters actually used this time.
quotaThis month's general quota usage (used / limit).

Possible errors

HTTPerrortrigger / how to fix
400invalid_board / invalid_positionsboard is not 3 cards, or positions.hero is invalid.
404no_solution (and a server errorType such as no_ranges / no_root_node)No presolved flop tree for this spot, or the node_id action line does not lead anywhere.

Download: request.jsonresponse.json

Turn projected range (turn→river) projected-range

POST https://pokerai.bet/v1/gto/turn/projected-range free

Full parameter / response schema, and try it live → interactive reference.

The turn→river version of the flop projected range, for a real-time turn solve. Narrow the ranges along a turn action line (including the street-closing CALL/CHECK) to directly get the starting range that enters the river. The entering-turn OOP/IP ranges are read from the solve's own config (the ranges it was scheduled with via /v1/gto/solver), so you only pass the solve handle solve + one node_id. Free (the solve was already charged via /v1/gto/solver), like /v1/gto/solver/tree; returns the same fields as range conversion.

Poll /v1/gto/solver/tree until spot_status = queryable first. For turn→river you don't assemble solver_results manually (the platform reads it from the solve and assembles it).

Request body

{
  "solve": "eyJ…",
  "node_id": "root/CHECK/BET 6.000000/CALL",
  "normalize": true,
  "bluff_discount_ratio": 0.8,
  "hero_position": "oop",
  "hero_hand": "AsKs"
}
fielddescription
solveRequired. The solve handle returned by /v1/gto/solver (a turn solve).
node_idRequired. A turn action line, may end in the street-closing CALL/CHECK. Solver node notation (with spaces), e.g. "root/CHECK/BET 6.000000/CALL" (from /v1/gto/solver/tree nodes).
normalizeOptional, default true. Whether to normalize the updated range.
bluff_discount_ratioOptional. Bluff discount (0..1).
hero_position / hero_handOptional card blocking: hero_position ("oop"/"ip") + hero_hand (e.g. "AsKs") removes every combo containing one of Hero's cards from the villain's updated range.
partner_handsOptional array of 4-char combos (e.g. ["Ac9c"]), requires hero_position. Removes every combo containing one of those cards from the villain's range_*_new_raw. Input only.

Call example

curl -s https://pokerai.bet/v1/gto/turn/projected-range -H "Authorization: Bearer $POKERAI_API_KEY" \
  -H "Content-Type: application/json" -d @turn_projected_range_request.json

Response (200, all fields listed, long strings truncated)

{"bluff_combos_ratio": 0.33, "bluff_discount_ratio": 1, "board": ["8d", "4h", "8s", "Qc"], "hand_bottom_ranks_ip": "TcTs:2943,TdTh:2943,TdTs:2943,9h9s:3020,…", "hand_bottom_ranks_oop": "AhKd:4646,AhKc:4646,AcKh:4646,JsTs:4746,…", "hand_ranks_ip": "Ac8c:2007,Ah8h:2007,KcKs:2645,KhKs:2645,…", "hand_ranks_oop": "8c8h:85,Ah8h:2007,Ac8c:2007,9c8c:2029,…", "node_id": "root/CHECK/BET 6.000000/CALL", "path_length": 3, "range_ip_new": "99:0.603064,A8s:0.5,AKs:0.70197,AQs:0.453337,…", "range_ip_new_raw": "9c9d:0.71487,9c9h:0.394271,9c9s:0.71487,…", "range_ip_new_raw_before_normalization": "9c9d:0.714699,9c9h:0.394176,9c9s:0.714699,…", "range_oop_new": "88:0.131148,98s:0.0902494,A8s:0.0344418,…", "range_oop_new_raw": "8c8h:0.777777,9c8c:0.185048,9h8h:0.17177,…", "range_oop_new_raw_before_normalization": "8c8h:0.418968,9c8c:0.0996806,9h8h:0.0925282,…"}
fielddescription
range_oop_new / range_ip_newThe normalized entering-river range (class notation), used directly as the river solve's oop_range / ip_range.
range_*_raw / range_*_raw_before_normalizationThe un-normalized / pre-discount intermediate values (combo notation).
hand_ranks_* / hand_bottom_ranks_*Hand strength ranking / bottom-of-range ranking (combo:rank, smaller is stronger).
node_id / board / path_lengthThe echoed action line, board (4 cards), and number of action-line steps.
bluff_discount_ratio / bluff_combos_ratioThe bluff discount parameters actually used this time.

Note: free, so the response has no quota field; and no pot_type (that is flop-only).

Possible errors / states

HTTPerror / statetrigger / how to fix
200{ "spot_status": "computing" }The solve has not converged yet — keep polling /v1/gto/solver/tree until queryable.
400missing_solve / missing_node_idMissing the solve handle or node_id.
410expiredThe solve expired (TTL) — reschedule via /v1/gto/solver.
502no_solution etc.Upstream solve error.

Download: request.jsonresponse.json

Node EVs solver

POST https://pokerai.bet/v1/gto/evs free

Full parameter / response schema, try it live → interactive reference.

Per-hand, per-action expected values at one node of a completed solve. Give the solve handle solve (from /v1/gto/solver) + a node_id (from /v1/gto/solver/tree); optional hand filters to one hand. Free (the solve was already charged). Poll /v1/gto/solver/tree until spot_status = queryable first.

Request body

{
  "solve": "eyJ0Ijoic2x2X3h4eXoi...(handle from /v1/gto/solver)",
  "node_id": "root",
  "hand": "2c2d"
}
fielddescription
solveRequired. The solve handle from /v1/gto/solver.
node_idRequired. A node from /v1/gto/solver/tree (solver notation, e.g. "root", "root/CHECK/BET 6.000000").
handOptional. Filter to one hand's EVs (e.g. "2c2d"); omit for all hands.

Response (200, each hand's EV array aligned with actions)

{"node_id": "root", "task_id": "slv_srp_…", "player": 1, "round": "FLOP", "actions": ["CHECK", "BET 4.000000", "BET 97.000000"], "evs": {"2c2d": [-0.826359, -0.792223, -1.643411], "2c2h": […], …}}
fielddescription
actionsThe node's actions in order; each hand's EV array aligns with this.
evsPer hand → array of each action's EV (bb). With hand given, evs is a single array for that hand.
player / round / node_id / task_idWhich player acts, the street, and the echoed node / solve id.

Note: free, so no quota field. Returns { "spot_status": "computing" } if the solve has not converged (poll /v1/gto/solver/tree).

Play a hand (full flow)

Stringing the endpoints together: one SRP hand (UTG opens, BTN calls), Hero = UTG. The curls below omit the auth header (same as Quickstart).

1) Preflop — should we open

POST /v1/gto/preflop
{ "hole_cards": "AdKd", "positions": { "hero": "UTG" },
  "preflop_actions": [ { "position": "SB", "action": "small blind", "amount": 0.5 }, { "position": "BB", "action": "big blind", "amount": 1 } ] }

Just the SB + BB posts (no raises) = Hero is the first to act (an opening spot). Returns the opening frequency and sizing.

2) Flop — flop strategy

Flop 2c2h2s. The flop is a decision tree, two steps: first fetch the tree, then use Hero's node token to fetch the strategy.

// 1) fetch the tree (no hole_cards needed)
POST /v1/gto/flop/tree
{ "board": "2c2h2s", "pot_type": "SRP",
  "positions": { "hero": "UTG", "raiser": "UTG", "caller": "BTN" } }
// → in nodes[], root (is_hero:true) carries a token

// 2) use root's token to fetch Hero strategy
POST /v1/gto/flop/node
{ "node": "<token of root>", "hole_cards": "AdKd" }

Hero's first decision = root; facing a bet / acting a second time → pick the corresponding is_hero:true node. See Flop decision tree.

3) Turn — real-time solve, range derived from the flop

The turn (e.g. 9d) needs a real-time solve, which requires both players' ranges entering the turn. Get them with the flop decision tree + range conversion:

  1. /v1/gto/flop/tree for the starting oop_range / ip_range and decision nodes.
  2. Along a flop action line (e.g. root/CHECK/BET_8/CALL), call /v1/gto/flop/node per node (without hole_cards) to fetch range_strategy and assemble solver_results — see the assemble_solver_results.py script in the range conversion section.
  3. /v1/gto/range updates along that line → range_oop_new / range_ip_new are the ranges entering the turn.
  4. Feed them to the solver (board with 4 cards), poll for the tree until queryable, then fetch the node:
    POST /v1/gto/solver
    { "board": "2c2h2s9d", "oop_range": "<OOP range entering turn>", "ip_range": "<IP range entering turn>",
      "pot": <turn pot>, "effective_stack": <turn effective stack>, "hero": "OOP" }

Already have your own range/spot? At step 3 give oop_range / ip_range directly and skip the derivation.

4) River — real-time solve

The river (board with 5 cards) is the same as the turn: you can solve it standalone (give the range entering the river directly), or use range conversion to update the turn action line once more to get the range entering the river and then feed it to the solver.

Poker engine / semantics API

Wraps pokerkit-plus (a superset of pokerkit 0.7.3): board/hand semantics, ranges & equity, eval/equity/ICM/notation, and game simulation. Reuses the same API key & quota — cheap endpoints charge the general bucket, Monte-Carlo endpoints the solve bucket. Base path /v1/pokerkit/*; card inputs are unseparated strings (AsKsQs), enums return {name,value}. Full per-endpoint params/schema & try-it-liveinteractive reference; machine-readable spec → /v1/pokerkit/openapi.json (auto-synced with the code).

Board / hand semantics charges 1 general

Board (Hero-independent): /texture (wetness/connectivity/draws available), /nuts (nuts + tying combos), /category-combos, /board-report (texture+nuts). Hero: /hand-tier (made-hand tier), /draws, /outs, /blockers, /hand-report (texture+tier+draws+outs in one call).

Request fields (shared in this group)

fielddescription
boardRequired. 3/4/5 community cards, unseparated, e.g. "AsKsQs".
holeRequired for Hero endpoints (hand-tier / draws / outs / blockers / hand-report). 2 hole cards, e.g. "JhTh"; board endpoints (texture / nuts / category-combos / board-report) omit it.
hand_typeOptional, default StandardHighHand (v1 only; other types return 400).
deadOptional. Dead / removed cards (accepted by all endpoints in this group).

Texture charges 1 general

POST https://pokerai.bet/v1/pokerkit/texture — Board structure: wetness/connectivity/rank band/draw availability/suit shape. Full params / try it live →

{"board": "AsKsQs"}

{"result": {"cards": ["As", "Ks", "Qs"], "wetness": {"name": "WET", "value": "Wet"}, "connectivity": {"name": "HIGH", "value": "High"}, "rank_band": {"name": "HIGH", "value": "High"}, "straight_draw": {"name": "OPEN_ENDED", "value": "Open-ended"}, "flush_draw": {"name": "LIVE", "value": "Live"}, "are_two_tone": false, "are_monotone": true, "are_rainbow": false}}

Nuts charges 1 general

POST https://pokerai.bet/v1/pokerkit/nuts — Strongest makeable hand + all tying two-card combos (with is_royal / board_is_nuts). Full params / try it live →

{"board": "AsKsQs"}

{"result": {"hand": "TsJsKsQsAs", "combos": [{"cards": ["Ts", "Js"], "hand": "TsJsKsQsAs", "as_frozenset": ["Js", "Ts"]}], "candidate_count": 1176, "board_is_nuts": false, "is_royal": true, "label": {"name": "STRAIGHT_FLUSH", "value": "Straight flush"}}}

Category combos charges 1 general

POST https://pokerai.bet/v1/pokerkit/category-combos — Every live two-card combo grouped by made category. Full params / try it live →

{"board": "AsKsQs"}

{"result": {"by_category": {"HIGH_CARD": [{"cards": ["2c", "3c"], "hand": "2c3cKsQsAs", "as_frozenset": ["2c", "3c"]}, {"cards": ["2c", "3d"], "hand": "2c3dKsQsAs", "as_frozenset": ["2c", "3d"]}], …}}}

Board report charges 1 general

POST https://pokerai.bet/v1/pokerkit/board-report — texture + nuts in one call (board overview). Full params / try it live →

{"board": "AsKsQs"}

{"result": {"texture": {"cards": ["As", "Ks", "Qs"], "wetness": {"name": "WET", "value": "Wet"}, "connectivity": {"name": "HIGH", "value": "High"}, "rank_band": {"name": "HIGH", "value": "High"}, "straight_draw": {"name": "OPEN_ENDED", "value": "Open-ended"}, "flush_draw": {"name": "LIVE", "value": "Live"}, "are_two_tone": false, "are_monotone": true, "are_rainbow": false}, "nuts": {"hand": "TsJsKsQsAs", "combos": [{"cards": ["Ts", "Js"], "hand": "TsJsKsQsAs", "as_frozenset": ["Js", "Ts"]}], "candidate_count": 1176, "board_is_nuts": false, "is_royal": true, "label": {"name": "STRAIGHT_FLUSH", "value": "Straight flush"}}}}

Hand tier charges 1 general

POST https://pokerai.bet/v1/pokerkit/hand-tier — Hero made-hand tier (pair / two-pair / trips / kicker tiers, is_nut). Full params / try it live →

{"hole": "JhTh", "board": "AsKsQs"}

{"result": {"category": {"name": "STRAIGHT", "value": "Straight"}, "is_nut": false, "pair_tier": null, "two_pair_tier": null, "three_of_a_kind_tier": null, "kicker_tier": null, "nut_rank": {"name": "NON_NUT", "value": "Non-nut"}}}

Draws charges 1 general

POST https://pokerai.bet/v1/pokerkit/draws — Hero draws (straight / flush draw, nut rank). Full params / try it live →

{"hole": "Ah5h", "board": "Kh7h2c"}

{"result": {"straight_draw": null, "flush_draw": {"name": "LIVE", "value": "Live"}, "nut_rank": {"name": "NUT", "value": "Nut"}}}

Outs charges 1 general

POST https://pokerai.bet/v1/pokerkit/outs — Hero outs that upgrade the made category, grouped + count. Full params / try it live →

{"hole": "Ah5h", "board": "Kh7h2c"}

{"result": {"by_category": {"ONE_PAIR": ["2d", "2s", "5c", "5d", "5s", "7c", "7d", "7s", "Kc", "Kd", "Ks", "Ac", "Ad", "As"], "FLUSH": ["2h", "3h", "4h", "6h", "8h", "9h", "Th", "Jh", "Qh"]}, "count": 23}}

Blockers charges 1 general

POST https://pokerai.bet/v1/pokerkit/blockers — how many nut combos Hero blocks (blocker cards / fraction). Full params / try it live →

{"hole": "AhAd", "board": "AsKsQs"}

{"result": {"nut_combos_total": 1, "nut_combos_blocked": 0, "blocker_cards": [], "block_fraction": 0.0, "blocks_nuts": false}}

Hand report charges 1 general

POST https://pokerai.bet/v1/pokerkit/hand-report — texture + tier + draws + outs in one call (Hero overview, flagship). Full params / try it live →

{"hole": "JhTh", "board": "AsKsQs"}

{"result": {"texture": {"cards": ["As", "Ks", "Qs"], "wetness": {"name": "WET", "value": "Wet"}, "connectivity": {"name": "HIGH", "value": "High"}, "rank_band": {"name": "HIGH", "value": "High"}, "straight_draw": {"name": "OPEN_ENDED", "value": "Open-ended"}, "flush_draw": {"name": "LIVE", "value": "Live"}, "are_two_tone": false, "are_monotone": true, "are_rainbow": false}, "tier": {"category": {"name": "STRAIGHT", "value": "Straight"}, "is_nut": false, "pair_tier": null, "two_pair_tier": null, "three_of_a_kind_tier": null, "kicker_tier": null, "nut_rank": {"name": "NON_NUT", "value": "Non-nut"}}, "draws": {"straight_draw": null, "flush_draw": null, "nut_rank": null}, "outs": {"by_category": {}, "count": 0}}}

Ranges / equity charges 1 general

/range/expand (notation → concrete combos), /range/value (value range by made-category floor; aggression ∈ NO_BET/SINGLE_BET/RAISED), /range/nut-advantage (exact nut share, no sampling).

Request fields (shared in this group)

fielddescription
notation(expand) range-notation array, e.g. ["AA","KQs","QQ+"].
hero / villain(nut-advantage) one range-notation array each.
boardCommunity cards (required for value / nut-advantage; expand omits it).
aggression(value) NO_BET / SINGLE_BET / RAISED sets the category floor; or floor to set it explicitly.

Range expand charges 1 general

POST https://pokerai.bet/v1/pokerkit/range/expand — Expand range notation into concrete two-card combos. Full params / try it live →

{"notation": ["AA", "KQs"]}

{"result": [["Ac", "Ad"], ["Ac", "Ah"], ["Ac", "As"], ["Ad", "Ah"], ["Ad", "As"], ["Ah", "As"], ["Kc", "Qc"], ["Kd", "Qd"], ["Kh", "Qh"], ["Ks", "Qs"]]}

Range value charges 1 general

POST https://pokerai.bet/v1/pokerkit/range/value — Build a value range by made-category floor (aggression). Full params / try it live →

{"board": "AsKsQs", "aggression": "SINGLE_BET"}

{"result": [["2s", "3s"], ["2s", "4s"], ["2s", "5s"], ["2s", "6s"], ["2s", "7s"], ["2s", "8s"], …]}

Range nut-advantage charges 1 general

POST https://pokerai.bet/v1/pokerkit/range/nut-advantage — Exact nut-share split by combo count (no sampling, deterministic). Full params / try it live →

{"hero": ["AA", "KK"], "villain": ["QQ", "JJ"], "board": "AsKsQs"}

{"result": {"hero_share": 0.5, "villain_share": 0.5, "basis": {"name": "NUT_SHARE", "value": "Nut share"}}}

Eval · equity · ICM · notation

charges 1 general /eval/hand, /eval/compare (rank + ties), /icm (deterministic), /notation/parse (.phh → structured). charges 1 solve Monte-Carlo: /equity, /hand-strength, /range/equity-advantage — with sample_count (capped) and seed (reproducible, ~2s@10k).

Request fields (shared in this group)

Monte-Carlo endpoints (equity / hand-strength / range/equity-advantage) charge the solve bucket; the rest charge general.

fielddescription
hole / holdings / boardEval inputs: single hole + board (eval/hand), or multi-hand holdings (2+) + board (eval/compare).
ranges / hole_range / hero / villainRange notation (equity / hand-strength / range/equity-advantage).
sample_count / seedMonte-Carlo: sample count (capped; over the cap clamps) / RNG seed (reproducible).
payouts / chips(icm) payout structure / per-player chips.
text(notation/parse) a .phh hand-history string.

Eval hand charges 1 general

POST https://pokerai.bet/v1/pokerkit/eval/hand — Evaluate hole + board into a made hand (5 cards + category label). Full params / try it live →

{"hole": "JhTh", "board": "AsKsQs"}

{"result": {"hand": "JhThAsKsQs", "label": {"name": "STRAIGHT", "value": "Straight"}}}

Eval compare charges 1 general

POST https://pokerai.bet/v1/pokerkit/eval/compare — Rank 2+ holdings on a board (with ties). Full params / try it live →

{"holdings": ["AcAd", "KsKh", "JhTh"], "board": "AsKsQs"}

{"result": [{"index": 2, "hole": "JhTh", "hand": "JhThAsKsQs", "rank": 1, "label": {"name": "STRAIGHT", "value": "Straight"}}, {"index": 0, "hole": "AcAd", "hand": "AcAdAsKsQs", "rank": 2, "label": {"name": "THREE_OF_A_KIND", "value": "Three of a kind"}}, {"index": 1, "hole": "KsKh", "hand": "KsKhAsKsQs", "rank": 3, "label": {"name": "THREE_OF_A_KIND", "value": "Three of a kind"}}]}

Equity charges 1 solve

POST https://pokerai.bet/v1/pokerkit/equity — Equity of multiple ranges on a board (Monte-Carlo). Full params / try it live →

{"ranges": [["AA"], ["KK"]], "board": "", "sample_count": 2000, "seed": 7}

{"result": {"equities": [0.8235, 0.1765], "sample_count": 2000}}

Hand strength charges 1 solve

POST https://pokerai.bet/v1/pokerkit/hand-strength — Hero win fraction vs N players (Monte-Carlo). Full params / try it live →

{"hole_range": ["AhKh"], "board": "Qh7c2d", "player_count": 3, "sample_count": 2000, "seed": 7}

{"result": {"hand_strength": 0.3945, "player_count": 3, "sample_count": 2000}}

ICM charges 1 general

POST https://pokerai.bet/v1/pokerkit/icm — ICM equity split from chips / payouts (deterministic). Full params / try it live →

{"payouts": [50, 30, 20], "chips": [5000, 3000, 2000]}

{"result": {"icm": [38.392857142857146, 32.75, 28.857142857142854]}}

Range equity-advantage charges 1 solve

POST https://pokerai.bet/v1/pokerkit/range/equity-advantage — Equity-share split between two ranges (Monte-Carlo). Full params / try it live →

{"hero": ["AA", "KK"], "villain": ["QQ", "JJ"], "board": "AsKsQs", "sample_count": 2000, "seed": 7}

{"result": {"hero_share": 0.80325, "villain_share": 0.19675, "basis": {"name": "EQUITY", "value": "Equity"}, "sample_count": 2000}}

Notation parse charges 1 general

POST https://pokerai.bet/v1/pokerkit/notation/parse — Parse a .phh hand history into structured config (variant / blinds / stacks / actions…). Full params / try it live →

{"text": "variant = \"NT\"\nante_trimming_status = true\nantes = [0, 0]\nblinds_or_straddles = [1, 2]\nmin_bet = 2\nstarting_stacks = [200, 200]\nactions = [\"d dh p1 AhKh\", \"d dh p2 QsQd\", \"p1 cbr 6\", \"p2 cc\", \"d db Qh7c2d\"]\n"}

{"result": {"variant": "NT", "ante_trimming_status": true, "antes": [0, 0], "blinds_or_straddles": [1, 2], "bring_in": null, "small_bet": null, "big_bet": null, "min_bet": 2, "starting_stacks": [200, 200], "actions": ["d dh p1 AhKh", "d dh p2 QsQd", "p1 cbr 6", "p2 cc", "d db Qh7c2d"], "automations": [{"name": "ANTE_POSTING", "value": "Ante posting"}, …], "author": null, "event": null, "day": null, "month": null, "year": null, "hand": null, "currency": null}}

Game simulation charges 1 general

Full-information, stateless replay: the client carries the action list, the server rebuilds the state with pokerkit (zero server state). /games/state (current-spot snapshot), /games/step (apply next_action; expected_action_count is an optimistic-concurrency token, mismatch → 409), /notation/replay (config or .phh text → per-step snapshots), /cards/normalize (validate/normalize a card string).

Request fields (shared in this group)

fielddescription
variantRequired. Variant code, e.g. "NT" (no-limit hold'em); full list at /v1/pokerkit/meta.
antes / starting_stacksRequired. Per-player antes / starting stacks (integer arrays).
blinds_or_straddles / min_betBlinds / straddles; min_bet required for no-limit / pot-limit (fixed-limit / stud use small_bet / big_bet / bring_in).
actionsAction list so far (pokerkit notation: d dh p1 AhKh deal hole, p2 cbr 6 raise to 6, p1 cc check/call, p1 f fold).
next_action / expected_action_count(step) action to apply / concurrency token (= length of the list you extend; mismatch → 409).
text / index(replay) a .phh text instead; index returns just that step. cards/normalize takes only cards (a card string).

Games state charges 1 general

POST https://pokerai.bet/v1/pokerkit/games/state — Action list → current-spot snapshot (all hole cards shown). Full params / try it live →

{"variant": "NT", "antes": [0, 0], "blinds_or_straddles": [1, 2], "min_bet": 2, "starting_stacks": [200, 200], "actions": ["d dh p1 AhKh", "d dh p2 QsQd", "p2 cbr 6"]}

{"result": {"snapshot": {"terminal": false, "street_index": 0, "actor_index": 0, "pot": 8, "bets": [2, 6], "stacks": [198, 194], "board": [], "hole_cards": [{"player": 0, "cards": ["Ah", "Kh"]}, {"player": 1, "cards": ["Qs", "Qd"]}], "legal_actions": [{"action": "fold"}, {"action": "check_or_call", "amount": 4}, {"action": "complete_bet_or_raise_to", "min": 10, "max": 200}]}, "actions": ["d dh p1 AhKh", "d dh p2 QsQd", "p2 cbr 6"]}}

Games step charges 1 general

POST https://pokerai.bet/v1/pokerkit/games/step — Apply next_action → new snapshot (concurrency token; 409 on mismatch). Full params / try it live →

{"variant": "NT", "antes": [0, 0], "blinds_or_straddles": [1, 2], "min_bet": 2, "starting_stacks": [200, 200], "actions": ["d dh p1 AhKh", "d dh p2 QsQd", "p2 cbr 6"], "next_action": "p1 cc", "expected_action_count": 3}

{"result": {"snapshot": {"terminal": false, "street_index": 1, "actor_index": null, "pot": 12, "bets": [0, 0], "stacks": [194, 194], "board": [], "hole_cards": [{"player": 0, "cards": ["Ah", "Kh"]}, {"player": 1, "cards": ["Qs", "Qd"]}], "legal_actions": [{"action": "deal_board"}]}, "actions": ["d dh p1 AhKh", "d dh p2 QsQd", "p2 cbr 6", "p1 cc"]}}

Notation replay charges 1 general

POST https://pokerai.bet/v1/pokerkit/notation/replay — config or .phh → per-step snapshots (optional index for a single step). Full params / try it live →

{"variant": "NT", "antes": [0, 0], "blinds_or_straddles": [1, 2], "min_bet": 2, "starting_stacks": [200, 200], "actions": ["d dh p1 AhKh", "d dh p2 QsQd", "p2 cbr 6", "p1 f"], "index": 2}

{"result": {"snapshot": {"terminal": false, "street_index": 0, "actor_index": 1, "pot": 3, "bets": [2, 1], "stacks": [198, 199], "board": [], "hole_cards": [{"player": 0, "cards": ["Ah", "Kh"]}, {"player": 1, "cards": ["Qs", "Qd"]}], "legal_actions": [{"action": "fold"}, {"action": "check_or_call", "amount": 1}, {"action": "complete_bet_or_raise_to", "min": 4, "max": 200}]}, "step_count": 5}}

Cards normalize charges 1 general

POST https://pokerai.bet/v1/pokerkit/cards/normalize — Validate / normalize a card string to standard 2-char cards. Full params / try it live →

{"cards": "Ah Ks Qs"}

{"result": ["Ah", "Ks", "Qs"]}

Also GET /v1/pokerkit/meta (versions / variant codes / hand types / enum vocabularies). Full parameter schema and try-it-live for every endpoint → interactive reference (browse both APIs from the top).

All example downloads

A summary of each endpoint's complete request/response (real data, no fields omitted) is in the "Download" of each section above, or: preflop · flop tree · flop node · solver · range · projected range · projected range response · assemble script.

Versioning & changelog

Versioning policy

Changelog

datechange
2026-07-04Added /v1/gto/evs (per-hand, per-action node EVs of a completed solve); /v1/gto/turn/projected-range now also supports partner_hands.
2026-07-04/v1/gto/flop/projected-range now honors bluff_discount_ratio / bluff_combos_ratio from the request, injects Hero's own hand into Hero's range, and adds partner_hands (block partner dead cards from the villain range).
2026-06-22Added the Poker engine / semantics API /v1/pokerkit/* (board/hand semantics, ranges/equity, eval/equity/ICM/notation, game simulation; same key & quota). See below.
2026-06-22Added /v1/gto/preflop/range (the whole 13×13 preflop range, 169 hands in one call).
2026-06-18Split flop: removed the single-query /v1/gto/flop, replaced by /v1/gto/flop/tree (fetch tree) + /v1/gto/flop/node (fetch node, free), aligned with the solver's tree/node.
2026-06-17Added /v1/gto/flop/projected-range (a convenience wrapper over /v1/gto/range: supply the full flop spot + action line, the server assembles the decision tree automatically and returns the turn range directly).
2026-06-17Unified board / hand format: input is an unseparated string, response board is always returned as an array.
2026-06-17Added the OpenAPI 3.0 spec; added range-notation / concepts / full-flow tutorial docs.
2026-06-17/v1/gto/solver now supports real-time flop solving (board=3); bet_sizes.flop can customize the flop bet sizes.
2026-06-17Fix: flop-query raise amount_bb was wrongly 0 (now returns the correct absolute BB).
2026-06-17Added range conversion /v1/gto/range; /flop/tree exposes the starting oop_range / ip_range; /flop and /solver/node return the full range strategy when hole_cards is not passed.
2026-06-16Added the turn / river real-time solver /v1/gto/solver family (separate solve quota); added the flop decision tree /v1/gto/flop/tree.
2026-06-15Removed the recommendation field from all responses — please choose actions yourself based on frequency.

pokerai.bet · GTO API v1