Skip to main content

Runbook: live verification (Phase 3)

This runbook is for the account owner. It is the script to run, with real money and minimum sizes only, before trusting sesame-bun with a working balance. It checks that orders are signed, placed, moved and cancelled correctly, that the risk limits hold, and that the audit log matches Polymarket’s records. Passing every step is part of the Phase 3 exit gate (PLAN §5, Phase 3). The Combos rows of step 16 (16.33 to 16.45) and pre-flight PF5 are the Phase 8 live checks (PLAN §5, Phase 8). Run it only from a location where you are permitted to trade on Polymarket. The development server is in a blocked location and cannot run it; the app never works around the geoblock (ADR 0005). Related documents: Phase 3 is being built while this runbook is written. Where the app’s behaviour is not fixed yet, this runbook says defined by implementation. Where a screen differs from the description, follow the API check given with each step; the API is the reference.

1. Prerequisites

Do not place a live order until every item here is true. Setup of the machine itself (Go, Node, Task, .env) is in dev-setup.md. Never set SESAME_FAKE_VENUE=1 for this run; the loader refuses it together with LIVE_TRADING=true.

1.1 Risk limits for the run

The app’s defaults are sized for a 100kto100k to 500k bankroll (ADR 0013 stage 2), far above this run. The table lists them. Lower both the settings and the ceilings to the run values before step 3:
  1. The ceilings, in .env, before the first start (pre-flight PF3). Set the eight RISK_CEILING_* variables from .env.example to the run values in the table. The app reads them only at start, so a change needs a restart. The effective limit is the lower of the setting and its ceiling, and no setting can be raised above its ceiling.
  2. The settings, in the app, before step 3. Open Settings, then Risk, and set every limit in the table. Lowering a limit needs no password; raising one asks for the UI password.
Check both: GET /api/settings/risk shows each limit and its ceiling at the run value. A data directory created before these defaults keeps any limit you changed; only limits still at the old defaults (25,25, 100, $250, 20 open orders, 30 a minute) moved to the new ones. Amounts in .env are pUSD without the $ sign (for example RISK_CEILING_ORDER_NOTIONAL=4). Restoring them after the run is in section 6. Stake presets: the defaults are 1,000,1,000, 2,500, 5,000,5,000, 10,000 and 25,000.Replacethem∗∗before∗∗youlower‘maxordernotional‘:setpreset1to∗∗25,000. Replace them **before** you lower `max_order_notional`: set preset 1 to **3** and remove the others or set them to 4orless.Settingscheckseachpresetagainstthesaved‘maxordernotional‘(securityF7),andapresetabove4 or less. Settings checks each preset against the saved `max_order_notional` (security F7), and a preset above 4 would only give rejected orders during the run. A buy of $3 at a price p is 3 ÷ p shares, so it meets a 5-share minimum at any price up to 0.60. Gate item 22 asks for first-run limits “just above the minimum order size”; max_daily_notional is set higher here because this runbook places about 15 orders.

1.2 Pick the markets

Choose these before you start and note their ids (pm:<conditionId>) in the results table. GET /api/markets/{market_id} shows tick_size, min_size, neg_risk, accepting_orders and seconds_delay.

1.3 Capture the logs

Start the app with its output saved to a file inside data/, which git ignores:
LOG_LEVEL=debug adds detail and is still redacted. Never share .env. Before sharing a log with anyone, run the secret check in section 3. Keep polymarket.com open, signed in to the account, on any device. It is the reference for every “matches polymarket.com” check.

1.4 Pre-flight

Do these on the day of the run, in order. PF1 to PF3 come before the app’s first start on the live host. Do not arm until PF1 to PF4 are done and recorded in the results file (section 2), and do not start the Combos rows (16.33 to 16.45) until PF5 is done too. They meet conditions C1, C2 and C4 of the Phase 3 security recheck. If one is not met, the run does not start. PF1. Gate 5 is closed in ADR 0012 (C1). A Session Key is not trade-only on-chain: only Polymarket’s relayer stands between a leaked key and a transfer out of the Deposit Wallet (ADR 0012). Open the ADR’s Gate 5 decision section and check that it records one of these, with a date and its evidence:
  1. a written statement from Polymarket that the relayer refuses session-signed batches other than session-key management;
  2. the result of the owner’s one-off relayer test: a session-signed transfer of 1 base unit of pUSD, sent from a permitted location outside this app with the official SDK, which the relayer refused;
  3. a written acceptance by the user and the tech lead that a leaked key can lose the whole Deposit Wallet (Accepted risk). It holds only while PF2 holds, and only for an attended run.
Record which of the three it is. If the section still reads “pending the user”, the run does not start. PF2. The wallet is within the cap (C2). Stage 1 of ADR 0013 is Accepted and names the cap. On polymarket.com, add the cash and, for each open position, its shares times its average price. The total is at or below the cap. Record the cash, the position cost and the total. If the total is above the cap, bring it down on polymarket.com before the app’s first start; after the start, a withdrawal is reported as foreign activity (PF4). The cap holds for as long as the Session Key is authorised, until ADR 0013 accepts stage 2, not only for the run (section 6). PF3. No open orders at the first start (C4). At its first start (a new data directory with an empty audit log), the app adopts every order the Session Key has open as its own and never reports it as foreign activity (security finding P3R-04). It logs each adopted order at Warn as baseline order adopted with its orderId, then the total as baseline orders adopted with orders, and writes one baseline audit row. Until the next arm, GET /api/trading shows baseline_at and baseline_orders, the count adopted. A start that finds audit history adopts nothing and logs baseline not adopted. Before the first start:
  1. On polymarket.com, cancel every open order on the account.
  2. Start the app with its log captured (section 1.3).
  3. In the start log, check that the line baseline orders adopted shows orders=0 and that no baseline order adopted line appears. GET /api/trading shows baseline_orders: 0.
If orders is above 0, any baseline order adopted line appears, or the lines are missing, stop the app, do not arm, and report the log lines to the tech lead (section 3, items 4 to 6). PF4. Your own actions on polymarket.com. From the first start, the app reports as foreign activity every trade it did not place, and every withdrawal, split, merge, conversion and outgoing transfer on the account, including your own on polymarket.com (security finding P3R-02). Redeems and incoming transfers are not reported. On foreign activity the app switches to SAFE, GET /api/trading reasons shows foreign_activity, and arming again needs the UI password: the switch asks for it, or send it as password in the body of POST /api/trading/arm. During the run, do none of these on polymarket.com except where a step asks for it (the step 9 fallback sell, row 16.22). If you see foreign_activity after something you did not do, follow the incident runbook instead of arming. PF5. Combos gate (rows 16.33 to 16.45). Before any quote request is sent with real credentials, every item below holds. They are the gate items G1 to G10 of the Phase 8 security review. Record each in the results file with the date, the build (git rev-parse HEAD) and the evidence. If one does not hold, skip every Combos row; the rest of the run is not affected. G1 to G3, G6, G9 and G10 are checked before arming; G4, G5, G7 and G8 are checked by the rows named and stop the Combos run when they fail. Smallest stake. When you reach the Combos rows after step 15, find the smallest stake the market makers quote (raising max_order_notional earlier would change step 10). Polymarket documents no minimum combo size (venue notes §16, A17). While Safe (a quote trades nothing), pick two legs of different games with comboStatus enabled, turn on combo mode, and press Get quote at the 3stake.Iftheslipreads‘Noquote⋅nomarketmakerquoted‘,tryoncemoreontwootherlegs.Ifthereisstillnoquote,raisethestakeonestepatatime(3 stake. If the slip reads `No quote · no market maker quoted`, try once more on two other legs. If there is still no quote, raise the stake one step at a time (5, then 10)andstopatthefirststakethatisquoted.Eachstepneedsthestakepreset,‘maxordernotional‘(thepresetsarecappedbyit)and‘maxcombonotional‘raisedtothenewstake,‘maxcomboexposure‘totwiceit,theirceilingsin‘.env‘likewise(arestart),andtheUIpassword;cashpluspositioncostmuststaywithintheADR0013cap(PF2).Donotgoabove10) and stop at the first stake that is quoted. Each step needs the stake preset, `max_order_notional` (the presets are capped by it) and `max_combo_notional` raised to the new stake, `max_combo_exposure` to twice it, their ceilings in `.env` likewise (a restart), and the UI password; cash plus position cost must stay within the ADR 0013 cap (PF2). Do not go above 10: record “no quote at $10 or less” and skip the rows that need a quote. Record the smallest stake quoted; rows 16.33 and 16.37 use it. Each request counts toward the 12 quote requests a minute.

2. Recording results

Copy the results table into a new file, docs/qa/live-verification-<YYYY-MM-DD>.md (the QA engineer owns docs/qa/; the tech lead merges it), and fill it in as you go. For each step record:
  • Pass or Fail.
  • Evidence: the client_order_id and venue order id of every order you placed, the reject code shown, and the time (UTC).
  • Anything unexpected, even when the step passes.
Never put a secret, a cookie, a CSRF token or a screenshot showing .env into the results. Order ids, token ids and addresses are public and fine to record.

3. If a step fails

Stop at the first failure. Do not repeat a failing order step to “see if it works now”: a second attempt can leave a second order on the book.
  1. Stop trading. Switch to SAFE (the header switch, or POST /api/trading/safe).
  2. Clear the book. Press Cancel All (Mod+Shift+X, where Mod is Ctrl on Windows and Linux). Check on polymarket.com that no order from this run is still open. If Cancel All itself fails, cancel the orders on polymarket.com.
  3. Turn trading off. Set LIVE_TRADING=false and restart the app.
  4. Record. Write down the step, what you did, what you expected, what happened, the client_order_id and order id, and the time.
  5. Collect the logs without secrets. Stop the app so the log file is complete, then check that the log holds none of the secret values from .env. This PowerShell snippet reads the values from .env and prints only whether one was found, never the value:
    The check covers only values set in .env. CLOB credentials derived at startup are not in .env; the redacting logger strips them, but read the lines you share. Share only the lines around the failure, and never the .env file.
  6. Report. Send the record and the log lines to the tech lead. If a secret was found in the log, or anything suggests the key is exposed, follow the incident runbook instead.
Resume from the failed step only after the cause is found and fixed, on a new build, with the sign-off still covering it.

4. Steps

“Far from the market” in these steps means a buy at least 3 ticks below the best bid and within price_band_ticks (10) of it: far enough not to fill, near enough to pass the price band. Unless a step says otherwise, orders are placed on market A with the $3 stake. Every step lists what to do, what to expect, and an API check. The API checks are GET requests you can open in the same browser tab where you are logged in, for example http://localhost:5173/api/trading. Unresolved placements. GET /api/trading has unresolved_placements: the number of placements whose outcome is unknown and not yet settled (security finding P3R-07). The app looks each one up at the venue by its signed order id 2 to 30 seconds after the unknown outcome. If the count is still above 0 after that, the venue has not answered the lookup; the app asks again every 60 seconds and never re-sends the order. A buy keeps its notional reserved against max_position_notional meanwhile. It should be 0 between steps. If it stays above 0, treat it as a failed step (section 3) and see the incident runbook.

Step 1. Startup

  1. Start the app (section 1.3), or keep it running from pre-flight PF3, and log in.
  2. Check the status: GET /api/status.
    • geoblock.blocked is false and geoblock.checked_at is within the last few minutes.
    • trading_enabled is true.
    • feeds lists pm.market, pm.sports and pm.user, each connected: true.
  3. Check the credentials: GET /api/account.
    • credentials.state is ready. It is not failed with reason wallet_mismatch, which means the Session Key is not listed as a signer of the configured wallet (security P2-03).
    • credentials.signer_address is the Session Key’s address, not the owner’s.
    • wallet_address is the Deposit Wallet.
  4. Check the trading state: GET /api/trading.
    • armed is false: every start is SAFE.
    • clock_skew_ms is between -2000 and 2000.
    • reasons holds only not_armed. Any other code means a gate is closed; see the reject codes.
    • unresolved_placements is 0 (see section 4).
Pass: all of the above. The startup log also shows the Session Key’s expiry from the session-signers check; the exact line is defined by implementation. Record the expiry date.

Step 2. Account figures match polymarket.com

Compare the app (top bar, Portfolio, Orders) with polymarket.com:
  • Cash matches the pUSD balance.
  • Positions: the same outcomes, sizes and average prices.
  • Open orders: the app shows only orders placed by this Session Key. Orders placed on polymarket.com or by another key do not appear. That is expected: a Session Key sees only its own orders and trades.
  • P&L: the P&L chart has the same shape as the profile chart (step 16 item 9).
Pass: cash and positions match; the open orders are exactly this key’s orders (none, on a first run).

Step 3. Arm

  1. Click the SAFE/ARMED switch in the header.
  2. The switch shows ARMED and the top bar gets its ARMED marker.
  3. GET /api/trading: armed: true, can_trade: true, reasons: [], and armed_until about arm_idle_minutes from now.
If arming is refused (409 not_armable), reasons lists why. Do not continue until it is empty apart from not_armed. Pass: ARMED, can_trade: true.

Step 4. A GTC buy far from the market

  1. Open market A’s ladder. Note the best bid.
  2. Click the Bid cell 3 ticks below the best bid.
  3. The order chip shows sending, then placed, at that level.
  4. Orders: one open order with side buy, the clicked price, original_size equal to 3 ÷ price in shares (rounding is defined by implementation), order_type GTC, status live, and a client_order_id.
  5. polymarket.com shows the order in the account’s open orders. Whether the site shows orders placed by a Session Key is UNVERIFIED; record what you see.
  6. Neg-risk exchange: repeat 1 to 4 on market B, then leave that order open for step 7.
Pass: both orders are live at the clicked price and size. Record both client_order_id values and order ids. The market B order proves the Neg Risk exchange signature; the market A order proves the CTF exchange signature.

Step 5. Replace by one tick

  1. On market A, select the order from step 4 (click its chip) and press ]. On a phone, use the + stepper in the order bar.
  2. The chip shows moving, then placed one tick higher.
  3. Orders: exactly one open order on market A, at the new price, with a new order id. The old order id is cancelled.
The backend cancels the old order and, once the cancel is confirmed, places the new one (POST /orders/{order_id}/replace). If the cancel fails, nothing new is placed. Pass: one open order, one tick higher, new id.

Step 6. Cancel

  1. Right-click the order’s chip on the ladder (long-press on a phone).
  2. The chip disappears; Orders no longer lists it.
  3. polymarket.com no longer shows it.
Pass: the order is cancelled in the app and on polymarket.com.

Step 7. Cancel-market

  1. On market A, place two buys far from the market at two different levels.
  2. Check that the market B order from step 4 is still open.
  3. Press Cancel (2) in market A’s ladder header (or C with the ladder focused).
  4. Both market A orders are cancelled in one action; the result toast says so.
  5. The market B order is still open.
Pass: market A has no open orders; market B’s order is untouched.

Step 8. Cancel All

  1. Place one more buy far from the market on market A, so there are orders on two markets.
  2. Switch to SAFE.
  3. Press Mod+Shift+X (or the top-bar Cancel All).
  4. Every open order is cancelled, although the app is in SAFE. The result toast gives the count.
  5. Orders is empty; polymarket.com shows no open order from this run.
Pass: all orders cancelled while SAFE. Cancel All must never be blocked by the risk engine or by SAFE.

Step 9. One small fill, then exit

  1. Arm again.
  2. On market A, check that the best ask has at least 10 shares.
  3. Buy the $3 stake at the best ask: press B with the ladder focused, or click the Bid cell at the best ask’s level. This is still a GTC limit order at that price; it fills against the ask.
  4. Expect: a fill toast; the order’s status matched; a fill in today’s fills with its price, size, value and role taker; within a few seconds a position in Portfolio; cash lower by the fill value plus any fee.
  5. In Portfolio, press the row’s Exit button (the walk price is on the button), or select the row and press E.
  6. Expect: a sell fill toast; the position closes (or leaves dust below the minimum size); cash comes back less the spread and fees.
If Exit is rejected with min_size (a position smaller than the minimum after fees) or venue_balance (see step 16 item 20), record it and sell the position on polymarket.com. Pass: the buy fills, the position appears, the exit sells it. Record both fills’ trade ids, prices, sizes and fees.

Step 10. Risk rejections

The UI shows each rejection as a toast naming the order and the reason; nothing reaches the venue. 10a. Over max order notional.
  1. In Settings, Risk, lower max_order_notional to $2.
  2. Click a bid far from the market with the $3 stake.
  3. Expect a rejection with code max_order_notional. No order appears.
  4. If Settings refuses to save because a stake preset is above the new limit (presets are validated against max_order_notional, security F7), skip 1 to 3 and rely on 10d.
10b. Price band.
  1. Click a bid 15 ticks below the best bid (beyond price_band_ticks).
  2. Expect a rejection with code price_band.
10c. Duplicate within the window.
  1. Click a bid far from the market once.
  2. Wait about half a second (longer than the 350 ms double-click guard), then click the same cell again.
  3. Expect: the first click places an order; the second is rejected with duplicate_intent.
  4. Cancel the order from the first click.
10d. The UI is bypassed. This sends an order straight to the API, as a script would, with a valid session and CSRF token. It checks that the backend enforces the limits (exit gate: “orders over the limit are rejected by the backend even if the UI is bypassed”). max_order_notional must still be 2(or2 (or 4 if 10a was skipped; then use size “20” so the notional is above $4).
  • The token id is in GET /api/markets/{market_id} under outcomes[].token_id.
  • Expect HTTP 200 with status: "rejected" and reject.code: "max_order_notional".
  • The Origin value must be the app’s public origin; in development it is the Vite address above. The exact allowlist is defined by implementation.
  • Logging in from the script starts a new session. Log out afterwards (POST /api/auth/logout with the same headers), or leave it to expire.
10e. Step-up and ceilings.
  1. In Settings, raise max_order_notional back to $4 without entering the password. Expect a refusal (403 step_up_required).
  2. Raise it with the password. Expect it to save.
  3. Try to set it above its RISK_CEILING_* value. Expect a refusal (400).
Pass: each rejection has the expected code, and no rejected order reached the venue (nothing new on polymarket.com).

Step 11. A double-click creates one order

  1. Double-click quickly (both clicks within 350 ms) on a bid far from the market.
  2. Orders shows one new order. polymarket.com shows one.
  3. Cancel it.
Optional: send the 10d request twice with the same client_order_id and a size within the limits (for example size “5” at a price far from the market). Both responses are identical and only one order exists. Cancel it. Pass: one order per intent.

Step 12. Idle to SAFE

  1. In Settings, Risk, set arm_idle_minutes to 1.
  2. Arm. Do nothing in the app for 70 seconds.
  3. The switch drops to SAFE on its own. GET /api/trading shows armed: false.
  4. Click a bid: nothing is sent (the cursor tag reads SAFE: arm to trade).
  5. Set arm_idle_minutes back to 10 (this raises it, so the password is asked).
Pass: the app returned to SAFE after one idle minute.

Step 13. The heartbeat on, then off

The heartbeat is Polymarket’s dead-man switch: while it runs, the venue cancels this key’s open orders about 10 to 15 seconds after heartbeats stop (venue notes §7). To see it work, the app must stop without cancelling its own orders, so this step turns cancel_on_shutdown off (the password is asked, because it turns off a safety setting). Heartbeat on:
  1. In Settings, Risk: heartbeat_enabled on, cancel_on_shutdown off.
  2. Arm and place a buy far from the market. Note its order id.
  3. Stop the app with Ctrl+C in its terminal.
  4. Wait 30 seconds.
  5. On polymarket.com (or after restarting the app, in Orders) the order is gone.
Heartbeat off:
  1. Restart the app, log in. In Settings, Risk: heartbeat_enabled off.
  2. Arm and place a buy far from the market.
  3. Stop the app with Ctrl+C. Wait 30 seconds.
  4. Restart the app and log in. Orders still shows the order as open; polymarket.com agrees.
  5. Cancel it. Set cancel_on_shutdown back on.
Pass: with the heartbeat on, the venue cancelled the order after the app stopped; with it off, the order stayed.

Step 14. A restart comes back SAFE

  1. Arm. Place a buy far from the market.
  2. Stop the app with Ctrl+C (cancel_on_shutdown is on).
  3. Restart the app and log in.
  4. GET /api/trading shows armed: false. The header shows SAFE.
  5. The order is gone: the app cancelled it at shutdown. polymarket.com agrees.
Pass: SAFE after restart, and no order left behind.

Step 15. Audit log

  1. Stop the app.
  2. Run task audit-verify. It checks the order_audit hash chain and reports the result. Whether it needs the app stopped is defined by implementation; stopping it is always safe.
  3. Expect it to pass.
  4. Compare the audit log with Polymarket’s records. How to list the rows (a task, a screen or a query) is defined by implementation. For every client_order_id in your results table:
    • accepted orders have an intent, a decision, a signed and a response row (ADR 0011). The signed row’s order id equals the response’s, and matches polymarket.com’s history with the same side, price and size;
    • orders that filled or were cancelled also have an ended row;
    • rejected intents (step 10) have an intent and a decision row, and no signed or response row;
    • the log has exactly one baseline row, from the first start (PF3);
    • no order on polymarket.com from this run is missing from the audit log.
Pass: task audit-verify passes and every order matches.

Step 16. UNVERIFIED venue items

These items in the Polymarket venue notes could not be checked from the development server. Most are answered by the steps above; the table says which. Record each answer in the results table (rows 16.1 to 16.46) as confirmed, different (describe what you saw) or not seen. The BE-Venue engineer then updates the venue notes and removes the UNVERIFIED mark. The venue notes’ Phase 3 section is being written with the order signing code. The Phase 3 rows below come from the plan and venue notes §5 to §10; the final list of Phase 3 items is defined by implementation. Add any item the Phase 3 section lists that is not here. From venue notes §14 (Phase 2 account reads): From venue notes §5 to §10 and the Phase 3 plan: From venue notes “Sports channel” (cricket): This row needs no credentials and no trading. Run it whenever a listed cricket game is live, on the day of the run or on its own. Row 16.29 snippet. The request is read-only and needs no credentials:
From venue notes §16 (Phase 8 Combos): Run these rows after step 15, once the PF5 gate holds. They need a Session Key authorised with ["CLOB","COMBOSRFQ"] (preferred) or ["ALL"]. With a CLOB-only key every quote request is refused combos_unavailable before anything is sent: record that under row 16.40 and skip the other Combos rows. Use the smallest stake PF5 found. A quote trades nothing and works in Safe; only an Accept needs Armed. The rows are numbered by topic. Run them in this order, since later rows use earlier results: 16.41, 16.33, 16.36, 16.45, 16.37, 16.38, 16.40, 16.42, 16.43, 16.34, 16.35, 16.39, 16.44. Rows 16.33, 16.35, 16.37 and 16.38 replace fixtures that are documented examples, not captures: each listed file in backend/internal/venue/polymarket/testdata/combos/ is marked UNVERIFIED and TEMPORARY in its SOURCES.txt. Rows 16.34 and 16.42 add new captures. Keep each answer’s exact body bytes; the capture method is defined by implementation, as for row 16.28. The bodies carry the wallet address and RFQ ids but no credential. BE-Venue commits each capture byte for byte, replaces the docs file, and removes its UNVERIFIED mark.

5. Results table

Copy this into docs/qa/live-verification-<YYYY-MM-DD>.md.

6. After the run

  1. Press Cancel All and check polymarket.com shows no open order from the run.
  2. Switch to SAFE.
  3. Restore the risk limits from section 1.1 in reverse order: first set the eight RISK_CEILING_* values in .env (the five order ceilings and the three RISK_CEILING_COMBO_*) back to the values you use for normal operation (or delete the lines to get the defaults) and restart, then raise the settings in Settings, then Risk, including max_combo_notional, max_combo_exposure and max_combo_legs (gate G9). Each raise asks for the password and cannot pass its ceiling. Raise them one at a time, and keep them small until you have traded with the app for a while. Set the stake presets last: no preset can be above the saved max_order_notional.
  4. If you are not trading straight away, set LIVE_TRADING=false and restart.
  5. Keep cash plus the cost of open positions, combo positions included, at or below the stage 1 cap in ADR 0013 for as long as the Session Key is authorised, until ADR 0013 accepts stage 2. If the wallet goes above the cap before then, or went above it during the run, revoke the key (Session Key setup §6).
  6. Send the results file and the checked log lines to the tech lead. The phase passes its exit gate when every step passes and the audit log matches Polymarket’s records.