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:- User guide: what each control does.
- Incident runbook: what to do when something looks wrong.
- Session Key setup: creating and configuring the signer.
- Polymarket venue notes: the UNVERIFIED items step 16 checks.
- Security review checklist §2: the live-trading gate.
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 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:- The ceilings, in
.env, before the first start (pre-flight PF3). Set the eightRISK_CEILING_*variables from.env.exampleto 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. - 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.
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 (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 2,500, 10,000 and 3** and remove the others or
set them to 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 insidedata/, 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:- a written statement from Polymarket that the relayer refuses session-signed batches other than session-key management;
- 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;
- 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.
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:
- On polymarket.com, cancel every open order on the account.
- Start the app with its log captured (section 1.3).
- In the start log, check that the line
baseline orders adoptedshowsorders=0and that nobaseline order adoptedline appears.GET /api/tradingshowsbaseline_orders: 0.
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 5, then 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_idand venue order id of every order you placed, the reject code shown, and the time (UTC). - Anything unexpected, even when the step passes.
.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.-
Stop trading. Switch to SAFE (the header switch, or
POST /api/trading/safe). -
Clear the book. Press Cancel All (
Mod+Shift+X, whereModis 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. -
Turn trading off. Set
LIVE_TRADING=falseand restart the app. -
Record. Write down the step, what you did, what you expected, what happened, the
client_order_idand order id, and the time. -
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.envand 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.envfile. - 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.
4. Steps
“Far from the market” in these steps means a buy at least 3 ticks below the best bid and withinprice_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
- Start the app (section 1.3), or keep it running from pre-flight PF3, and log in.
- Check the status:
GET /api/status.geoblock.blockedisfalseandgeoblock.checked_atis within the last few minutes.trading_enabledistrue.feedslistspm.market,pm.sportsandpm.user, eachconnected: true.
- Check the credentials:
GET /api/account.credentials.stateisready. It is notfailedwith reasonwallet_mismatch, which means the Session Key is not listed as a signer of the configured wallet (security P2-03).credentials.signer_addressis the Session Key’s address, not the owner’s.wallet_addressis the Deposit Wallet.
- Check the trading state:
GET /api/trading.armedisfalse: every start is SAFE.clock_skew_msis between -2000 and 2000.reasonsholds onlynot_armed. Any other code means a gate is closed; see the reject codes.unresolved_placementsis 0 (see section 4).
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).
Step 3. Arm
- Click the SAFE/ARMED switch in the header.
- The switch shows ARMED and the top bar gets its ARMED marker.
GET /api/trading:armed: true,can_trade: true,reasons: [], andarmed_untilaboutarm_idle_minutesfrom now.
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
- Open market A’s ladder. Note the best bid.
- Click the Bid cell 3 ticks below the best bid.
- The order chip shows
sending, thenplaced, at that level. - Orders: one open order with
sidebuy, the clickedprice,original_sizeequal to 3 ÷ price in shares (rounding is defined by implementation),order_typeGTC,statuslive, and aclient_order_id. - 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.
- Neg-risk exchange: repeat 1 to 4 on market B, then leave that order open for step 7.
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
- On market A, select the order from step 4 (click its chip) and press
]. On a phone, use the+stepper in the order bar. - The chip shows
moving, thenplacedone tick higher. - Orders: exactly one open order on market A, at the new price, with a new order id. The old order id is cancelled.
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
- Right-click the order’s chip on the ladder (long-press on a phone).
- The chip disappears; Orders no longer lists it.
- polymarket.com no longer shows it.
Step 7. Cancel-market
- On market A, place two buys far from the market at two different levels.
- Check that the market B order from step 4 is still open.
- Press Cancel (2) in market A’s ladder header (or
Cwith the ladder focused). - Both market A orders are cancelled in one action; the result toast says so.
- The market B order is still open.
Step 8. Cancel All
- Place one more buy far from the market on market A, so there are orders on two markets.
- Switch to SAFE.
- Press
Mod+Shift+X(or the top-bar Cancel All). - Every open order is cancelled, although the app is in SAFE. The result toast gives the count.
- Orders is empty; polymarket.com shows no open order from this run.
Step 9. One small fill, then exit
- Arm again.
- On market A, check that the best ask has at least 10 shares.
- Buy the $3 stake at the best ask: press
Bwith 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. - Expect: a fill toast; the order’s status
matched; a fill in today’s fills with its price, size,valueandroletaker; within a few seconds a position in Portfolio; cash lower by the fill value plus any fee. - In Portfolio, press the row’s Exit button (the walk price is on the button), or
select the row and press
E. - Expect: a sell fill toast; the position closes (or leaves dust below the minimum size); cash comes back less the spread and fees.
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.- In Settings, Risk, lower
max_order_notionalto $2. - Click a bid far from the market with the $3 stake.
- Expect a rejection with code
max_order_notional. No order appears. - 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.
- Click a bid 15 ticks below the best bid (beyond
price_band_ticks). - Expect a rejection with code
price_band.
- Click a bid far from the market once.
- Wait about half a second (longer than the 350 ms double-click guard), then click the same cell again.
- Expect: the first click places an order; the second is rejected with
duplicate_intent. - Cancel the order from the first click.
max_order_notional must still be 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}underoutcomes[].token_id. - Expect HTTP 200 with
status: "rejected"andreject.code: "max_order_notional". - The
Originvalue 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/logoutwith the same headers), or leave it to expire.
- In Settings, raise
max_order_notionalback to $4 without entering the password. Expect a refusal (403step_up_required). - Raise it with the password. Expect it to save.
- Try to set it above its
RISK_CEILING_*value. Expect a refusal (400).
Step 11. A double-click creates one order
- Double-click quickly (both clicks within 350 ms) on a bid far from the market.
- Orders shows one new order. polymarket.com shows one.
- Cancel it.
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
- In Settings, Risk, set
arm_idle_minutesto 1. - Arm. Do nothing in the app for 70 seconds.
- The switch drops to SAFE on its own.
GET /api/tradingshowsarmed: false. - Click a bid: nothing is sent (the cursor tag reads
SAFE: arm to trade). - Set
arm_idle_minutesback to 10 (this raises it, so the password is asked).
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 turnscancel_on_shutdown off (the password is asked, because it turns off a safety setting).
Heartbeat on:
- In Settings, Risk:
heartbeat_enabledon,cancel_on_shutdownoff. - Arm and place a buy far from the market. Note its order id.
- Stop the app with Ctrl+C in its terminal.
- Wait 30 seconds.
- On polymarket.com (or after restarting the app, in Orders) the order is gone.
- Restart the app, log in. In Settings, Risk:
heartbeat_enabledoff. - Arm and place a buy far from the market.
- Stop the app with Ctrl+C. Wait 30 seconds.
- Restart the app and log in. Orders still shows the order as open; polymarket.com agrees.
- Cancel it. Set
cancel_on_shutdownback on.
Step 14. A restart comes back SAFE
- Arm. Place a buy far from the market.
- Stop the app with Ctrl+C (
cancel_on_shutdownis on). - Restart the app and log in.
GET /api/tradingshowsarmed: false. The header shows SAFE.- The order is gone: the app cancelled it at shutdown. polymarket.com agrees.
Step 15. Audit log
- Stop the app.
- Run
task audit-verify. It checks theorder_audithash chain and reports the result. Whether it needs the app stopped is defined by implementation; stopping it is always safe. - Expect it to pass.
- 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_idin your results table:- accepted orders have an intent, a decision, a
signedand 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
endedrow; - rejected intents (step 10) have an intent and a decision row, and no signed or response row;
- the log has exactly one
baselinerow, from the first start (PF3); - no order on polymarket.com from this run is missing from the audit log.
- accepted orders have an intent, a decision, a
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:
["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 intodocs/qa/live-verification-<YYYY-MM-DD>.md.
6. After the run
- Press Cancel All and check polymarket.com shows no open order from the run.
- Switch to SAFE.
- 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 threeRISK_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, includingmax_combo_notional,max_combo_exposureandmax_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 savedmax_order_notional. - If you are not trading straight away, set
LIVE_TRADING=falseand restart. - 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).
- 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.