0016. Combo ids: the pmc: namespace, legs keyed by Gamma position ids, eligibility from Gamma
- Status: Proposed
- Date: 2026-10-01
Context
A combo is its own market with its own condition and positions, separate from the Predictions CLOB. Its legs are ordinary Predictions outcomes, but the Combos APIs name them differently from the CLOB (venue notes §16, “Legs”):- The RFQ keys legs by Protocol V2 position ids, Gamma’s
positionIds, not by CLOB token ids (clobTokenIds) or condition ids. - The Data API’s
leg_condition_idon combo positions is not always the GammaconditionId(the fixtures show a0x01…form). - Two sources say which markets can be legs: Gamma
comboStatuson each market, and the public RFQ catalog (combos-rfq-api.polymarket.com/v1/rfq/combo-markets).
Decision
- Namespace
pmc:for the Combos venue:pmc:<combo conditionId>for a combo market andpmc:<combo YES positionId>for a combo position, which is also the Accept order’s token. Legs staypm:<tokenId>, the outcomes the rest of the app already shows, prices and links to. - Legs are keyed by Gamma position id only. The metadata cache stores each outcome’s
positionIdsentry inoutcomes.position_id(''when Gamma sends none), indexed. The browser sends leg token ids;engine/ordersmaps each to its position id through the catalog, and the adapter sends them as canonical decimals in ascending order. A leg the venue reports (a combo position’s legs) maps back to an outcome by position id: from the cache, or, for a closed market the cache does not hold, from Gamma byposition_idswithclosed=true, which is then cached. A leg is never matched by condition id or by the Data API’sleg_condition_id. - Eligibility comes from Gamma
comboStatusonly, refreshed with the rest of the market metadata intomarkets.combo_eligible:enabledis eligible;pending,disabledand any unknown value are not. A market with an outcome whose position id is missing is not eligible either. The public RFQ catalog is never read. engine/riskrefuses a quote request with a leg whose market is not eligible, or is unknown to the catalog, withcombo_ineligible, and a closed market withmarket_closed. Two outcomes of one market, or of one neg-risk event, arecombo_contradictory_legs.- The adapter checks that the venue’s quote echoes the request (legs by position id, side,
size, wallet) and names a combo condition before the quote is shown; a mismatch is
no_quotewithcombo_quote_mismatch.
Consequences
- Leg prices, links and names come from the existing
pm:outcomes and books; the combo itself has no book and no price but the quote (ADR 0007 and PLAN §3.7). - Combo positions and combo trades are told apart from Predictions ones by the
pmc:prefix; Data API trade rows withis_comboare mapped topmc:ids. - Eligibility is as fresh as the catalog’s Gamma refresh. A market the venue enables shows as eligible after the next refresh; one it disables in between is refused by the venue on the quote request. The app does not fill the gap from the catalog endpoint.
- A leg the cache has never seen needs one Gamma request before a combo position can show its names.
- Open: security finding P8-05. Gamma sends
outcomes,clobTokenIdsandpositionIdsas separate arrays, and the cache pairs them by index. The proposed fix marks a market ineligible unless its position ids are consecutive from the first, and compares the filled position’s legs with the Accept’s; the live run checks it (row 16.42).
Alternatives considered
- Key legs by CLOB token id and let the adapter translate at the edge. Rejected: the translation still needs the position id, and storing it once in the cache keeps one mapping.
- Match legs by condition id. Rejected: the Data API’s leg condition ids do not match Gamma’s.
- Read the RFQ catalog for eligibility, or merge it with Gamma. Rejected: two sources for one signal; when one goes stale the merged result still looks right.
- Reuse
pm:for combos. Rejected: combo positions would look like Predictions positions to every consumer ofpm:ids, including the books and the portfolio.