B2C Features
The B2C feature set of the strategy API POST /lookup endpoint is composed of three building blocks:
- Request Profile — several simple parameters control how the result is post-processed and presented;
request_profilejoins them into a single field that applies a predefined configuration. Each parameter still works individually, and explicit values always win over the profile. - Reduced Actions Tiers — shrink the player-facing menu to 3, 4, or up to 6 actions. Raise sizes are grouped into representatives that carry the group's combined probability, and the complete action menu is still returned alongside in
strategy.actions, so no information is discarded. - EV Mistake Score — score the action the player actually took: it is matched to the closest entry of the reduced menu, and
ev_mistake_scorereports how much EV that choice gave up versus the best entry of that same menu —0means the player made the best available play.
Examples on this page use a No Limit Hold'em cash game. Everything works identically on every supported game type.
Endpoint
Endpoint: POST /lookup
curl --location '<glue-URL>/lookup' \
--header 'Content-Type: application/json' \
--header 'Authorization: <Auth-TOKEN>' \
--data '{ ... }'The actual <glue-URL> for your environment (dev / stg / prod) and the <Auth-TOKEN> are provided separately.
Request Profile
{
"request_id": "unique-request-id",
"hand": { <Hand-object> },
"request_profile": "<client_profile>"
}| Field | Type | Required | Description |
|---|---|---|---|
request_profile | string | No | The name of your profile. The value is provided during onboarding, together with your URL and token. |
That is a complete request — with a profile, request_id, hand and request_profile are all you need. The name match is flexible (letter case and -/_ separators are ignored), but we recommend sending exactly the spelling you were given.
What the Profile Sets
With request_profile present, the request is processed as if you had sent:
{
"request_id": "unique-request-id",
"hand": { <Hand-object> },
"request_profile": "<client_profile>",
"strategy_min_threshold": 0.05,
"disable_bet_randomness": true,
"reduced_actions_tier": 3,
"show_labels": true,
"mode": "bot",
"protocol_ver": "v2.1"
}The values below are the standard set. The exact set attached to your profile is defined per integration and can differ.
| Field | Standard value | What it does |
|---|---|---|
strategy_min_threshold | 0.05 | Actions whose probability is below the threshold are zeroed and the remaining probability mass is renormalized. Removes noise-level actions from the results. |
disable_bet_randomness | true | By default the API applies a slight variation to bet-size selection for a more human-like response. The profile disables it so identical requests give consistent, reproducible results. |
reduced_actions_tier | 3 | Which reducer builds the reduced_actions presentation menu. Its presence also makes the API return the model's complete action menu in strategy.actions. Tier 3 is the standard 6-action reducer; see Reduced Actions Tiers for the smaller tiers. |
show_labels | true | Attaches the ev_rank classification label to every returned action (see Reading the Response). |
mode | "bot" | Declares the request's use case. Currently informational. |
protocol_ver | "v2.1" | Protocol version used for the request. |
Overriding a Default
Two rules govern the merge:
- Any field you send explicitly is kept as-is. The profile never overrides a value you provide.
- Only fields you omit (or set to
null) are filled with the profile defaults.
For example, to get the 4-button presentation menu (Fold / Call / Small Raise / Large Raise) instead of the standard 6-action one:
{
"request_id": "unique-request-id",
"hand": { <Hand-object> },
"reduced_actions_tier": 2,
"request_profile": "<client_profile>"
}is processed as:
{
"request_id": "unique-request-id",
"hand": { <Hand-object> },
"request_profile": "<client_profile>",
"strategy_min_threshold": 0.05,
"disable_bet_randomness": true,
"reduced_actions_tier": 2,
"show_labels": true,
"mode": "bot",
"protocol_ver": "v2.1"
}with your reduced_actions_tier: 2 kept and everything else defaulted (see Reduced Actions Tiers for what each tier returns).
Verifying the merge: the response's request object echoes the request after the profile was applied. During integration, inspect it to confirm exactly which values were in effect — see the worked example below.
Warning: an unrecognized
request_profilevalue is not an error. The request is processed normally, but no defaults are applied — you get the API's bare behavior with no error message. If the response is missingreduced_actionsor the labels, first check therequestecho: if the defaults are absent, the profile name did not match.
Reading the Response
The profile enables the post-processed presentation fields. The division of labor:
reduced_actions— the player-facing menu: the reduced action set (6 entries under the standard tier), each with areduced_action_sizingdisplay size. Before the flop it is a multiplier of the bet being raised over — the big blind, the straddle when one is posted, or the previous raise — with1.0for a call and0for a fold. After the flop it is the bet size as a percentage of the pot (50.0for a half-pot bet), with0for fold, check, and call. Use this menu for anything shown to players.strategy.actions— the model's complete action menu (every size the model considered), before reduction. Use it for verification and analysis.
With show_labels every action in both arrays carries the ev_rank label:
ev_rank | Meaning |
|---|---|
EVR_4 | The strategy actually plays this action (probability > 0). |
EVR_3 | Not played, but its EV is good (> +0.5 bb). |
EVR_2 | Not played, EV between −0.5 and +0.5 bb. |
EVR_1 | Not played, EV between −5.0 and −0.5 bb. |
EVR_0 | Not played, EV below −5.0 bb (clear mistake). |
Two further effects of the standard values worth knowing: probabilities you see are already thresholded and renormalized (strategy_min_threshold), and bet-size selection is deterministic (disable_bet_randomness), so repeated identical requests return the same values.
Reduced Actions Tiers
Note: the reduction rules described below represent the current behavior and will be updated in a future release.
- Tier 1 — 3 actions (Fold / Call / Raise)
- Tier 2 — 4 actions (Fold / Call / Small Raise / Large Raise)
- Tier 3 (default) — the usual 6-action reducer
Minimal request:
curl --location '<glue-URL>/lookup' \
--header 'Content-Type: application/json' \
--header 'Authorization: <Auth-TOKEN>' \
--data '{
"request_id": "tier2-demo",
"hand": { ... },
"request_profile": "<client_profile>",
"reduced_actions_tier": 2
}'The reduced_actions field in the strategy API response always returns a compact set of player-facing actions. By default the API uses a 6-action reducer ("Tier 3"). This section covers two additional tiers that produce even fewer actions for simpler client UIs.
How to use it
Add reduced_actions_tier to your existing request profile request — any field you send is kept as-is, and the profile fills in the rest:
{
"request_id": "unique-request-id",
"hand": { ... },
"request_profile": "<client_profile>",
"reduced_actions_tier": 1
}This is interpreted as:
{
"request_id": "unique-request-id",
"hand": { ... },
"request_profile": "<client_profile>",
"strategy_min_threshold": 0.05,
"disable_bet_randomness": true,
"reduced_actions_tier": 1,
"show_labels": true,
"mode": "bot",
"protocol_ver": "v2.1"
}Note that your explicit reduced_actions_tier: 1 wins over the profile's own default tier — that is the standard override rule. All other profile defaults still apply normally.
If you omit reduced_actions_tier, the profile path is unchanged (Tier 3 with the 6-action reducer), so this is fully opt-in.
Without a request profile
If you are not using request_profile, the tier field alone is enough — its presence enables the reducer and makes the API return the full model menu in strategy.actions:
{
"request_id": "unique-request-id",
"hand": { ... },
"reduced_actions_tier": 1
}Allowed values:
| Value | Output |
|---|---|
1 | 1 raise representative + legal non-raise actions (fold / call / check) |
2 | 2 raise representatives (small + large) + legal non-raise actions |
3 (default) | The standard 6-action reducer |
| (field omitted) | Same as 3 |
Each tier produces a fixed shape: Tier 1 and Tier 2 emit their fixed representatives, and Tier 3 emits the standard 6-action menu.
The result is in response.reduced_actions[]. The raw model output stays in response.strategy.actions[] for verification.
Tier examples
Tier 1 — postflop, facing a bet
{
"request_id": "tier1-demo",
"hand": { ... },
"reduced_actions_tier": 1
}Possible reduced_actions[]:
[
{ "action_name": "f", "hand_strategy": 0.20, "action": { "action": "fold" } },
{ "action_name": "c", "hand_strategy": 0.25, "action": { "action": "call" } },
{ "action_name": "r100", "hand_strategy": 0.55, "action": { "action": "raise", "pot_size": 1.0 } }
]The r100 entry's hand_strategy = 0.55 is the sum of every raise sizing's probability — every r<N> the model emitted plus any allin entries. The chosen sizing (r100) is the highest-probability one among them.
Tier 2 — postflop, facing a bet, with all-in
{
"request_id": "tier2-demo",
"hand": { ... },
"reduced_actions_tier": 2
}Possible reduced_actions[]:
[
{ "action_name": "f", "hand_strategy": 0.15, "action": { "action": "fold" } },
{ "action_name": "c", "hand_strategy": 0.20, "action": { "action": "call" } },
{ "action_name": "r50", "hand_strategy": 0.25, "action": { "action": "raise", "pot_size": 0.5 } },
{ "action_name": "allin400", "hand_strategy": 0.40, "action": { "action": "allin", "pot_size": 4.0 } }
]r50 represents the small group (pot ratio ≤ 0.50). allin400 represents the large group (pot ratio > 0.50) — in this spot the all-in is the highest-probability large raise. Each representative's hand_strategy is the sum within its own group only, not across all raises.
Tier 2 — postflop, no bet yet
When action checks to the hero, the model emits check plus bet/allin actions instead of call + raise/allin:
[
{ "action_name": "x", "hand_strategy": 0.40, "action": { "action": "check" } },
{ "action_name": "b33", "hand_strategy": 0.35, "action": { "action": "bet", "pot_size": 0.33 } },
{ "action_name": "allin300", "hand_strategy": 0.25, "action": { "action": "allin", "pot_size": 3.0 } }
]Here b33 is the small-group rep (sum of all bet sizings with pot ratio ≤ 0.50), and allin300 is the large-group rep (sum of every large bet sizing plus the all-in). If the model had no large bets and no large all-in in this spot, only x and b33 would be returned — Tier 2 omits a group entirely when it is empty.
Tier notes and limits
- The tiers only affect
response.reduced_actions.response.strategy.actionsalways contains the full unreduced model output. - A value of
reduced_actions_tieroutside{1, 2, 3}falls back to Tier 3 (default behavior). - If the model emits no raise actions at all on a given hand, Tier 1 / Tier 2 return only the legal non-raise actions. No synthetic raise is inserted.
EV Mistake Score
For B2C requests, ev_mistake_score uses the enhanced EV results available through the B2C configuration.
The feature scores how costly the player's actual decision was. Send player_action — the action the player really took, in the compact action-code format (f/fold, c/call, x/check, r50, b75, allin...):
{
"request_id": "unique-request-id",
"hand": { <Hand-object> },
"request_profile": "<client_profile>",
"player_action": "r50"
}The response then gains two read-only fields — nothing else changes (same strategy.actions, same reduced_actions, same numbers as the request without player_action):
{
"model_closest_action": "r50",
"solution_information": {
"ev_mistake_score": 1.85
}
}| Field | Where | Meaning |
|---|---|---|
model_closest_action | top level | The action_name from reduced_actions[] that the player's choice maps to — see the matching rules below. |
ev_mistake_score | solution_information | How much EV the player gave up, in big blinds: the difference between the best hand_ev in reduced_actions[] and the matched entry's hand_ev. 0 means the player took (or matched) the best available action; the larger the value, the costlier the decision. |
How model_closest_action is matched
The player's action is resolved against the entries of reduced_actions[], in this order:
- Exact name match, with common synonyms normalized:
fold↔f,check↔x,call↔c,bet50↔b50,raise75↔r75, and so on. - Bet/raise codes with no exact match (
b<N>/r<N>) resolve to the entry closest by pot percentageN; ties go to the higher-EV entry. - All-in codes (
allin<N>) resolve to the all-in entry when one is present, otherwise by pot percentage like any other raise. cwith no call entry (postflop with no bet to face) resolves to thecheckentry.
Because the matching runs against the reduced menu, the tier you selected sets the granularity: on Tier 1 every bet, raise, or all-in maps to the single raise representative; on Tier 2 it maps to the small or large representative by pot percentage (N ≤ 50 → small); on Tier 3 it maps to the closest of the up-to-6 entries. The mistake score therefore always compares the player against the same menu your UI displayed.
Notes:
- The computation reads from
reduced_actions[], so the reduced menu must be present in the response — the request profile guarantees that. Without it,player_actionis silently ignored. - The fold entry's EV is normalized to
0, so a fold where playing on was better scores exactly the EV of the best available option. - Combine with
reduced_actions_tierto control the granularity the player's action is compared at. - In the worked example spot below,
"player_action": "fold"returnsmodel_closest_action: "f"andev_mistake_score: 2.74— the EV of the best raise, which the fold gave up — while"player_action": "r32"returns0.0.
Worked Example
8-handed NLHE cash game with a straddle (seat 2) and a 50-chip ante. Preflop, first to act; the hero (seat 3, holding TdTc) is under the gun:
curl --location '<glue-URL>/lookup' \
--header 'Content-Type: application/json' \
--header 'Authorization: <Auth-TOKEN>' \
--data '{
"request_id": "profile-demo",
"hand": {
"gameuuid": "3b-0.5_8p_game",
"game_type": "nlhe",
"game_mode_code": "normal",
"players": [
{ "seat_no": 0, "stack": 46720 },
{ "seat_no": 1, "stack": 32580 },
{ "seat_no": 2, "stack": 27000 },
{ "seat_no": 3, "stack": 40368, "hole_cards": "TdTc" },
{ "seat_no": 4, "stack": 105250 },
{ "seat_no": 5, "stack": 9700 },
{ "seat_no": 6, "stack": 35440 },
{ "seat_no": 7, "stack": 13550 }
],
"big_blind": 100,
"ante": 50,
"dealer_seat": 7,
"sb_seat": 0,
"bb_seat": 1,
"straddle_seat": 2,
"actions": { "entries": [] }
},
"request_profile": "<client_profile>"
}'The response (abridged — { ... } marks elided parts: the full hand echo, part of the 9-entry action menu and the 6-entry reduced menu, and members not covered on this page):
{
"request": {
"request_id": "profile-demo",
"hand": { ... },
"request_profile": "<client_profile>",
"strategy_min_threshold": 0.05,
"disable_bet_randomness": true,
"reduced_actions_tier": 3,
"show_labels": true,
"mode": "bot",
"protocol_ver": "v2.1"
},
"strategy": {
"actions": [
{ ... },
{
"action": { "action": "allin", "amount": 40318, "pot_size": 42.229473684210525, "seat_no": 3 },
"action_name": "allin4223",
"hand_strategy": 0.0,
"hand_ev": -28.764751434326172,
"ev_rank": "EVR_0"
},
{
"action": { "action": "raise", "amount": 500, "pot_size": 0.31578946113586426, "seat_no": 3 },
"action_name": "r32",
"hand_strategy": 0.5032436201623129,
"hand_ev": 2.7414441108703613,
"ev_rank": "EVR_4"
},
{
"action": { "action": "raise", "amount": 700, "pot_size": 0.5263158082962036, "seat_no": 3 },
"action_name": "r53",
"hand_strategy": 0.49675637983768706,
"hand_ev": 2.7414441108703613,
"ev_rank": "EVR_4"
},
{
"action": { "action": "raise", "amount": 900, "pot_size": 0.7368420958518982, "seat_no": 3 },
"action_name": "r74",
"hand_strategy": 0.0,
"hand_ev": 2.5574519634246826,
"ev_rank": "EVR_3"
},
{ ... }
],
"suggested_action": {
"action": "raise",
"amount": 500,
"pot_size": 0.31578946113586426,
"seat_no": 3
}
},
"reduced_actions": [
{ "action": { "action": "fold", "amount": null, "pot_size": null, "seat_no": 3 },
"action_name": "f", "hand_strategy": 0.0, "hand_ev": 0.0,
"ev_rank": "EVR_2", "reduced_action_sizing": 0.0 },
{ "action": { "action": "raise", "amount": 500, "pot_size": 0.31578946113586426, "seat_no": 3 },
"action_name": "r32", "hand_strategy": 0.49835683555883337, "hand_ev": 2.7414441108703613,
"ev_rank": "EVR_4", "reduced_action_sizing": 2.5 },
{ "action": { "action": "raise", "amount": 700, "pot_size": 0.5263158082962036, "seat_no": 3 },
"action_name": "r53", "hand_strategy": 0.5016431644411666, "hand_ev": 2.7414441108703613,
"ev_rank": "EVR_4", "reduced_action_sizing": 3.5 },
{ ... }
],
"supportScore": 0.5,
"solution_information": {
"situation_key": {
"street": "preflop",
"flop": "na",
"players_bin": "8",
"bb_depth": 100.0
}
},
"warnings": [],
"flags": []
}How to read it:
- The
requestecho shows every profile default that was applied — this is the merge-verification tool from the section above. - The strategy is an even mix of two raise sizes with
TdTc: raise to 500 ("r32", 50.3%) and raise to 700 ("r53", 49.7%), both labeledEVR_4. Thesuggested_actionis the raise to 500. - The label does its job on the rest of the menu: open-shoving 100 bb (
"allin4223") isEVR_0— an unplayed action with a disastrous EV of −28.76 bb — while the unplayed"r74"isEVR_3, an action the strategy skips even though its EV is still positive. - The model's full menu has 9 entries (
strategy.actions); the standard tier-3 reducer trimmed it to the 6-entryreduced_actionsmenu, each entry carrying its display sizing (reduced_action_sizing). This spot has a 2 bb straddle, so the raise to 500 chips shows2.5— 2.5 times the straddle it raises over.
Notes
- The profile is applied per request. There is no server-side state — omitting
request_profileon a call simply processes that call without the defaults. - Every parameter in the set can also be sent standalone, without a profile; the profile is a convenience, not a gate.