Presolved lookup
FASTPreflop and 5.8M+ flop solutions. Return a strategy in milliseconds with no task polling.
Start with preflopUse MelaSolver GPU through one self-serve API. Start with a millisecond presolved lookup, or submit a custom flop, turn, or river spot to the real-time solver pool.
Download the public collection, import it into Postman, and set the collection apiKey variable to your API key before sending a request. It uses Authorization: Bearer {{apiKey}} and the public https://pokerai.bet base URL; the included examples cover GTO preflop strategy and PokerKit board texture.
Postman collectionThe downloadable source contains no real key, Cookie, or private endpoint. For the complete public contract, see the API Reference and OpenAPI snapshot.
Both use the same API key and monthly quota.
Preflop and 5.8M+ flop solutions. Return a strategy in milliseconds with no task polling.
Start with preflopCustom flop, turn, and river trees. Submit once, poll by task ID, then retrieve the solved strategy.
Submit a solveInstall, authenticate, call. Nothing else to provision.
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}]}'
{
"hole_cards": "AhKh",
"situation": "RFI",
"strategy": [
{ "action": "raise", "frequency": 1, "amount_bb": 3, "sizing_pot": 0.8 }
],
"quota": { "used": 6, "limit": 100 }
}
Move from first call to production without hunting through one long page.
Every request must carry an API Key. Get a Key in 60 seconds:
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
Two kinds of monthly quota, metered separately and reset on the 1st of each month; current usage is in the console:
/v1/gto/range, and projected-range calls each charge 1./v1/gto/solver charges 1 each time it triggers a new solve; reusing a cached solve and fetching the tree/node are free (the response has a solve_quota field when charged, and lacks it when not charged).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:
| HTTP | error / meaning | retry? |
|---|---|---|
| 400 | Invalid input or missing field (see each endpoint's table for the specific error). | No, fix the input |
| 401 | missing_api_key / invalid_api_key: missing or invalid Key. | No, check the Key |
| 403 | invalid_node_token / invalid_solve: token/handle invalid or not yours. | No, re-fetch the tree / reschedule first |
| 404 | no_solution: no GTO data for this spot yet. | No, change the spot |
| 429 | quota_exceeded (general) / solve_quota_exceeded (solve): monthly quota used up. | No, resets on the 1st or upgrade your quota |
| 429 | status: busy: all solvers busy (only /v1/gto/solver), not charged. | Yes, back off and retry |
| 502 | no_result (reason: timeout / no_worker_available) / auth_unavailable / solver_unreachable: backend transiently unavailable. | Yes, back off and retry 2–3 times |
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.
Once you have a Key (see above), save it as POKERAI_API_KEY and copy this curl — the simplest successful call: Hero has an opening opportunity in UTG (RFI), and the preflop presolved solution returns in milliseconds. You can also copy the same starter snippet from the dashboard.
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.
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.
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}}
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.
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).
The overall mental model — read this once and the per-endpoint sections below will go more smoothly.
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).
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.
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
/flop/tree (charges 1) → /flop/node (free). The root node = Hero's first decision./solver scheduling returns a solve handle (charges 1) → /solver/tree polling + /solver/node (both free). The range is given only once at scheduling, not passed again.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.
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). When done querying, you may optionally call /solver/release to return the port to the pool immediately (otherwise it is reclaimed by TTL); after release, further queries on this solve handle return expired.
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.
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…).
| field | type | description |
|---|---|---|
hole_cards | string | Hero's 2 hole cards, e.g. "AdKd". |
positions.hero | string | Hero's position, one of SB BB UTG MP CO BTN (placed under positions). |
preflop_actions | array | The 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_version | string | Optional. 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. |
| field | type | description |
|---|---|---|
position | string | The position of this action, one of SB BB UTG MP CO BTN. |
action | string | ∈ "small blind" / "big blind" / "raise" / "call" / "fold" (note the blinds are two-word strings). |
amount | number | The 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. |
allin | boolean | Optional. 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.
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.
{"hole_cards": "AhKh", "situation": "Raise", "strategy": [{"action": "raise", "frequency": 1, "amount_bb": 9, "sizing_pot": 0.8}], "quota": {"used": 7, "limit": 100}}
| field | description |
|---|---|
hole_cards | The echoed hand. |
situation | The 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. |
quota | This month's general quota usage (used / limit). |
Preflop amount_bb (derived from the presolved flop pot, raised to):
| number of raises before Hero | spot | amount_bb |
|---|---|---|
| 0 | open | 3 |
| 1 | 3bet | 9 |
| 2 | 4bet | 25 |
| ≥3 | 5bet+ | all-in 100 (allin: true) |
preflop_versionSame 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_200NL | 6 max 100bb GG 200NL 3b/f 2.2x - 2.5x |
6max_RC_100bb_100NL | 6 max 100bb GG 100NL 3b/f |
6max_RC_40bb | 6 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"}
| HTTP | error | trigger / how to fix |
|---|---|---|
| 400 | invalid_hole_cards | hole_cards is not 2 cards (e.g. "AdKd"). |
| 400 | unsupported_table_size / invalid_positions / invalid_actions | Table type (currently only 6max), hero/positions, or preflop_actions is invalid. |
| 400 | unsupported_preflop_version | preflop_version is not in the allowed set (6max / 6max_RC_100bb_200NL / 6max_RC_100bb_100NL / 6max_RC_40bb). |
| 404 | no_solution | No presolved solution for this preflop spot; change the spot. |
Download: request.jsonresponse.json
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.
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 (tree → node).
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.
board | The 3 flop cards, e.g. "2c2h2s". |
pot_type | "SRP" single-raised / "3BET" / "4BET" / "LIMP" limped. |
positions | The position of each role (SB BB UTG MP CO BTN), required per pot_type as below. |
flop_version | Optional. 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_type | required positions | Hero value |
|---|---|---|
SRP | hero, raiser, caller | raiser or caller |
3BET / 4BET | hero, raiser, three_bettor | raiser or three_bettor |
LIMP | hero, limper | one 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}}
| field | description |
|---|---|
oop_range / ip_range | The 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_count | Pot, effective stack (BB), and total number of decision nodes. |
quota | This month's general quota usage (used / limit). |
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}
| field | description |
|---|---|
is_hero | Whether this node is Hero's decision. |
strategy[] | Hero node (with hole_cards): the mixed strategy for this hand. action ∈ check / 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_strategy | Opponent 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).
| HTTP | error | trigger / how to fix |
|---|---|---|
| 400 | 1) invalid_board / invalid_positions; 2) missing_node / invalid_hole_cards | 1) board is not 3 cards, or hero/positions invalid; 2) missing node or hole_cards is not 2 cards. |
| 403 | invalid_node_token | (step 2) node token invalid or not yours — call /v1/gto/flop/tree to fetch the tree first. |
| 404 | no_solution | No presolved solution for this spot/board. |
Download: tree_request.jsontree_response.jsonnode_request.jsonnode_response.json
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).
# 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","bet_sizes":{"flop":[33,75],"turn":[67],"river":[75]},"raise_sizes":{"flop":[50],"turn":[80],"river":[125]},"donk_sizes":{"turn":[55],"river":[90]},"raise_limit":3}'
# 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..."}'
board | 3=flop / 4=turn / 5=river, e.g. "2c2h2s9d" (turn). |
oop_range / ip_range | Required. The two players' ranges entering this street, weighted combo strings, e.g. "AsKs:1,QQ:0.75,...". |
pot / effective_stack | Required. The pot and behind effective stack (BB) entering this street; they determine the bet sizes and must be real. |
hero | Required: "OOP" or "IP". |
bet_sizes | Optional: override opening bet sizes per street, e.g. {"flop":[33,75],"turn":[67],"river":[75]} (% of pot); if flop is omitted it defaults to 50%. |
raise_sizes | Optional: override raise sizes per street, e.g. {"flop":[50],"turn":[80],"river":[125]} (% of pot); omitted streets reuse bet_sizes or defaults. |
donk_sizes | Optional: override OOP donk-lead sizes, e.g. {"turn":[55],"river":[90]} (% of pot); defaults are turn 67% and river 100%. |
raise_limit | Optional: tree-wide raise cap, 1–4; defaults to 3 for flop/turn solves and 4 for river-only solves. |
// Request (flop, with custom bet / raise / donk sizes)
{"board": "2c2h2s", "oop_range": "AA,KK,QQ,AKs", "ip_range": "JJ,TT,AQs,KQs", "pot": 7.5, "effective_stack": 97, "hero": "OOP", "bet_sizes": {"flop": [33,75], "turn": [67], "river": [75]}, "raise_sizes": {"flop": [50], "turn": [80], "river": [125]}, "donk_sizes": {"turn": [55], "river": [90]}, "raise_limit": 3}
// Response (first trigger, charges 1 solve)
{"status": "computing", "solve": "eyJ0Ijoic2x2XzJmMjk2MWU3Y2Y0ODU3OGUiLCJ1IjoiYjk4Y2Y5NzAtZTVjMS00Njk5LTg4ODQtOGQzYzcwMGIyNGJlIiwidHMiOjE3ODIxMjkxMTUyMDJ9.rtmeNcZoBNWJRhkAGkTSSR58ZafpAh35mi510bgPU6c", "solve_quota": {"used": 1, "limit": 100}}
| field / case | description |
|---|---|
solve | The solve handle, used by the subsequent /tree and /node; the range is given only once in this step. |
status = computing | A new solve was triggered, charges 1 solve quota (see solve_quota). |
status = queryable | A cached solve already exists for this spot, not charged (no solve_quota field), poll /tree directly. |
429 status = busy | All 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/raise_sizes/donk_sizes/raise_limit) 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.
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…"}, …]}
| field | description |
|---|---|
spot_status | Solve 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_count | Street, pot, effective stack, and total number of decision nodes. |
solve_seconds | Wall-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. |
Full parameter / response schema, and try it live → interactive reference.
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}]}
| field | description |
|---|---|
is_hero | Whether 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_strategy | Opponent node (or no hole_cards): in range_strategy each hand's frequencies align to the actions order; also includes range_hand_count. |
Full parameter / response schema, and try it live → interactive reference.
When you are done, you may optionally call /v1/gto/solver/release to free the port immediately (otherwise the system reclaims it 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). Full /solver/release parameter / response schema → interactive reference.
| HTTP | error | trigger / how to fix |
|---|---|---|
| 400 | invalid_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. |
| 400 | missing_solve / missing_node | (fetch tree/node) missing the solve handle or node token. |
| 403 | invalid_solve / invalid_node_token | Handle/token invalid or not yours — reschedule / re-fetch the tree. |
| 429 | status: busy | All solvers are busy, not charged, back off and retry. |
| 502 | solve_failed | Triggering the solve failed; retry later. |
| 503 | upstream_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
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",
"bet_sizes": {"turn": [67], "river": [75]},
"raise_sizes": {"turn": [80], "river": [125]},
"donk_sizes": {"turn": [55], "river": [90]},
"raise_limit": 3}).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"])
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.
{
"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.)
# 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
{"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}}
| field | description |
|---|---|
range_oop_new / range_ip_new | The 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_raw | The intermediate value after bluff discount, not normalized (combo notation); use as needed. |
range_oop_new_raw_before_normalization / range_ip_new_raw_before_normalization | The raw value narrowed only along the action line, without bluff discount or normalization. |
hand_ranks_oop / hand_ranks_ip | Hand strength ranking (combo:rank, smaller value is stronger). |
hand_bottom_ranks_oop / hand_bottom_ranks_ip | Bottom-of-range ranking. |
node_id / board / path_length | The echoed action line, board, and number of action-line steps. |
bluff_discount_ratio / bluff_combos_ratio | The bluff discount parameters actually used this time. |
quota | This month's general quota usage (used / limit). |
| HTTP | error | trigger / how to fix |
|---|---|---|
| 400 | missing_field | Missing one of range_oop / range_ip / solver_results / node_id. |
| 400 | bad_request | The 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)
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.
{
"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"]
}
| field | description |
|---|---|
board | Required. The flop (unseparated string, same as other endpoints), e.g. "2c2h2s". |
pot_type | Required. The pot type, e.g. "SRP". |
positions | Required. { hero, raiser, caller } (same as flop); 3bet/limp spots may carry three_bettor / limper. |
node_id | Optional, 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". |
normalize | Optional, default true. Whether to normalize the updated range. |
bluff_discount_ratio / bluff_combos_ratio | Optional, 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_hand | Optional 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_hands | Optional 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_version | Optional. 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. |
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
{"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}}
| field | description |
|---|---|
range_oop_new / range_ip_new | The 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_raw | The intermediate value after bluff discount, not normalized (combo notation); use as needed. |
range_oop_new_raw_before_normalization / range_ip_new_raw_before_normalization | The raw value narrowed only along the action line, without bluff discount or normalization. |
hand_ranks_oop / hand_ranks_ip | Hand strength ranking (combo:rank, smaller value is stronger). |
hand_bottom_ranks_oop / hand_bottom_ranks_ip | Bottom-of-range ranking. |
node_id / board / pot_type / path_length | The echoed action line, board, pot type, and number of action-line steps. |
bluff_discount_ratio / bluff_combos_ratio | The bluff discount parameters actually used this time. |
quota | This month's general quota usage (used / limit). |
| HTTP | error | trigger / how to fix |
|---|---|---|
| 400 | invalid_board / invalid_positions | board is not 3 cards, or positions.hero is invalid. |
| 404 | no_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
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).
{
"solve": "eyJ…",
"node_id": "root/CHECK/BET 6.000000/CALL",
"normalize": true,
"bluff_discount_ratio": 0.8,
"hero_position": "oop",
"hero_hand": "AsKs"
}
| field | description |
|---|---|
solve | Required. The solve handle returned by /v1/gto/solver (a turn solve). |
node_id | Required. 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). |
normalize | Optional, default true. Whether to normalize the updated range. |
bluff_discount_ratio | Optional. Bluff discount (0..1). |
hero_position / hero_hand | Optional 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_hands | Optional 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. |
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
{"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,…"}
| field | description |
|---|---|
range_oop_new / range_ip_new | The normalized entering-river range (class notation), used directly as the river solve's oop_range / ip_range. |
range_*_raw / range_*_raw_before_normalization | The 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_length | The echoed action line, board (4 cards), and number of action-line steps. |
bluff_discount_ratio / bluff_combos_ratio | The bluff discount parameters actually used this time. |
Note: free, so the response has no quota field; and no pot_type (that is flop-only).
| HTTP | error / state | trigger / how to fix |
|---|---|---|
| 200 | { "spot_status": "computing" } | The solve has not converged yet — keep polling /v1/gto/solver/tree until queryable. |
| 400 | missing_solve / missing_node_id | Missing the solve handle or node_id. |
| 410 | expired | The solve expired (TTL) — reschedule via /v1/gto/solver. |
| 502 | no_solution etc. | Upstream solve error. |
Download: request.jsonresponse.json
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.
{
"solve": "eyJ0Ijoic2x2X3h4eXoi...(handle from /v1/gto/solver)",
"node_id": "root",
"hand": "2c2d"
}
| field | description |
|---|---|
solve | Required. The solve handle from /v1/gto/solver. |
node_id | Required. A node from /v1/gto/solver/tree (solver notation, e.g. "root", "root/CHECK/BET 6.000000"). |
hand | Optional. Filter to one hand's EVs (e.g. "2c2d"); omit for all hands. |
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": […], …}}
| field | description |
|---|---|
actions | The node's actions in order; each hand's EV array aligns with this. |
evs | Per hand → array of each action's EV (bb). With hand given, evs is a single array for that hand. |
player / round / node_id / task_id | Which 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).
Stringing the endpoints together: one SRP hand (UTG opens, BTN calls), Hero = UTG. The curls below omit the auth header (same as Quickstart).
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.
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.
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:
/v1/gto/flop/tree for the starting oop_range / ip_range and decision nodes.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./v1/gto/range updates along that line → range_oop_new / range_ip_new are the ranges entering the turn.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.
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.
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-live → interactive reference; machine-readable spec → /openapi.en.json (combined GTO + pokerkit snapshot).
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).
| field | description |
|---|---|
board | Required. 3/4/5 community cards, unseparated, e.g. "AsKsQs". |
hole | Required 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_type | Optional, default StandardHighHand (v1 only; other types return 400). |
dead | Optional. Dead / removed cards (accepted by all endpoints in this group). |
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}}
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"}}}
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"]}], …}}}
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"}}}}
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"}}}
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"}}}
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}}
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}}
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}}}
/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).
| field | description |
|---|---|
notation | (expand) range-notation array, e.g. ["AA","KQs","QQ+"]. |
hero / villain | (nut-advantage) one range-notation array each. |
board | Community 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. |
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"]]}
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"], …]}
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"}}}
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).
Monte-Carlo endpoints (equity / hand-strength / range/equity-advantage) charge the solve bucket; the rest charge general.
| field | description |
|---|---|
hole / holdings / board | Eval inputs: single hole + board (eval/hand), or multi-hand holdings (2+) + board (eval/compare). |
ranges / hole_range / hero / villain | Range notation (equity / hand-strength / range/equity-advantage). |
sample_count / seed | Monte-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. |
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"}}}
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"}}]}
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}}
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}}
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]}}
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}}
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}}
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).
| field | description |
|---|---|
variant | Required. Variant code, e.g. "NT" (no-limit hold'em); full list at /v1/pokerkit/meta. |
antes / starting_stacks | Required. Per-player antes / starting stacks (integer arrays). |
blinds_or_straddles / min_bet | Blinds / straddles; min_bet required for no-limit / pot-limit (fixed-limit / stud use small_bet / big_bet / bring_in). |
actions | Action 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). |
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"]}}
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"]}}
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}}
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).
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.
/v1 in the path is the major version. Breaking changes (removing/changing fields, changing semantics) go to a new major version /v2, and /v1 stays available./v1 without separate notice — please parse by "ignoring unknown fields" and do not strictly allowlist-validate response fields.| date | change |
|---|---|
| 2026-07-22 | /v1/gto/solver now accepts independent raise_sizes, donk_sizes, and raise_limit fields in addition to bet_sizes. |
| 2026-07-12 | Added POST /v1/gto/solver/release — release a solve's pool port early so it returns to the pool immediately instead of waiting out the cache TTL (optional, free; call after your last /solver/tree / /solver/node for that solve). |
| 2026-07-04 | Added /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-22 | Added 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-22 | Added /v1/gto/preflop/range (the whole 13×13 preflop range, 169 hands in one call). |
| 2026-06-18 | Split 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-17 | Added /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-17 | Unified board / hand format: input is an unseparated string, response board is always returned as an array. |
| 2026-06-17 | Added 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-17 | Fix: flop-query raise amount_bb was wrongly 0 (now returns the correct absolute BB). |
| 2026-06-17 | Added 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-16 | Added the turn / river real-time solver /v1/gto/solver family (separate solve quota); added the flop decision tree /v1/gto/flop/tree. |
| 2026-06-15 | Removed the recommendation field from all responses — please choose actions yourself based on frequency. |
pokerai.bet · GTO API v1