Portal
Sign In Console

B2C Features

The B2C feature set of the strategy API POST /lookup endpoint is composed of three building blocks:

  1. Request Profile — several simple parameters control how the result is post-processed and presented; request_profile joins them into a single field that applies a predefined configuration. Each parameter still works individually, and explicit values always win over the profile.
  2. 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.
  3. EV Mistake Score — score the action the player actually took: it is matched to the closest entry of the reduced menu, and ev_mistake_score reports how much EV that choice gave up versus the best entry of that same menu — 0 means 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>"
}
FieldTypeRequiredDescription
request_profilestringNoThe 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.

FieldStandard valueWhat it does
strategy_min_threshold0.05Actions whose probability is below the threshold are zeroed and the remaining probability mass is renormalized. Removes noise-level actions from the results.
disable_bet_randomnesstrueBy 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_tier3Which 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_labelstrueAttaches 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:

  1. Any field you send explicitly is kept as-is. The profile never overrides a value you provide.
  2. 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_profile value 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 missing reduced_actions or the labels, first check the request echo: 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 a reduced_action_sizing display 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 — with 1.0 for a call and 0 for a fold. After the flop it is the bet size as a percentage of the pot (50.0 for a half-pot bet), with 0 for 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_rankMeaning
EVR_4The strategy actually plays this action (probability > 0).
EVR_3Not played, but its EV is good (> +0.5 bb).
EVR_2Not played, EV between −0.5 and +0.5 bb.
EVR_1Not played, EV between −5.0 and −0.5 bb.
EVR_0Not 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:

ValueOutput
11 raise representative + legal non-raise actions (fold / call / check)
22 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&lt;N&gt; 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.actions always contains the full unreduced model output.
  • A value of reduced_actions_tier outside {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
  }
}
FieldWhereMeaning
model_closest_actiontop levelThe action_name from reduced_actions[] that the player's choice maps to — see the matching rules below.
ev_mistake_scoresolution_informationHow 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:

  1. Exact name match, with common synonyms normalized: foldf, checkx, callc, bet50b50, raise75r75, and so on.
  2. Bet/raise codes with no exact match (b&lt;N&gt; / r&lt;N&gt;) resolve to the entry closest by pot percentage N; ties go to the higher-EV entry.
  3. All-in codes (allin&lt;N&gt;) resolve to the all-in entry when one is present, otherwise by pot percentage like any other raise.
  4. c with no call entry (postflop with no bet to face) resolves to the check entry.

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_action is 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_tier to control the granularity the player's action is compared at.
  • In the worked example spot below, "player_action": "fold" returns model_closest_action: "f" and ev_mistake_score: 2.74 — the EV of the best raise, which the fold gave up — while "player_action": "r32" returns 0.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 request echo 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 labeled EVR_4. The suggested_action is the raise to 500.
  • The label does its job on the rest of the menu: open-shoving 100 bb ("allin4223") is EVR_0 — an unplayed action with a disastrous EV of −28.76 bb — while the unplayed "r74" is EVR_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-entry reduced_actions menu, each entry carrying its display sizing (reduced_action_sizing). This spot has a 2 bb straddle, so the raise to 500 chips shows 2.5 — 2.5 times the straddle it raises over.

Notes

  • The profile is applied per request. There is no server-side state — omitting request_profile on 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.