Skip to main content

Interactions

How input maps to actions everywhere in the app. Component looks are in components.md; screen layouts are in screens/.

Preconditions for trading

A trade action (anything that places or moves one of your orders) needs:
  1. Trading enabled on the server (/status trading_enabled: LIVE_TRADING set and not geoblocked).
  2. The Safe/Armed switch on Armed (server state).
  3. Fresh data for the price being clicked (not stale).
The UI reflects these states so the user knows what a click will do. engine/risk enforces them; the UI hiding a button is not a safety control. Cancel actions only need a connection: Cancel all, cancel-market and single cancels work in Safe, with trading off, and on stale data.

Click, drag and long-press

Event timing

  • Mouse: a trade fires on pointerdown (primary button). It does not wait for click, and no animation delays it.
  • Touch and pen: a trade fires on tap: pointer up within 500 ms and less than 8 px of movement. More movement is a scroll; a longer press is a long-press. This keeps scrolling from placing orders.
  • Double-click guard: a second activation of the same target (same market, side, price and stake) within 350 ms reuses the first click’s idempotency key, so the backend places one order. A click after 350 ms is a new order. (The 350 ms is a behaviour constant in the frontend trade helper, not a motion token.)

The one rule for right-click and long-press

Right-click (desktop) or long-press (touch, 500 ms) on your own order cancels it. On anything else it opens that thing’s context menu (desktop menu, mobile action sheet). Long-press gives haptic feedback where supported (navigator.vibrate(10)). The browser context menu is suppressed only inside trading panels.

Ladder (desktop)

Ladder (mobile bottom sheet)

Drag to reprice

Desktop only (mobile uses the order bar steppers).
  1. Pointerdown on a placed or partially filled chip, then move more than 4 px: drag starts. The original chip turns dashed and stays at its level.
  2. A ghost chip follows the pointer snapped to rows; the target row gets a dashed --ring outline; the cursor tag reads Move buy 25 → 44¢. If the target crosses the book (a buy at or above the best ask), the tag adds · fills now.
  3. Release on a row: send one reprice (the backend cancels and re-places; Polymarket has no amend). Chips show the moving state until both steps resolve.
  4. Release outside the ladder, on the original row, or press Esc: no action.
  5. The ladder does not auto-scroll during a drag. Near the edge, the user scrolls with the wheel while holding the chip.
Delayed, sending and cancelling chips cannot be dragged.

Board, watchlist, portfolio, orders

Deferred list changes

Principle 3 applied to lists. Row values update live. Row structure (insert, remove, reorder) is deferred while the list is in use:
  • In use = the pointer is over the list (desktop), or a finger touched it in the last 1.5 s (mobile).
  • A row that should be removed stays in place as a ghost: stale-styled, struck through, not interactive.
  • New rows and re-sorted rows are held. A 24 px pill at the top of the list reads 3 updates. Clicking it applies them.
  • When the list stops being in use, pending changes apply in one frame, with no layout animation.
  • Rows inserted above the viewport never shift visible rows: lists use scroll anchoring (overflow-anchor: auto) with stable row keys.

Stake selection

  • The active stake is a pUSD amount picked from the presets in settings (1–6 values). It is global for the session and survives route changes; it resets to the first preset on reload.
  • Where it shows: the top-bar segmented control (desktop), the stake bar (mobile), the ladder footer, and every cursor tag.
  • How to change it: click a segment, or in a focused trading panel (armed) press 1–6 to pick a preset, or - / = to step to the previous / next one (stopping at the ends).
  • How it becomes an order:
    • Buy at price p: size = stake ÷ p shares.
    • Sell from a Sell button or ask cell: size = min(stake ÷ p, position).
    • Exit (portfolio, dock and the ladder header), Shift+click sell: size = whole position.
    • Hedge: size comes from the backend’s hedge calculation.
    • In every case the backend rounds to the market’s size tick and minimum size in engine/risk. If the result is below the minimum, the order is rejected with the minimum stated.
  • The cursor tag and the mobile stake bar show the estimated size before the click ($25 → 59.52 sh).

Hotkey map

Hotkeys register only through lib/hotkeys. There are three classes:
  1. Navigation: always on, in Safe or Armed. They never touch orders.
  2. Global safety: Cancel all. Works everywhere, including inside text inputs, in Safe, and with any panel focused.
  3. Trading: only in the focused panel: the last one clicked, or reached with F6. Ignored while focus is in a text input. Keys that place, move or cancel orders work only when Armed. Keys that only move a cursor or a selection, or open something (the arrows, Space, Enter where it opens a ladder or a market, Alt+Enter, Esc), send nothing and also work in Safe; they are marked “(Safe too)” below.
Enter and Space belong to a focused control first. While a button, link, tab or field inside the panel has keyboard focus, Enter and Space activate that control as usual. Moving a panel’s cursor with the arrows moves keyboard focus from the control to the panel, so the next Enter or Space acts on the cursor. Focused panel indicator. The focused panel shows a 2 px --ring line along its top edge, inside the panel, so nothing moves. It shows only when it tells the user something:
  • after F6 / Shift+F6 (and Q, which moves focus to the slip), in Safe or Armed, so keyboard users see where focus went; the next click hides it again unless the rule below holds;
  • while Armed, when more than one visible panel could take the trading keys: two or more panels with trading keys (the board beside a docked ladder or the slip), or a focused panel without them (the collapsed bottom dock, or its Fills tab) beside one that has them.
With one panel on screen, or in Safe, a click shows nothing; the trading keys still act on the last-clicked panel. The line keeps at least 3:1 against both surfaces in both themes. Controls keep their own :focus-visible ring. Mod = Ctrl on Windows/Linux, ⌘ on macOS. Single-letter keys have no modifier.

Global safety

Trading (Armed, focused panel)

Any trading panel: Ladder (docked, in market view, or the mobile sheet with a hardware keyboard): While the keyboard cursor is on the ladder, it counts as in use: it does not re-centre by itself until Esc clears the cursor (Space and the edge pills still re-centre). A tick size change rebuilds the grid and clears the cursor. Board, watchlist and the market view’s outcome list (each outcome is one line): Each outcome sits on its own line with its Sell and Buy buttons, and B / S already pick the side, so ← / → have no outcomes to move between within a line; they move between games instead. In the market view the outcome list is its own panel: click it (or F6) for these keys; the rest of the view keeps the ladder’s keys. Portfolio and the dock’s Positions tab: Orders and the dock’s Orders tab: In the dock, the keys follow the open tab; the collapsed dock and the Fills tab have none. The cursor and selection last while the tab stays open. Combo slip (focused): While combo mode is on, B and S do nothing on the board, watchlist and market-view outcome list, because their price buttons are leg toggles. The ladder keeps its keys. When a trading hotkey is pressed in Safe, nothing is sent and the Safe/Armed switch shows its one-off ring highlight. Cancel hotkeys (C, Delete) are in the trading class too; in Safe, the Cancel buttons and right-click still cancel.

Sports delay window

On in-play sports markets the venue holds a marketable order for a few seconds before matching it (status delayed), and it cannot be cancelled during that time.
  • Before the click: a market with an in-play delay shows a delay chip in the ladder header and on the board row: 3s delay (muted text, no icon). Users know a crossing order will wait.
  • On the click: the order chip goes sending, then delayed with a countdown ring and seconds, at its price level. The board row’s pending-order dot shows the warning colour.
  • During the delay:
    • Right-click, long-press, drag, [/], stepper and Delete on that order do nothing. The tooltip or toast reads In delay window (2s).
    • Position, P&L and liquidation value do not change: they change on fills only.
    • Other orders in the market can still be placed and cancelled.
  • Cancel all or cancel-market during a delay: every other order is cancelled; each delayed order gets a queued cancel (cancel_pending), sent when its window ends, and the result toast says so: Cancelled 5 · 2 in delay window.
  • When the delay ends: the chip moves to filled, partially filled (the rest rests as placed), or placed, as reported by the user feed.
  • If the backend does not send an end time, the chip reads Delayed without a ring.

Pre-game wipe

The venue cancels resting orders on sports markets when the game starts.
  • Before the start, while you have resting orders in that game: the ladder status line and the board row show Starts in 8:04 · resting orders cancel at start (warning fill in the last 10 minutes, muted before). On the board it is the Wipe chip, with that text in its tooltip; a phone card, which has no tooltip, says Orders cancel at start.
  • One start format. Every view writes a start the same way, from the same clock: in 1:41 (m:ss) in the last 10 minutes, in 12m inside the hour (minutes rounded down from the seconds left, so in 10m is followed by in 10:00), in 3h under 24 hours (lib/format’s formatWhen), then 3 Oct 14:47, with the exact local time in the tooltip. The board clock, score badges, the ladder strip, the market’s Rules tab and the palette all show it, and tick every second in the last 10 minutes.
  • At the start, the game_start_orders notification: Orders cancelled at start / 3 orders · Lakers v Celtics (notifications.md), sent once every order open at the start has ended, at most 30 s after it. It lists only orders the venue reported cancelled without a cancel from this app, each for its unmatched size. Its toast carries Open; Re-post lives on the ladder status line and the board row, which read the wiped orders from the notification’s orders. The ladder status line shows 3 orders cancelled at start · Re-post for 5 minutes or until dismissed; an order the venue left open is not offered.
  • Re-post is one click and needs Armed. It sends the same side, price and unmatched size for each wiped order as new orders, each through risk; each rejected one gets its own reject toast (orders now outside the price band, for example). Re-posted and dismissed orders are not offered again on any device: the server keeps each order’s handled_at, set by the placement with the order’s client_order_id or by Dismiss, and sends the change to every session. That client_order_id is minted by the server with the list, so a second Re-post from any tab or device returns the first placement instead of a second order.

Error and reject feedback

Messages are factual: what happened and the numbers, no advice (CLAUDE.md). Rules:
  • Never auto-retry an order or reprice from the UI. After a failure, the user clicks again. The UI never resends on its own, and neither does the backend (CLAUDE.md).
  • The reject toast always names the order (side, outcome, price), because the user may have clicked several.
  • Rejected and failed intents are listed in the Orders screen’s Rejected tab for the session.

Click budgets

PLAN §4.1 budgets, with the design path that meets each. “Armed” is a precondition (one click per session on the switch) and is not counted. Combo budgets (build and buy in 6, exit in 2) are in combos.md. Mobile ladder-sheet repricing costs one extra tap because a tap on a price level has to place orders. The Orders tab meets the budget exactly (see open question 13).