sesame-bun: Implementation Plan
A single-user web trading terminal for Polymarket (Predictions, then Combos), with a second prediction-market venue (Yeet) added later. The Go backend signs orders with a server-held Session Key; the React frontend is designed to need as few clicks as possible for live sports trading and for managing many positions. Status: draft v1, 2026-09-30. The tech lead (the main agent) owns this document.1. Decisions
Geographic restriction (hard constraint)
This dev server’s IP resolves to US/VA and Polymarket geoblocks it (/api/geoblock → blocked:true). The project will not include proxying, IP masking or any other geoblock workaround. Read-only public endpoints are used from here for development. The backend calls the geoblock endpoint at startup and refuses to enable trading while blocked is true. Live trading (Phase 3 verification onward) runs only from a location where the user is permitted to trade.
2. Team (subagents)
Each role is one subagent working in its own git worktree. Not every role is active in every phase (see the phase tables). At most about 8 run at once.How parallel work stays unblocked
- Contract first. The first task of each phase is the tech lead and the relevant engineers updating
/api/openapi.yaml. After codegen, FE and BE build against the same generated types at the same time. - Folder ownership. Each role edits only its own folders (see Owns). Changes elsewhere go through the tech lead.
- Merge order within a phase: contract, then backend, then frontend, then docs. Each PR is rebased onto
mainbefore merging. - Definition of done for a PR: lint/format/typecheck clean, builds, the UX designer has reviewed FE PRs, and the security reviewer has reviewed anything touching keys, signing, auth or risk.
3. Architecture
3.1 Repo layout
3.2 Backend: actors and event bus
- Actor = a goroutine that owns its state and reads a mailbox channel. No other code touches that state. Request/reply uses a message carrying a reply channel plus a
contextfor timeouts. - Supervision:
thejerf/suture/v4restarts crashed actors with backoff. Feed actors reconnect on their own and publishFeedStatusevents, which the UI uses to grey out stale data. - Bus: typed topics. Venue feeds publish
core.TopicBookEvents,core.TopicGamesandcore.TopicFeedStatus; engine and hub topics are defined in their packages (e.g.hub.TopicFeeds). Later phases add order, fill, position, risk and alert topics. Subscribers get bounded buffers. Slow consumers drop and resync from a snapshot rather than blocking anyone else.
engine/orders, RiskEngine is engine/risk, Portfolio is engine/portfolio, AlertEngine is engine/alerts, BookActor is engine/books, and BrowserHub is hub.
- OrderManager:
- Each order carries a client idempotency key.
- Pre-trade checks call RiskEngine first.
- Every order is written to the append-only audit log before it’s sent.
- After a user-feed reconnect, it re-reads open orders and trades over REST, because that feed doesn’t replay missed events.
- It tracks the sports-delay state (
delayed, which can’t be cancelled).
- RiskEngine limits:
LIVE_TRADINGenv flag, and the UI SAFE/ARMED switch- geoblock status
- max order notional, max position notional per market (
max_position_notional), max daily notional, max open orders - tick and minimum-size rounding
- a price sanity band (the clicked price must be within N ticks of the current book)
- Dead-man heartbeat: an optional setting (off by default). While it’s on, Polymarket cancels every open order if the backend stops.
3.3 The Venue boundary
core defines small interfaces that each adapter implements, and advertises optional capabilities:
Discovery: search, events, markets, tags, sports metadataMarketData: book snapshots and streams, price historyTrading: place, cancel, cancel-market, cancel-all, open ordersPortfolio: positions, activity, balancesMarketFeedandSportsFeed: feed actors publishing on the core bus topics (seebackend/internal/core/venue.go, the source of truth for these interfaces)
pm:<tokenId>, yeet:<id>).
3.4 Browser ↔ backend
- REST for commands and queries.
- One WebSocket per tab, with a subscription protocol:
{op:"sub", topics:["book:pm:<token_id>","game:pm:<game_id>","feeds","status"]}(later phases add order and position topics). The server replies with a full snapshot, then sends changes. Updates are coalesced to about 10 Hz per topic. Every message has a sequence number, and a gap triggers a resync.
3.5 SQLite schema (initial)
Phase 8 (Combos) adds columns in migration
0018_combos.sql (0016 and 0017 were taken by Phases 4 and 6) and no new table:
Later columns:
0019_market_combo_status.sql adds markets.combo_status (enabled, pending or disabled, the Gamma comboStatus with unknown values as disabled; it says why a market is not combo_eligible), and 0020_notification_orders.sql adds notifications.orders (JSON, the open orders a game_start_orders start cancels, for Re-post; '' for other kinds).
- The combo quote and accept audit rows reuse
order_auditand its kinds (intent,decision,signed,response,reconcile). The body carriesaction(combo_quoteorcombo_accept), therfq_idandquote_id, and for the accept the order hash;notionalis the quote’stotal_requiredfor a buy and 0 for a sell. The quote and the accept each have their ownclient_order_id, soUNIQUE (client_order_id, kind)holds. - An accept whose
signedrow has noresponseor finalreconcilerow is reconciled at start by an RFQ status read, like an unknown placement. - Recent RFQs (10 min) live in
engine/ordersmemory only. Combo positions and activity are not stored. - The foreign-activity check needs no new column: combo trades are matched to accept audit rows by
tx_hash, and the existingused_sizesandchecked_idscover them.
3.6 Security baseline
- Secrets come only from env. They are never logged: a redacting logger (
sloghandler) strips them. - The Session Key has limited scope and an expiry. The owner key never reaches this machine.
- UI login:
- argon2id password hash
HttpOnly,SameSite=Strictcookie, plus a CSRF token on mutating requests- rate-limited login attempts
- Binds to
127.0.0.1in dev. TLS goes in front at deploy. - Telegram bot token and chat ID come from env. Messages never include secrets.
3.7 One source per signal
Each signal has exactly one source (seeCLAUDE.md). A snapshot followed by deltas from the same feed counts as one source. Change this table before changing code.
4. UX specification (summary; full specs in docs/design/)
- Global shell, desktop:
- Top bar: Ctrl+K palette, venue badge, cash/exposure/open orders, SAFE/ARMED switch, Cancel All (hotkey)
- Left rail: Live, Watchlist, Portfolio, Orders, Alerts
- Bottom dock: positions, orders and fills
- Global shell, mobile:
- Bottom tab bar: Live, Watchlist, Portfolio, Orders, Search
- A sticky Cancel All button, and the SAFE/ARMED switch in the header
- Ladders open as full-height bottom sheets
- Command palette:
- Searches markets, teams, your positions and commands
- Prefixes:
@teams/events,#tags,>commands - Enter opens, Shift+Enter pins, Alt+Enter opens in a new panel
- Ranking: pinned, then held, then live, then volume
- Live sports board:
- One row or card per game, grouped by league: score, clock, period, possession, bid/ask per outcome (click to trade at the preset stake), position and P&L, a pending-orders dot
- Up to 4 ladders on desktop
- A warning before game start that resting orders will be cleared, with one-click re-post after the start
- A “delayed (can’t cancel)” countdown chip
- Price ladder:
- Depth, last trade, your orders on their price levels
- A “P&L if closed here” column
- Click to place, drag to reprice, right-click (long-press on mobile) to cancel
- Header buttons: Exit, Hedge and Cancel all
- The ladder doesn’t re-centre while the pointer is over it
- Portfolio:
- Position rows with liquidation value (walking the book, not the midpoint), unrealized P&L, resting-order count, and live score
- Row actions: Exit, Exit @ join ask, Add, ×Orders, expand to show the ladder
- The orders table supports inline tick steppers and bulk cancel/reprice
- Grouping by event, league, expiry or venue
- Stale data: when a feed lags past a threshold, prices grey out and clicking them is disabled.
- Hotkeys: only active when ARMED, and they act only on the focused panel (heavy focus ring). The Cancel All hotkey always works.
4.1 Motion and navigation
Aim: navigation feels instant and continuous. Motion explains a change; it never delays one. Making navigation fast (more important than animating it):- Routes preload on hover or focus (TanStack Router
preload: "intent"), so data and code are ready before the click. - Previous data stays on screen while the next loads (
placeholderData: keepPreviousData), so nothing flashes blank. Skeletons show only on a cold load. - Route chunks split per feature and are prefetched when the app is idle.
- Scroll position is restored per route. The watchlist, board and portfolio keep their state when you switch tabs.
tokens.css; all components use these):
Where motion is used:
Rules that override the tables above:
- Ladders and books never auto-scroll or re-centre with animation while the pointer or a finger is over them. A price must never move under the cursor.
- Values are never tweened or counted up (prices, sizes, P&L, balances). They update instantly.
- Only
transformandopacityare animated; no layout properties on live rows. Rows with streaming data never use layout animations. prefers-reduced-motionturns all durations to 0 except the price flash, which becomes a static colour marker.- Clicking an action never waits for an animation. Order commands are sent on pointer-down/click, and animations run alongside.
- Motion library: none by default. CSS transitions, the View Transitions API, vaul and sonner cover the table above. Adding a library (e.g.
motion) needs a tech lead decision.
4.2 Design decisions (Phase 0)
The tech lead accepted the designer’s proposals indocs/design/index.md:
- The price flash is 400 ms (
--dur-flash), the one exception to the 240 ms cap. Portfolio rows expand by changing height in one frame and fading the contents in; height is never animated. - Trading hotkeys need ARMED; navigation hotkeys work in SAFE; Cancel All is global.
- The backend supplies money figures shown in the UI (exit price, liquidation value, hedge sizes). The browser previews share estimates and the ladder’s per-level P&L-if-closed with exact decimals; these are display-only (decided in Phase 2: per-level values on every tick would be heavy to stream).
- A cancel sent while an order is in the sports delay window is queued by the backend and sent when the window ends; the contract carries the delay end time.
- Exit sells the whole position at the lowest bid level that fully fills it. Position-reducing orders are exempt from the price band but capped by a max-slippage setting (Phase 3 contract).
- Ladder rows are 28 px with a mouse and 44 px on touch; up to 4 docked ladders persist across Live and Watchlist.
- Selling with no position is disabled. A repeat click on the same price within 350 ms reuses the first idempotency key.
- Palette ranking applies pinned > held > live > volume inside text-match tiers.
- Contract gaps (notification settings, layout state, feed age, cancel-only venue state) land in the phase contracts that need them.
5. Phases
Each phase lists the parallel work per role, what gets delivered, and the exit gate (acceptance criteria the QA engineer checks and the tech lead signs off).Phase 0: Foundations
Goal: An empty but fully wired monorepo in which every role can work in parallel.
Exit gate:
task devstarts backend and frontend.- Login works.
- The theme toggles.
task lintpasses on a clean tree.- Startup logs the geoblock status and shows trading as disabled while blocked.
Phase 1: Predictions, read-only market data
Goal: Find any market in seconds and watch live prices and scores.
Exit gate:
- Searching an event name, team or tag finds it in under 300 ms.
- A live game’s score and book update without a reload.
- A dropped feed greys out prices and recovers.
- Works on a phone-width viewport.
- Contract:
api/openapi.yamlPhase 1 paths and WS schemas; Go domain types and venue interfaces inbackend/internal/core/{market,venue}.go. Engine code depends oncoreinterfaces only, never onvenue/polymarket. - Public
/healthreturnsstatusonly; version, geoblock, trading state and feed health move to authenticated/status(security F20). - The geoblock actor retries a failed check with backoff (30 s, 1 min, 2 min, then every 10 min) instead of waiting 10 min; it still fails closed.
- The frontend may mirror
theme_modein localStorage purely to paint the first frame; settings stay the source and overwrite it on load. - Security controls due this phase: F3a/F3c (no redirects, validated venue URLs), F11 (Host allowlist, Origin checks, no CORS, WS Origin check), F12 (security headers), F13 (error body caps), F14/F24 (session hardening), F18 (lockfiles,
govulncheck,gitleaks), F19 (nodangerouslySetInnerHTML,lib/url), F23 (input limits).
Phase 2: Account and portfolio, read-only
Goal: See all positions, orders and activity, live.
Exit gate:
- Every open position and order matches polymarket.com.
- Liquidation value updates live.
- After killing and restoring the network, the orders list is correct again with no manual refresh.
- Contract:
api/openapi.yamlaccount paths and schemas (AccountSummary,Position,Order,Fill,Activity,PnlSeries,SignedDecimal) and WS topicsaccount,positions,orders,fills; Go types and interfaces inbackend/internal/core/account.go(core.Account,core.UserFeed, topics). - Credentials (security F1):
POLYMARKET_SESSION_KEY(trade-only Session Key signer, hex) andPOLYMARKET_WALLET_ADDRESS(the Deposit Wallet). CLOB API credentials come fromPOLYMARKET_CLOB_API_KEY/_SECRET/_PASSPHRASEwhen set; otherwise they are derived at startup through L1 auth and kept in memory only, never stored. OptionalPOLYMARKET_OWNER_ADDRESS: the loader refuses to start when the signer address equals it. The oldPOLYMARKET_PRIVATE_KEYname is refused with a message to rename it. - Data API reads need only the wallet address and work without credentials; open orders, cash and the User WS need credentials and report
credentials_unavailable(503) without them. - Valuation (liquidation value walking the bids, unrealized P&L) is computed in
engine/portfoliofrom positions and local books, coalesced to at most one update per second per position. - The tech lead’s own verification uses the fake venue and the golden vectors; it never uses the user’s real key from the geoblocked dev host (security F8). The user verifies with real credentials from a permitted location.
- Phase 1 UX leftovers for FE-Trading: event pages list open markets first and do not subscribe closed markets’ books; board league chips show only leagues with games in the window.
Phase 3: Trading (Predictions)
Goal: One-click trading with hard safety limits.
Exit gate:
- Security sign-off.
- Every live verification step passes with the audit log matching Polymarket’s records.
- All click budgets in §4 are met.
- Orders over the limit are rejected by the backend even if the UI is bypassed.
- Contract:
POST /orders/place,/orders/cancel,/orders/cancel-market,/orders/cancel-all,/orders/{id}/replace,/positions/exit,GET /trading,POST /trading/armand/trading/safe,GET|PUT /settings/risk; WS topictrading; Go types inbackend/internal/core/trading.go(OrderIntent,PlaceResult,RejectCode,core.Trading,core.Heartbeat,TradingState). - Order path: the browser mints a UUIDv7
client_order_idper intent.engine/orderswrites the intent to the audit log, asksengine/risk, writes the decision, calls the venue, and writes the response. A repeatedclient_order_idreturns the first result. A timeout isunknownand is reconciled from open orders and trades, never re-sent. - Risk (F6, F7, F10, F16): limits from settings, capped by env ceilings (
RISK_CEILING_*); raising a limit needs the UI password; order rate and duplicate-intent window; price band against a fresh book (reduce-only exits exempt, capped byexit_slippage_ticks); ARMED lives in memory, every start is SAFE, and it drops to SAFE on idle timeout, logout, geoblock, credential loss or foreign activity; clock skew against the venue’s server time. - Audit log (F15): intent, decision and response as separate rows keyed by
client_order_id; UPDATE and DELETE blocked by triggers; each row carries a hash chained to the previous one;task audit-verifychecks the chain. Never stores signatures or L2 headers. - Cancel All works in SAFE and when trading is otherwise off; the risk engine never blocks a cancel. A cancel during a sports delay window is queued and sent when the window ends.
- Exit:
walksells the whole position at the lowest bid withinexit_slippage_ticksof the best bid;joinrests a post-only sell at the best ask. Positions carryexit_priceandexit_completefromengine/portfolio. - Hedge (two-outcome markets):
POST /positions/hedgetakes only the client_order_id and the held token;engine/ordersbuys the other outcome’s size difference at the walk price on its asks (withinprice_band_ticksof the best ask) through the usual audit and risk path. Positions carry the same quote ashedge(orhedge_unavailable) fromengine/portfolio. - Signing: V2 order EIP-712 for both exchanges, signature type 3 with the ERC-7739 wrapper, and the session-key wrapper per docs/venue/polymarket.md §10. Golden vectors must match; the session-key wrapper has none, so it is built from the type-3 vectors plus the documented encoding and marked UNVERIFIED until live verification. go-ethereum stays (P2-10), recorded in ADR 0010.
- Security P2 items due now: P2-02(c) and P2-03 (session-signers check at credential start,
wallet_mismatch), P2-04 (open-orders drift check, foreign activity from the Data API, F5), F16 skew. - Live verification cannot run from the geoblocked dev host. The phase ends with everything built and tested against the fake venue and golden vectors, the security gate reviewed, and
docs/runbooks/live-verification.mdready for the user to run from a permitted location. - UI polish from the Phase 2 screenshots and the user’s layout feedback (
CLAUDE.md“Layout consistency”): one control height per row (mobile header: FAKE DATA badge, OFF switch and Cancel All), Positions/Activity tabs without the extra nested strip, search placeholder wrapping at 1440, “12,000.00 sh” wrapping in the portfolio size column, the mobile portfolio summary running off the edge, and average prices rounded for display.
Phase 4: Alerts and notifications
Exit gate:
- A price alert set from the ladder fires on desktop and on Telegram within 2 s of the trigger.
- Fills and game-start book wipes send notifications.
- Contract:
GET|POST /alerts,PUT|DELETE /alerts/{alert_id},GET /notifications(cursor paging),POST /notifications/read,POST /notifications/test,GET|PUT /settings/notifications; WS topicsalertsandnotifications. Error codetelegram_error(502); an unconfigured Telegram shows astelegram_configured: falsein the notification settings, not as an error. - Alert kinds:
price,spread,score,game_start,game_end. A price alert above fires when the best bid reaches the threshold (it can be sold there), below when the best ask reaches it (it can be bought there); the threshold sits on the market’s tick. A repeating alert re-arms after the value moves one tick back across. At most 200 alerts. Alerts persist in SQLite and survive a restart; on start they resume from the live books without firing on the first snapshot unless the condition newly holds. engine/alertsis one actor. It asksengine/booksto keep the books of active price and spread alerts subscribed, and reads games fromengine/games. It never fires on a stale book or game feed (§3.7).notifyis one actor that turns bus events intoNotifications. It writes each one to thenotificationstable throughstore.Writer, publishes it on thenotificationstopic, and fans out per the user’s rules: in-app toast, sound and desktop in the browser; Telegram from the backend. Kinds:alert,fill,order_rejected,order_unknown,game_start_orders,feed_down(after 30 s),foreign_activity,trading_safe,session_key_expiring(24 h ahead),credentials. History is kept 30 days.- Telegram is outbound only. It uses
TELEGRAM_BOT_TOKENandTELEGRAM_CHAT_IDfrom env (both or neither; a half-set pair fails startup),sendMessageas plain text with noparse_mode, and the token only in the request path, never logged or put in an error (the redactor learns it). It allows 20 messages a minute and coalesces the rest into one summary message. It retries once only on 429, honouringretry_after, and never after a timeout (a duplicate is worse than a gap). It never receives updates and has no webhook. - Desktop notifications use the Notification API, shown through a minimal same-origin service worker (
/sw.js) so they appear while the tab is in the background and focus the app on click. The worker has no fetch handler, no caching and no push subscription. Nothing is delivered when no tab is open; Telegram covers that. - Sounds are synthesised with Web Audio (no audio files), one short tone per severity, at
sound_volume, and play only after the first user gesture. - Alert creation takes one click from the ladder (a bell per level on hover, prefilled at that price and the side-appropriate direction) and from the board (game alerts in the row menu). The alerts screen lists, pauses, resumes and deletes them. No emoji anywhere (
CLAUDE.md“Icons and text”). - Telegram can be checked from this host once the user adds a bot token and chat ID to
.env. Until then it is tested against a local HTTP server.
Phase 5: Removed
Funding (deposits, withdrawals, bridge) and relayer operations (redeem, split, merge) are out of scope: the app only holds trade-only credentials (§1). Phase numbers after this are unchanged. Resolved positions are claimed on polymarket.com; in-app redeem could be added later only if the user provides credentials that allow it.Phase 6: Hardening and deployment
Exit gate:
- One-command deploy.
- The app is reachable over HTTPS behind login.
- The backup restore has been tested.
Phase 7: Removed
Polymarket Perps (perpetual futures) is out of scope at the user’s request (2026-09-30). Phase numbers after this are unchanged.Phase 8: Combos
Combo markets, the RFQ quote request and accept flow (decline is local), combo positions and activity, and the Combos exchange signing domain (Exchange V3, timestamps in seconds). UI: a leg builder from the board (“add leg” on any price), a quote timer, and combo positions. Phase 8 decisions (tech lead, at kickoff; research indocs/venue/polymarket.md §16):
- Contract:
POST /combos/quote,POST /combos/accept,GET /combos/rfqs(this app’s RFQs from the last 10 min),GET /portfolio/combos,GET /activity/combos; WS topicscombo_rfqsandcombo_positions(snapshot then deltas, as the other topics). Additions:Market.combo_eligible,Activity.combo,CredentialStatus.scopes,AccountSummary.combo_cost_basis,TradingState.combos_can_tradeandcombos_reasons, the risk limits below, notification kindscombo_filledandcombo_failed, the Error codecombos_unavailable(503) and the combo reject codes. Go types and theCombosandComboAccountcapabilities inbackend/internal/core/combos.go, plusOutcome.PositionID,Market.ComboEligible,Activity.Combo,CredentialStatus.ScopesandTradingState.CombosCanTrade/CombosReasons. - Venue API: the Combos Requester API (
https://combos-rfq-gateway-requester-api.polymarket.com/v1/requester/rfq:POST /requests,POST /requests/{rfq_id}/accept,GET /requests/{rfq_id}) with CLOB L2 auth only. No Builder API credentials: they would also authorise relayer calls, which the app must not hold (§1). The order’sbuilderfield is zero. The official SDK implements only the Builder Gateway, so the adapter calls the Requester API directly. - Namespace
pmc:for combo markets (pmc:<combo conditionId>) and combo positions (pmc:<combo positionId>). Legs staypm:outcomes. - Adapter at
backend/internal/venue/polymarket/combos/(the standard file names; nows.go, as there is no requester feed). Signing stays invenue/polymarket/signing: its allowlist gains the pair ("3", Exchange V30xe3333700cA9d93003F00f0F71f8515005F6c00Aa), V3 timestamps are in seconds, and the golden vectors intestdata/combos/signing_vectors.jsonmust match byte for byte. The session-key wrapper around the accept signature (§10) has no vector and stays UNVERIFIED until the live run. - Legs are keyed by Gamma
positionIdsonly (never CLOB token ids or condition ids). The browser sends leg token ids; the server maps each to its position id through the catalog. Eligibility comes from GammacomboStatus(enabledonly); the public RFQ catalog is not a source (§3.7). A market with an outcome whose position id cannot be resolved hascombo_eligiblefalse, and a quote request naming it is refused withcombo_ineligible. - Quote request (glossary: Combo, Leg, Quote, Accept, Decline, Expired, No quote): works in Safe.
engine/riskCheckQuoterefuses it withtrading_disabled(LIVE_TRADING off),geoblocked,no_credentials(credentials not ready),combos_unavailable(no combos scope),max_quotes_per_minute(a local cap of 12 per rolling minute, below the venue’s 15 per wallet;retry_after_mssays when the next is allowed),max_combo_legs,combo_ineligible,market_closed,combo_contradictory_legs(two outcomes of one market or one neg-risk event), and for a buymax_combo_notionalandmax_combo_exposureagainst the stake. It does not require Armed. The slip’s stake is the session’s active stake, shared with one-click orders; there is no separate combo stake. A quote request is never repeated: a lost answer isno_quotewithcombo_no_response, and there is nothing to Accept. The server checks that the venue’s answer echoes the request (legs, side, size) before it shows the quote (combo_quote_mismatch). - Accept needs Armed. The browser sends only
client_order_id,rfq_idandquote_id; the server signs the amounts of the quote it holds (maker_amount_e6andtaker_amount_e6copied unchanged).engine/riskchecks every gate of the quote request except the quote cap, thenmax_orders_per_minute(an Accept counts as an order), and for a buymax_combo_notional,max_combo_exposureandmax_daily_notionalagainsttotal_required_e6(fee included), and for a sell thattotal_required_e6shares are held (no_position). Risk never usesmakerAmount, since the fee sits outside the signed order. A quote is sent for Accept at most once (combo_already_accepted). - Accept outcome: an unknown Accept (timeout, lost answer, 5xx, 503, 409
INVALID_RFQ_STATE) is reconciled by a status read of thatrfq_idand never re-sent, overruling the venue docs’ “safely retry” (CLAUDE.md). A status 409 means the Accept was not recorded:venue_not_found. After an Accept the status is polled every 500 ms untilCONFIRMEDorFILLED(filled, withtx_hash) orFAILED,CANCELED,EXPIRED,REJECTEDor an unknown status (not filled).combo_filledorcombo_failed(titlesCombo filled,Combo rejected,Combo failed) is notified once per Accept, withNotification.client_order_id(Phase 4 contract) set to the Accept’sclient_order_id. - Decline is local only: there is no venue call. The UI dismisses the quote and the server forgets it when it expires. Cancel all also declines a live quote in the app (frontend only; it cannot stop an Accept in flight). Timer:
ComboQuote.expires_in_msis computed by the server when it sends each response or WS frame, and the browser counts down from it starting when the message arrives, never from its own clock againstexpires_at.expires_at(the venue’s deadline, corrected by the measured venue clock skew; the docs give 5 s and 10 s windows, the answer rules) stays for display and audit. The server refuses an Accept at or afterexpires_atwithout a venue call; the UI disables Accept under 1 s left. - No indicative product price: the slip shows each leg’s live price from its book and no combined price until a quote arrives.
- Combos need a signer with the
COMBOSRFQorALLscope, alongsideCLOB: a key scopedCOMBOSRFQalone fails the CLOB credential check, so the recommended key is["CLOB","COMBOSRFQ"](P8-11). The app’s current key isCLOBonly; until the credential status reports the combos scope, quote requests and Accepts are refused withcombos_unavailableandcombos_reasonslists it. The combo endpoints answer 503combos_unavailableonly when no Combos venue is configured. Reading combo positions and activity needs only the wallet address. - Combo positions:
/v2/positions/combosevery 60 s and after a fill, valued at cost. Statusesopen,won,lost,partialplusredeemable; the venue’sREDEEMABLEmaps to won andRESOLVED_PARTIALto partial, both to be confirmed in the live run.AccountSummary.combo_cost_basissums the open ones and counts in exposure (liquidation value plus combo cost), but not in liquidation value or unrealised P&L. Exit is a sell quote for the position’s size; payouts are claimed on polymarket.com. - Foreign activity:
/v2/activityrows withis_comboare matched to this app’s accepts bytx_hash. Before Phase 8, such a row would be flagged foreign; the check must learn it before combos go live. - Exchange V3 approvals (pUSD
approvefor buys, PositionManagersetApprovalForAllfor sells) are set by the user on polymarket.com; a missing one comes back asvenue_balance. - Risk limits, defaults (the user confirms them at the end of the phase, with gate 5):
max_combo_notional5,000 pUSD,max_combo_exposure25,000 pUSD (at leastmax_combo_notional),max_combo_legs10, each capped by an env ceiling (RISK_CEILING_COMBO_NOTIONAL,RISK_CEILING_COMBO_EXPOSURE,RISK_CEILING_COMBO_LEGS; proposed defaults 25,000, 100,000 and 50) and raised only with the UI password, as the other limits. Until the0016columns are wired,GET /settings/riskreports the defaults as both limits and ceilings. - Migrations start at
0016(Phase 4 took 0014 and 0015); the Phase 8 columns are in §3.5. - Everything is built and tested against the fake venue; the live checks in §16 “Fixtures still needed” run from a permitted location with a key scoped
["CLOB","COMBOSRFQ"]or["ALL"].
Phase 9: Yeet
Implement thecore capabilities for Yeet’s API once its docs arrive. The Venue boundary and namespaced IDs mean the UI gains venue filters but no structural changes.
6. Running the team
Each phase runs roughly like this:- Kickoff: the tech lead writes the phase’s contract diff and task briefs, one per role.
- Parallel build: the roles work in separate worktrees at the same time. Each opens a PR per piece of work.
- Review: the DevOps+Quality agent runs lint/format/typecheck. The UX designer reviews FE PRs, and the security reviewer reviews sensitive PRs.
- Merge: the tech lead merges in contract → backend → frontend → docs order.
- Exit gate: QA runs the acceptance checklist, the tech lead reports to the user, and the next phase starts.
CLAUDE.md format (no AI names or co-author trailers). Each PR is registered with this thread.