Portal
Sign In Console

PLO Tag Query & Browsing Contract

How to query the PLO range/tag browser and what comes back. Applies to plo4, plo5, and plo6 hand requests (examples use plo4). The controls described here are additive: they never mutate the underlying canonical bucket enumeration, they only filter, sort, page, and annotate it.

The request carries two optional objects:

ObjectPurpose
ploTagOptions (alias plo_tag_options)Response shaping: schema, per-combo tag/facet payloads, paging, sorting, profiling, strategy toggle.
plo_tag_queryFiltering + query semantics: require / exclude tags, **comboContains**, groupBy, plus its own paging/sort that can override ploTagOptions.

Both are optional. Sending neither returns the default first page of the canonical bucket map. Sending plo_tag_query (even empty) switches the response into "query mode" and adds a ploTagQueryResult block.

Every field accepts both camelCase and snake_case. Where a field is read from both objects, the precedence rules in Field precedence apply.


Quick reference

plo_tag_query

FieldTypeDefaultMeaning
requirestring or string[][]Tag names/aliases the hand must have (AND).
excludestring or string[][]Tag names/aliases the hand must not have.
comboContains (combo_contains)string or string[][]Suit-specific card filter (AND). See comboContains.
rankPattern (rank_pattern)stringunsetExact rank-multiset filter (ranks only, any suit/order). See rankPattern.
groupBy (group_by)string or string[][]Grouping key(s); echoed back.
normalizestring"conditional"Echoed normalization mode.
pageOffset (page_offset)int0Page start.
pageSize (page_size)int or "all"200Page size; "all" returns the full matched set.
sortField (sort_field)string"""combos", "pct", "ev", "raise"/"betRaise".
sortActionSlot (sort_action_slot)int-1Action column for pct/ev.
sortDir (sort_dir)string"desc""desc" or "asc".
skipZeroPages (skip_zero_pages)boolfalseEchoed hint for pct/ev/raise. See Sorting.
max_groupsint0Reserved (grouping cap).
max_examples_per_groupint0Reserved (grouping cap).

ploTagOptions

FieldTypeDefaultMeaning
includeSchema (include_schema)boolfalseAdd ploTagSchema (the tag dictionary for this variant/street).
includePerComboTags (include_per_combo_tags)boolfalseAdd ploTagMap (tag-id list per combo).
includePerComboFacets (include_per_combo_facets)boolfalseAdd ploFacets (named facet values per combo).
profileboolfalseAdd ploProfile timing/count block.
pageOffset (page_offset)int0Page start.
pageSize (page_size)int or "all"200Page size; "all" = full matched set.
aggregateAll (aggregate_all)boolfalseCompute exact aggregate over the full matched set, then strip per-combo payload.
sortField (sort_field)string""Same values as plo_tag_query.sortField.
sortActionSlot (sort_action_slot)int-1Action column for pct/ev.
sortDir (sort_dir)string"desc""desc" or "asc".
skipZeroPages (skip_zero_pages)boolfalseEchoed hint for pct/ev/raise. See Sorting.

comboContains, rankPattern, require, exclude, and groupBy live only in plo_tag_query. ploTagOptions does not carry them.

Strategy inference toggle

Tag browsing works without inference. Strategy outputs (ploStrategy, ploReach, actionLabels, and the pct/ev/raise sorts) require inference to be turned on with any one of:

  • top level: ploGetStrategy / plo_get_strategy / ploRunStrategy / plo_run_strategy
  • inside ploTagOptions: getStrategy / get_strategy / includeStrategy / include_strategy

comboContains

comboContains filters the range down to hands that physically contain specific, suit-qualified cards. It is the right tool for "show me every hand that has the Ace of hearts" or "show me hands with both the Ah and the Kh". For an exact set of ranks in any suit/order (e.g. "every A-K-8-3 hand") use rankPattern instead; broad class searches (e.g. "any ace", "any pair") belong in require / exclude.

Value format

Each entry is a string of one or more concatenated 2-character cards:

  • Rank (first char): 2 3 4 5 6 7 8 9 T J Q K A, case-insensitive.
  • Suit (second char): c d h s, case-insensitive.
  • Normalized internally to uppercase rank + lowercase suit (ah -> Ah).
"comboContains": ["Ah"]            // single card
"comboContains": ["AhKh"]          // compact multi-card string
"comboContains": ["Ah", "Kh"]      // array form, same as "AhKh"
"comboContains": ["AhKsQdJc"]      // a fully specified 4-card hand (plo4)

A value may be a single string or an array of strings. Multiple cards (whether in one compact string or spread across the array) are combined and de-duplicated, then matched with AND semantics: a hand must contain every requested card.

What it returns

Unlike normal browsing (which returns canonical, suit-isomorphic representatives with a multiplicity), comboContains returns the actual physical combos that contain the requested cards. Every returned row therefore has multiplicity = 1, and:

totalMatchingHandCount == totalMatchingComboCount

Each combo is still mapped to its bucket in ploBucketMap, so bucketing, tags, facets, sorting, and strategy all work on the physical rows.

Exact, deterministic match counts (plo4 preflop, empty board):

QueryMatched handsWhy
["Ah"]20825choose the other 3 cards from 51: C(51,3)
["AhKh"]1225choose the other 2 from 50: C(50,2)
["AhKsQdJc"]1the hand is fully specified

Postflop, the board cards are removed from the deck first, so the counts shrink accordingly.

Validation (fails closed)

  • A value must have even length and every 2-char chunk must be a valid card. Anything else (e.g. "AK83", "ZZ", "Ah5") marks that value invalid.
  • If any value is invalid, the whole query fails closed: 0 matches, with:
"ploTagQueryResult": {
  "status": "invalid_combo_contains",
  "warning": "one_or_more_combo_contains_cards_are_invalid",
  "comboContains": ["Ah", "ZZ"]
}
  • Requesting more distinct cards than the hand holds (e.g. 5 cards for plo4) yields 0 matches with status: "ok".
  • Requesting a card that is on the board yields 0 matches (you cannot hold a board card).

Composing with tags and sort

comboContains composes with require / exclude (the tag filter is applied to each physical combo) and with any sortField:

"plo_tag_query": {
  "comboContains": ["Ah"],
  "require": ["double_suited"],
  "exclude": ["pair"],
  "sortField": "raise",
  "sortDir": "desc",
  "pageOffset": 0,
  "pageSize": 50
}

rankPattern

rankPattern filters the range down to hands whose ranks are exactly a given multiset, ignoring suits and order. It is the rank-only complement to comboContains: use comboContains when you care about specific suited cards (Ah), and rankPattern when you care only about the ranks ("every A-K-8-3 hand, any suits").

Value format

A single string of exactly holeCardCount rank characters (4 for plo4, 5 for plo5, 6 for plo6), no suits:

  • Rank: 2 3 4 5 6 7 8 9 T J Q K A, case-insensitive.
  • Order does not matter; duplicate ranks are allowed (and meaningful).
  • Echoed back uppercased, in the order sent (not reordered).
"rankPattern": "AK83"   // plo4: ranks {A,K,8,3}, any suits, any order
"rankPattern": "3k8a"   // same set as "AK83" (case- and order-insensitive)
"rankPattern": "AAK3"   // plo4 one-pair shape: ranks {A,A,K,3}

Unlike comboContains, rankPattern is a single string (not an array) and carries no suits. comboContains: "AK83" is still invalid exact-card input and fails closed as invalid_combo_contains — the two filters do not overlap.

Semantics

The hand's hole-card ranks are sorted and compared for exact multiset equality against the sorted pattern. A plo4 hand matches AK83 only if its four ranks are exactly one A, one K, one 8, and one 3. Because duplicate ranks are significant, AAK3 matches only hands holding exactly two aces (plus a K and a 3) — not AAA3 and not AK83.

What it returns

On its own, rankPattern browses like the default: it returns canonical, suit-isomorphic representatives with their multiplicity, so totalMatchingComboCount is the raw-combo total and ploMultiplicity values can be > 1. Combined with comboContains, it returns the physical combos that contain the requested cards (multiplicity = 1), exactly as comboContains does on its own.

Exact, deterministic raw-combo counts (plo4 preflop, empty board):

QuerytotalMatchingComboCountWhy
rankPattern: "AK83"2564 distinct ranks × 4 suits each: 4^4
rankPattern: "AAK3"96aces C(4,2)=6, K 4, 3 4: 6·4·4
rankPattern: "AK83" + comboContains: ["Ah"]64Ah fixed, other 3 ranks × 4 suits: 4^3

Postflop the board cards are removed from the deck first, so counts shrink accordingly (a rank fully consumed by the board can drop the match to 0).

Validation (fails closed)

The pattern must be exactly holeCardCount characters and every character a valid rank. Anything else — wrong length (e.g. "AK8" or "AK835" for plo4), an invalid rank character ("AK8Z"), or an empty string — fails closed: 0 matches, with:

"ploTagQueryResult": {
  "status": "invalid_rank_pattern",
  "warning": "rank_pattern_is_invalid",
  "rankPattern": "AK8Z"
}

If both comboContains and rankPattern are invalid in the same request, invalid_combo_contains takes precedence.

Composing with comboContains, tags, and sort

rankPattern ANDs with comboContains, require / exclude, and any sortField:

"plo_tag_query": {
  "rankPattern": "AK83",
  "comboContains": ["Ah"],
  "sortField": "raise",
  "sortDir": "desc"
}

This returns the A-K-8-3 hands that hold the Ah, ordered by collapsed bet/raise probability.


Tag filtering: require / exclude

require and exclude take tag names or aliases and combine as "has all of require AND none of exclude". The available vocabulary is data-driven and depends on the variant and street, so treat the schema as the source of truth rather than hard-coding names:

  • Set ploTagOptions.includeSchema: true to get ploTagSchema, an array of { id, name, plane, domain, aliases } entries valid for the current holeCardCount and board street.
  • Use a name or any of its aliases in require / exclude.

If a require tag cannot be resolved the query fails closed (status: "unresolved_required_tag"); an unresolved exclude tag is ignored with status: "ok_with_unresolved_exclude". Unresolved aliases are listed under ploTagUnresolved.


Sorting

Set sortField (and sortDir, default "desc"). Pages are slices of one global order, so paging through a sort is stable.

sortFieldNeeds strategy?sortActionSlot?Ranks by
"combos"nonosuit-isomorphic multiplicity
"pct"yesrequiredprobability of the chosen action slot
"ev"yesrequiredEV of the chosen action slot
"raise" / "betRaise" / "bet_raise" / "betraise"yesnocollapsed bet/raise = sum(strategy[2..10])

Action slots: 0 = fold, 1 = check/call, 2..10 = all-in + bet/raise sizes.

  • "combos" is resolved purely from multiplicity (known before inference), so it is applied directly (ploPaging.sort.applied: true).
  • "pct" / "ev" / "raise" depend on model output, so they require the strategy toggle. The full matched set is ranked by that output, then sliced to the requested page; the final ploPaging.sort.applied is true and ploPageOrder reflects the order. Combos with no inference output always sort to the end regardless of direction.
  • skipZeroPages is accepted and echoed back under ploPaging.sort (and ploTagQueryResult). It is a forward-looking hint; the current pipeline does not prune zero/unavailable rows from the page on its own — those rows simply sort last (see above). Do not rely on it to change counts.
  • For "ev", if the model emits no usable or non-identical EVs the sort is not applied and sort.applied: false with sort.reason: "ev_unavailable".
  • An unsupported sortField keeps normal paging and sets sort.applied: false with sort.warning: "unsupported_sort_field".

Paging

  • pageOffset / pageSize page the matched set. Integer pageSize is capped (default 200).
  • pageSize: "all" returns the entire matched set with per-combo data (no cap). The full unfiltered PLO set is large, so this is an explicit opt-in.
  • aggregateAll: true enumerates the full matched set for an exact aggregate and then strips the per-combo payload, so only the rollup (ploOverallAggregate, bucket-tree aggregates) is returned.
  • Response paging echoes pageOffset, pageSize, pageHandCount, totalMatchingHandCount, totalMatchingComboCount, hasNextPage, nextOffset.

Field precedence

When both objects are present:

  • plo_tag_query supplies require, exclude, groupBy, comboContains, rankPattern, and (always) its pageOffset / pageSize.
  • skipZeroPages from plo_tag_query wins only if it is present.
  • A sort from plo_tag_query wins only if sortField is non-empty, so a ploTagOptions-level sort survives a filter-only plo_tag_query.

Response

Always present (taxonomy)

FieldMeaning
taxonomyOnlytrue.
holeCardCount4, 5, or 6.
numBoardCardsboard size (0 preflop).
sampledModefalse (results are exact).
ploBucketTree3-level taxonomy tree (id, label, children).
ploBucketMap{ comboString: bucketKey } for the page. Combo strings are concatenated cards, e.g. "AhKdQcJs".
ploMultiplicity{ comboString: suitIsomorphicCount } (always 1 for comboContains).
ploBucketCounts{ bucketKey: matchedCount } over the full matched set.
ploPagingpaging block + sort echo (see below).
ploPageOrderarray of combo strings in global sort order (present when a sort is applied).
fullTotalCount, totalCanonicalHandCount, totalRawComboCountenumeration totals.

ploPaging.sort echoes the request and the resolution state. field and dir are always present; applied says whether the page already reflects the global order; actionSlot appears for pct/ev; skipZeroPages is echoed when sent; warning/reason appear on the unsupported/ev-degenerate paths.

"sort": {
  "field": "raise",
  "dir": "desc",
  "applied": true
}

When plo_tag_query is present: ploTagQueryResult

"ploTagQueryResult": {
  "schemaVersion": 1,
  "sampledMode": false,
  "estimated": false,
  "available": true,
  "status": "ok",
  "normalization": "conditional",
  "require": ["double_suited"],
  "exclude": ["pair"],
  "groupBy": [],
  "comboContains": ["Ah"],
  "matchedHandCount": 8479,
  "matchedComboCount": 8479,
  "matchedWeight": 8479,
  "paging": { "...": "mirror of ploPaging" },
  "groups": []
}

status values: ok, invalid_combo_contains, invalid_rank_pattern, unresolved_required_tag, ok_with_unresolved_exclude, tag_data_unavailable. rankPattern (uppercased) is echoed only when a rank pattern was requested. (groups is reserved and returned empty by this path.)

When tag payloads are requested

  • includeSchema -> ploTagSchema: [{ id, name, plane, domain, aliases }].
  • includePerComboTags -> ploTagMap: { comboString: [tagId, ...] } (resolve ids via ploTagSchema).
  • includePerComboFacets -> ploFacets: { comboString: { facetName: value } }.
  • ploTagAvailable, ploTagSchemaVersion, and on alias problems ploTagWarning + ploTagUnresolved.

When strategy inference is on

FieldMeaning
ploStrategyper combo: strategy[11], evs[11], ev, raise, betRaise, and reach (if a reach query ran).
ploOverallAggregatemultiplicity-weighted fold/call/raise + EV rollup.
ploBucketTree[].aggregatethe same rollup attached per tree node.
ploReach{ comboString: reachProbability } (when requested).
actionLabels, actionHistory, tableState, supportScoregrid/UI metadata.

A single ploStrategy entry:

"AhKhQdJd": {
  "strategy": [0.12, 0.35, 0.03, 0.10, 0.18, 0.08, 0.14, 0.0, 0.0, 0.0, 0.0],
  "raise": 0.53,
  "betRaise": 0.53,
  "ev": 1.24,
  "evs": [0.0, 1.1, 1.2, 1.3, 1.4, 1.2, 1.1, 0.0, 0.0, 0.0, 0.0]
}

When profile: true

ploProfile with filterPageMs, bucketCount, enumeratedCanonicalHands, matchedCanonicalHands, returnedPageHands, totalBuildMs.


Worked examples

1. Browse the default first page (no inference)

{
  "protocol_ver": "v2.1",
  "hand": { "game_type": "plo4", "...": "..." },
  "ploTagOptions": { "pageOffset": 0, "pageSize": 50 }
}

Returns the first 50 canonical buckets/combos with ploMultiplicity, no ploStrategy.

2. comboContains: every hand with the Ah

{
  "protocol_ver": "v2.1",
  "hand": { "game_type": "plo4", "...": "..." },
  "ploTagOptions": { "pageOffset": 0, "pageSize": 100 },
  "plo_tag_query": { "comboContains": ["Ah"] }
}

ploTagQueryResult.matchedHandCount == 20825; every key in ploBucketMap contains Ah; all ploMultiplicity values are 1.

3. comboContains + tags + collapsed raise sort (with inference)

{
  "protocol_ver": "v2.1",
  "show_labels": true,
  "hand": { "game_type": "plo4", "...": "..." },
  "ploTagOptions": {
    "getStrategy": true,
    "pageOffset": 0,
    "pageSize": 50,
    "sortField": "raise",
    "sortDir": "desc",
    "includePerComboTags": true
  },
  "plo_tag_query": {
    "require": ["double_suited"],
    "exclude": ["pair"],
    "comboContains": ["Ah"],
    "sortField": "raise",
    "sortDir": "desc"
  }
}

Returns double-suited, unpaired hands that contain Ah, ordered by total bet/raise probability; ploPageOrder holds the order and each ploStrategy row carries raise/betRaise.

4. pct sort on one size, skipping zero rows

{
  "protocol_ver": "v2.1",
  "hand": { "game_type": "plo4", "...": "..." },
  "ploTagOptions": {
    "getStrategy": true,
    "pageOffset": 0,
    "pageSize": 50,
    "sortField": "pct",
    "sortActionSlot": 4,
    "sortDir": "desc",
    "skipZeroPages": true
  }
}

Ranks combos by the probability of action slot 4 (a bet/raise size). Combos with zero/no probability there sort to the end. skipZeroPages is echoed under ploPaging.sort but does not itself prune the page (see Sorting).

5. rankPattern: every A-K-8-3 hand (rank-only, any suits)

{
  "protocol_ver": "v2.1",
  "hand": { "game_type": "plo4", "...": "..." },
  "plo_tag_query": { "rankPattern": "AK83", "pageSize": "all" }
}

ploTagQueryResult.status == "ok", rankPattern == "AK83", and ploPaging.totalMatchingComboCount == 256; every key in ploBucketMap has ranks {A,K,8,3} in some suit order. Adding "comboContains": ["Ah"] narrows it to the 64 physical combos that hold the ace of hearts (each with multiplicity = 1).