Combos
Build a combo from 2 to 50 legs, get a quote from the market makers, accept it inside its window, and manage combo positions (Phase 8). Terms are in glossary.md: Combo, Leg, Combo mode, Slip, Use as leg, Remove, Market maker, Quote, No quote, Accept, Decline, Expired, Combo position. Venue facts that shape the design (polymarket.md §16):- A quote arrives about 400 ms after the request, or the request ends with no quote.
- The window to accept is 5–10 s, given by the server’s
expires_at. Only Accept trades. - There is no venue decline. Decline dismisses the quote in the app; an unaccepted quote expires on its own.
- A requester only buys the combo (a budget in pUSD, fees included) or sells combo shares (Exit). There is no combo book and no combo price other than the quote.
- Get quote works in Safe. Accept needs Armed, trading on and a quote with 1 s or more left.
Leg builder
Decision: combo mode, no hover affordance
TheCombo toggle turns on combo mode. While it is on, the Sell and Buy buttons of
every outcome line on the board, the watchlist and the market view’s outcomes panel are
replaced by one leg toggle in the same box. A click on it uses that outcome as a leg,
or removes it. The pressed Combo toggle and the slip’s Combo heading are on screen
exactly while those buttons build legs. Nothing else changes: the ladder, the portfolio
and the orders table keep placing and cancelling orders as usual.
Why this and not the draft’s hover + leg:
- What you see is what a click does. A price button’s click sends a GTC limit at that price (principle 4). A mode that silently changed that would be dangerous. Here the green and red buttons disappear and a neutral leg toggle takes their place, so in either mode the control on screen states its effect. The dangerous mistake (meaning to use a leg, placing an order) needs the user to ignore a green Buy button.
- No small target beside a one-click Buy. A hover
+ legnext to Buy puts a near-miss one pixel from a real order. - One path on both devices. Hover does not exist on touch, and long-press already cancels your orders or opens the action sheet (interactions.md, “The one rule”). A mode plus a tap works the same on a phone.
- Copy rules.
+ legis icon and text, which is reserved for navigation and Cancel all, andAddmeans buying more of a held outcome (glossary).
The Combo toggle
outlinebutton at--control-h(44 on touch), labelCombo,aria-pressed. On:--primarybg and--primary-foregroundtext, like the active stake segment.- One global state: every toggle shows it. It survives route changes for the session and is off after a reload (legs are kept for the browser session).
- Tooltip:
Combo mode · L. Disabled when combos are unavailable, with the reason (Combos unavailable · Session Key lacks the combo scope). - Every placement is left-anchored or in a full-width header, so the toggle does not move when the slip opens.
Leg toggle
Replaces an outcome line’s Sell and Buy buttons while combo mode is on. It takes their whole box, so nothing else on the line moves.- “Same market” for the replace rule is the Gamma market. Three board lines that are one
neg-risk event (soccer home, draw, away) count as one market for this rule, since two of
their Yes outcomes cannot both win. Any other contradiction is the venue’s to report
(
Rejected · legs contradict each other). - On touch there are no tooltips: a tap on a disabled toggle shows its reason in the combo bar’s text slot for 3 s instead.
Slip
The slip holds the legs, the stake, the quote and the actions. There is one slip. It has two states:- Build (heading
Combo): the legs used in combo mode; a buy quote for the stake. - Exit (heading
Exit combo): opened by a combo position’sExit. Legs are the position’s and read-only, the size is the whole position, and the quote is a sell. The build legs are set aside and come back when the exit ends (filled, declined, expired, rejected or closed). An exit slip does not turn combo mode on; the board’s price buttons stay Buy and Sell.
Desktop (right dock, 320 px)
Where it shows: Live, Watchlist and market view while combo mode is on (or an exit is loaded); always on Portfolio › Combos, soExit fills it without moving the table. It is
hidden, with its state kept, on other routes.
- It is the rightmost column, from the top bar to the bottom dock, 1 px
--borderon its left. Ladder slots becomefloor((width − rail − 440 − 320) / 232), at least 1: two at 1440. Docked ladders that no longer fit are hidden, not closed, and return when the slip closes. Below 1280 px the slip replaces the ladder dock. - Opening and closing re-lay the page in one frame, on the toggle click, like docking a ladder. The toggle does not move, and while the pointer is over the dock area the closing rule of app-shell.md applies (the gap stays until the pointer leaves).
- It is a focusable panel with the panel focus indicator (
F6andQreach it).
Combo or Exit combo (heading), the leg count (3 legs, muted, slot fits
50 legs), Clear (ghost, text; removes every leg, with the undo toast
Legs cleared · Undo, 5 s; hidden in an exit slip, width kept), and [x] (close; turns
combo mode off in build, ends the exit in exit; tooltip Close · L in build).
Leg rows (44 px, two lines, 12 px side padding):
- Order is the order legs were used; a replaced leg keeps its row. Rows never reorder.
- A new leg is scrolled into view only when the pointer is not over the slip.
- No combined price is shown for the legs. The quote is the one source of a combo’s price, and multiplying leg prices in the browser would be a second one (and money arithmetic).
Stake $ + 6 × 40). In exit it reads
Size 201.6 sh (the whole position) instead.
Quote area (fixed 120 px, never grows or shrinks):
Quote timer. The countdown ring at 14 px, then whole
seconds left (
7s), in a slot that fits 99s. It runs linearly from the remaining time
the server gives (never from a browser clock guess) and shows seconds rounded down.
--muted-foreground until under 1 s; then, while it reads 0s, it sits on a --warning
chip with --warning-foreground, the moment Accept disables. Reduced motion: the ring is
hidden and the seconds stay.
Action row (48 px): two equal slots, 8 px apart, both --control-h. The left slot is
Decline (ghost) while a quote is live and empty otherwise. The right slot is the one
filled button; its label and colour change with the state, its size never does.
Slip states
Rules:
- Accept is inert for its first 500 ms. It appears in the slot the user just clicked
for Get quote, so a double click or a repeated
Qcannot accept a quote nobody saw. - Changing the legs or the stake while a quote is live declines it (in the app;
nothing is sent), and the status line reads
Declined · legs changedorDeclined · stake changed. - Accept is sent once. The UI never resends it. After a timeout the backend reads the
RFQ status; the slip shows
Checking…until it knows (interactions.md, error feedback). - Filled keeps the legs, so the user sees what they bought. The next buy takes
Get quotethenAcceptagain;Clearempties the slip. - Cancel all also declines a live quote in the app. It cannot stop an accept in flight.
- A quote is announced once through a polite live region:
Quote 12.4 cents, 201.6 shares, cost $25.00 with fees, 7 seconds. Expiry and results are announced; the seconds are not.
Rejected · or No quote · extend reject-messages.ts:
Mobile (390 px): combo bar and sheet
Combo bar (44 px, full width, above the tab bar). It shows while combo mode is on or an exit is loaded. On Live, Watchlist and market view it takes the stake bar’s place, at the same height, so nothing moves; on other tabs it sits above the tab bar and the content gets 44 px of bottom padding.- Left: a text slot (body, truncates) that opens the sheet on tap. It shows
3 legs · $25(build) orExit · 201.6 sh(exit); with a live quote, the timer, the price and the cost or receive figure; otherwise the status word (Checking…,Filled,Declined,No quote,Rejected), and for 3 s the reason for a tapped disabled toggle. - Right: the primary slot, 136 × 44, with the same labels and rules as the desktop right
slot (including the 500 ms guard). There is no Decline on the bar: letting a quote
expire sends nothing either, and the sheet has
Decline. - The bar hides while the on-screen keyboard is open (the palette covers the screen anyway) and returns when it closes. A live quote keeps its timer meanwhile.
- About 7 legs are visible at 390 × 844; the list scrolls, and the stake, quote and action rows stay pinned.
- A long-press on a leg row opens the action sheet, headed by the full outcome and market
title (the touch equivalent of the tooltip):
Open market,Remove leg. - Closing the sheet (swipe down,
[x]) keeps the slip and any live quote; the bar shows it.
Combo positions (Portfolio › Combos)
Portfolio tabs becomePositions 12, Combos 3, Activity. The count is open combo
positions. Data: Data API combo positions through the backend, every 60 s and after a
fill (PLAN §3.7); valued at cost, since there is no combo book. Leg prices in expanded
rows come from engine/books like every price.
Desktop
The table fills the width left of the slip (1056 px at 1440).- Exit requests the sell quote at once (it works in Safe) and loads the exit slip. A second Exit on another row replaces the exit (the first quote is declined in the app).
- Exit is disabled with the reason when combos are unavailable, while an accept is in
flight (
Combo being accepted), or when a leg’s market is closed and not resolved (A leg's market is closed). - Below 1280 px, Avg then Open legs hide, from the right.
- Expanded row: one 32 px line per leg, inset 32 px, no nested box: outcome, market
(muted), leg status (
Open,Won,Lost), the live best bid for open legs (price cell with flash). The market text links to the market view. 50 legs expand to 50 lines; the table scrolls, never an inner box. Height appears in one frame and the lines fade in (motion.md, portfolio row). - Keys:
↑↓select,Enterexpand,Eexit (requests the quote and focuses the slip; works in Safe because it trades nothing). - Resolved rows sit in a
Resolvedgroup at the bottom, shown withShow resolved, as on the Positions tab. - The Activity tab lists combo trades with type
Comboand the legs summary in the market column.
Mobile
- Cards as on the Positions tab (358 px, 12 px padding). Tap the card body: the legs
expand inside the card (44 px lines, same content as desktop). Tap
Exit: the sheet opens with the sell quote being requested. - Won cards show
Claim on polymarket.com [external-link]instead ofExit; Lost and Partial cards show no action row (the card keeps its height with the status line only).
Four states per view
Hotkeys
Registered inlib/hotkeys; also listed in interactions.md.
- While combo mode is on,
BandSdo nothing on the board, watchlist and market-view outcome list (their buttons are leg toggles). The ladder keeps all its keys. - The palette gets
Combo mode(toggles it) andGet quotecommands. - Keyboard path for a 3-leg buy:
L, arrows andSpacethree times,Q,Mod+Enter.
Click budgets
“Armed” is a precondition (one click per session) and is not counted.Stress cases
Checked withSESAME_FAKE_VENUE_STRESS=1 at 1440 and 390.
- 50 legs. The leg list scrolls inside the slip (about 12 rows at 1440 × 900, 7 at
390 × 844); the header, stake, quote and action rows stay put and the count reads
50 legs. Toggles that would add a 51st leg are disabled. The combo row’s legs cell truncates, and its tooltip lists 10 legs then+40 more; the expanded row shows all 50. - Long titles. Every leg line truncates with
…and a tooltip (desktop) or the long-press action sheet heading (touch). Prices, the count, the timer and the figures never truncate; their slots reserve99.9¢,50 legs,99s,999,999.99and$99,999.99. - 390 px with the keyboard open. The slip has no text input (the stake is presets), so the keyboard only opens from the palette or another screen’s input. The combo bar hides under it rather than riding above it, a live quote keeps its timer, and the bar returns when the keyboard closes. If a text input is ever added to the sheet, the sheet sizes to the visual viewport with the stake, quote and action rows pinned above the keyboard and the leg list shrinking to at least two rows.
- A quote arrives while the pointer is over the slip. The quote area, both action slots and every figure slot already have their final size: values fill blanks, the right slot’s label and colour change in place, and nothing moves. Accept ignores input for its first 500 ms. The leg list never scrolls on its own while hovered, and leg prices update in place with the usual flash.
- Ladders docked when the slip opens. At 1440 with four ladders, two stay and two are hidden until the slip closes; none is closed.
- Stake at the max. Six presets fit the slip’s 296 px row; a
$99,999.99cost fits its figure slot.
Motion
- Desktop slip: its column appears in one frame; its contents fade in (
--dur-base,--ease-out), like a ladder dock slot. No width animation. - Mobile sheet: vaul with the motion tokens (
--dur-slow), as the ladder sheet. - Quote figures, the status line and the right slot’s label change instantly. The timer ring is a timer, not a transition (motion.md). Leg prices flash like any price cell.
- Nothing waits for an animation: Get quote and Accept fire on click, and Accept’s 500 ms guard is a behaviour constant, not a motion token.
Notifications
Two notification kinds (copy in notifications.md):combo_filled (Combo filled) and combo_failed (Combo rejected or Combo failed).
They also reach the user when the slip is hidden or closed.
Tokens
None added. The slip reuses--card, --border, --primary, --bid, --ask,
--warning, the control and touch heights, and the motion tokens. The 320 px slip width
and the 500 ms Accept guard are layout and behaviour constants in the component, like the
232 px ladder slot and the 350 ms double-click guard.
Open questions for the tech lead
- Timer source. The contract should carry the remaining time (
expires_in_ms) or the server’s clock withexpires_at, so a skewed browser clock cannot lengthen the countdown. The UI disables Accept under 1 s left either way. - One stake. The slip’s stake is the session’s active stake, shared with one-click orders (one concept, one control). The alternative is a separate combo stake; it would need its own glossary term.
- Get quote while trading is off or geoblocked is disabled in the UI (a quote that
cannot be accepted is noise, and the request would come from a blocked place).
engine/riskCheckQuoteshould refuse it too. RESOLVED_PARTIALandREDEEMABLE. The spec maps them toPartialandWon(with the claim link). Confirm against the live run.- Portfolio summary. Proposal: combo cost counts in Exposure; Liq value and Unrealised P&L leave combos out (no combo book). Otherwise the strip needs a combo line.
- Cancel all declines a live quote in the app. Confirm this reading of “kill everything”.