OllinDocs
Data flow & contracts

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), suit s 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's actions order.

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

FieldTypeMeaning
schema_version1Frozen. Breaking changes require a v2 document.
family_idstringThe solved spot, e.g. "100bb_3bet_call_p7v5_n13380".
boardstringCanonical board id, e.g. "Ks7s6s".
node_pathstring"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_bbnumberPot 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 }
]
FieldMeaning
idStable, unique in the document: fold | check | call | bet_<pctPot> | raise_<pctPot> | allin.
kindfold | check | call | bet | raise | allin.
amount_bbChips the actor puts in. Absent for fold/check.
fraction_potSizing as a fraction of the pot at this node (bet/raise/allin).

Ranges, strategy, EVs (per-combo truth)

FieldTypeMeaning
ranges.oop / ranges.ipRecord<ComboId, number>Reach probability per combo in [0,1], card removal already applied — combos colliding with the board are absent.
strategyRecord<ComboId, number[]>The acting player's mixed strategy per combo, aligned with actions, summing to ~1 (tolerance ~1e-6).
evsRecord<ComboId, number[]>EV per combo per action, bb, from the actor's perspective, valued at this node.
evs_currentRecord<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.
Excerpt — real document, 3-bet pot, Ks7s6s
"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:

FieldMeaning
class"AA", "AKs", "AKo", …
row / col0–12, ranks A→2. row < col suited, row > col offsuit, equal = pair.
weightTotal reach weight of live combos in the class (sum of the actor's range weights). 0 ⇒ no live combos; strategy/ev zero-filled.
strategyReach-weighted mean action probabilities, aligned with actions.
evReach-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

FieldMeaning
exploitability_bbNash distance of the solve, bb.
exploitability_pct_potSame as % of pot (0.2832 = 0.28% pot — inside the ≤ 0.3% target).
oop_ev_bb / ip_ev_bbEquilibrium 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 }
FieldMeaning
campaign / stamp / tierProvenance: campaign id, immutable dataset stamp, serving tier that produced the document.
pack_sha256Content address of the pack this document was extracted from — also the backbone of the ETag.
solverOptional 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.

On this page