NodeDocument v1 (postflop)
The single serving contract for every postflop strategy response, field group by field group.
Every strategy-bearing postflop response is a NodeDocument v1 — defined in
ollin-study-api/schema/node-document.v1.ts (with a JSON Schema mirror) and
mirrored client-side as src/lib/api/contract.ts. All three serving tiers
(root today, tree and live later) return this exact shape; only
coverage grows.
Conventions
- Cards — rank
A K Q J T 9…2(uppercase), suits h d c(lowercase). - Combo id — two cards, higher rank first; pairs ordered by suit
s > h > d > c:"AhAd","KsQs","AhKc". - Board id — three cards sorted rank-descending:
"AsKh2d". - Units — all chip amounts in big blinds (bb).
- Alignment — every per-combo/per-class array (
strategy,evs,grid169[].strategy,grid169[].ev) aligns index-for-index with the document'sactionsorder.
Source of truth
The per-combo strategy is the source of truth. The 13×13 grid169 is
aggregated by the server from per-combo data (reach-weighted means)
and is derived — clients must not treat it as independent data, and must not
recompute it when a live document is present.
Top-level identity
| Field | Type | Meaning |
|---|---|---|
schema_version | 1 | Frozen. Breaking changes require a v2 document. |
family_id | string | The solved spot, e.g. "100bb_3bet_call_p7v5_n13380". |
board | string | Canonical board id, e.g. "Ks7s6s". |
node_path | string | "root" for tier root. Tree/live tiers: action-path segments joined by ".", e.g. "root.bet_33.call" — reserved, not served in v0. |
street | "flop" | "turn" | "river" | v0 serves flop roots only; turn/river arrive with the tree tier. |
actor | "oop" | "ip" | Seat to act at this node. |
pot_bb / effective_stack_bb | number | Pot and effective remaining stack at this decision point (bb). |
actions
[
{ "id": "check", "kind": "check" },
{ "id": "bet_25", "kind": "bet", "amount_bb": 12.9236, "fraction_pot": 0.25 },
{ "id": "bet_55", "kind": "bet", "amount_bb": 28.4319, "fraction_pot": 0.55 },
{ "id": "allin", "kind": "allin", "amount_bb": 100, "fraction_pot": 1.93445 }
]| Field | Meaning |
|---|---|
id | Stable, unique in the document: fold | check | call | bet_<pctPot> | raise_<pctPot> | allin. |
kind | fold | check | call | bet | raise | allin. |
amount_bb | Chips the actor puts in. Absent for fold/check. |
fraction_pot | Sizing as a fraction of the pot at this node (bet/raise/allin). |
Ranges, strategy, EVs (per-combo truth)
| Field | Type | Meaning |
|---|---|---|
ranges.oop / ranges.ip | Record<ComboId, number> | Reach probability per combo in [0,1], card removal already applied — combos colliding with the board are absent. |
strategy | Record<ComboId, number[]> | The acting player's mixed strategy per combo, aligned with actions, summing to ~1 (tolerance ~1e-6). |
evs | Record<ComboId, number[]> | EV per combo per action, bb, from the actor's perspective, valued at this node. |
evs_current | Record<ComboId, number> (v1.1, optional) | Scalar EV of the actor's current mixed strategy per combo — the strategy-weighted mean of evs[combo], as emitted by the solver. Present only when the producer supplies it. |
"strategy": { "4c3c": [0, 0.998825, 0.001175, 0, 0, 0] },
"evs": { "4c3c": [14.8319, 15.5388, 14.9497, 12.4406, 4.6278, -0.1049] },
"evs_current": { "4c3c": 15.5381 }grid169 — the server-aggregated chart
Exactly 169 cells, row-major, aggregated from strategy/evs:
| Field | Meaning |
|---|---|
class | "AA", "AKs", "AKo", … |
row / col | 0–12, ranks A→2. row < col suited, row > col offsuit, equal = pair. |
weight | Total reach weight of live combos in the class (sum of the actor's range weights). 0 ⇒ no live combos; strategy/ev zero-filled. |
strategy | Reach-weighted mean action probabilities, aligned with actions. |
ev | Reach-weighted mean EVs (bb), aligned with actions. |
{ "class": "AA", "row": 0, "col": 0, "weight": 6,
"strategy": [0.021477, 0.941234, 0.035977, 0.001312, 0, 0],
"ev": [47.0729, 47.3414, 47.1934, 47.0528, 46.6779, 46.2034] }exploitability
| Field | Meaning |
|---|---|
exploitability_bb | Nash distance of the solve, bb. |
exploitability_pct_pot | Same as % of pot (0.2832 = 0.28% pot — inside the ≤ 0.3% target). |
oop_ev_bb / ip_ev_bb | Equilibrium EVs of the whole node per seat (bb), when published. |
meta — provenance and v1.1 flags
{ "campaign": "berserk_v1", "stamp": "20260721T131354Z", "tier": "root",
"pack_sha256": "38b207591014bfb9…", "solver": "solve_config=tiny via extract_roots_v0.py",
"ip_weights_uniform": true, "bb_chips": 100 }| Field | Meaning |
|---|---|
campaign / stamp / tier | Provenance: campaign id, immutable dataset stamp, serving tier that produced the document. |
pack_sha256 | Content address of the pack this document was extracted from — also the backbone of the ETag. |
solver | Optional solver identifier. |
ip_weights_uniform (v1.1) | true when ranges.ip is served with uniform weight 1 per combo because the producer data carries no recoverable IP reach weights. UIs must treat ranges.ip as membership-only when set — this is the "IP weights: uniform (v0)" badge. |
bb_chips (v1.1) | The chips-per-bb divisor used for every bb value in the document — 100 since adapter rev 2, the same HRC solve constant the preflop packs declare in settings.bb_chips, so postflop and bridge bb labels agree exactly. |
bb_chips_derived (v1.1, historical) | true in rev-1 documents, whose divisor was derived as eff_stack_chips / depth_bb — an approximation that pinned effective_stack_bb to depth_bb and inflated bb labels in raised pots. Retired in rev 2; you will not see it on live documents. |
The v1.1 fields are additive — v1 clients that ignore them stay correct. They exist because the sealed v0 producer format predates the serving contract; the API's in-process adapter absorbs the producer quirks and surfaces the honest caveats here rather than hiding them.