> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sesameterminal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Runbook: live verification (Phase 3)

# 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](../PLAN.md#phase-3-trading-predictions)). 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](../PLAN.md#phase-8-combos)).

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](../adr/0005-no-geoblock-workarounds.md)).

Related documents:

* [User guide](../guides/user/trading.md): what each control does.
* [Incident runbook](incident.md): what to do when something looks wrong.
* [Session Key setup](session-key-setup.md): creating and configuring the signer.
* [Polymarket venue notes](../venue/polymarket.md): the UNVERIFIED items step 16 checks.
* [Security review checklist §2](../security/review-checklist.md#2-phase-3-live-trading-gate):
  the live-trading gate.

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.

| # | Prerequisite | How to check |
| - | - | - |
| P1 | The security gate is signed off | `docs/security/signoff-phase-3.md` exists and is signed by the Security Reviewer for the commit you are running. Nothing else counts as sign-off |
| P2 | You are in a permitted location, with no VPN, proxy or relay | `https://polymarket.com/api/geoblock` in a browser on the same network shows `"blocked":false` |
| P3 | The Session Key is set up and authorised with `CLOB` scope, and with `COMBOSRFQ` as well (or `ALL`) for the Combos rows 16.33 to 16.45 | [Session Key setup](session-key-setup.md) sections 2 to 4, and section 8 for Combos, are done; the credential state is `ready`. See [pre-flight PF5](#14-pre-flight) |
| P4 | The Deposit Wallet holds no more than the working-balance cap | Stage 1 of [ADR 0013](../adr/0013-working-balance-cap.md) is Accepted. On the day of the run, before arming, cash plus the cost of open positions is at or below its cap ([pre-flight PF2](#14-pre-flight)). About $20 to $50 in pUSD covers this runbook. Keep the rest of your funds elsewhere |
| P5 | `LIVE_TRADING=true` in the app host's environment | Exactly the lowercase string `true`. Any other value keeps trading off |
| P6 | The build is the reviewed commit | `git rev-parse HEAD` matches the commit named in the sign-off; `task lint` and `task test` pass |
| P7 | The host clock is synchronised | Windows: `w32tm /query /status` shows a recent successful sync. Linux: `timedatectl` shows `System clock synchronized: yes` |
| P8 | No other trading on the account during the run | Do not place orders on polymarket.com or through another tool while you run the steps, except where step 16 asks for it. See [pre-flight PF4](#14-pre-flight) for what the app does when you do |

Setup of the machine itself (Go, Node, Task, `.env`) is in
[dev-setup.md](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 $100k to $500k bankroll
([ADR 0013](../adr/0013-working-balance-cap.md) 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.

| Limit | Default | Ceiling variable in `.env` | Default ceiling | Value for the run (setting and ceiling) | Why |
| - | - | - | - | - | - |
| `max_order_notional` | \$25,000 | `RISK_CEILING_ORDER_NOTIONAL` | \$50,000 | \$4 | Just above the \$3 test stake (gate item 22) |
| `max_position_notional` | \$100,000 | `RISK_CEILING_POSITION_NOTIONAL` | \$250,000 | \$8 | Room for one small position |
| `max_daily_notional` | \$1,000,000 | `RISK_CEILING_DAILY_NOTIONAL` | \$5,000,000 | \$60 | About 15 test orders of \$3 each, with room to repeat a step |
| `max_open_orders` | 100 | `RISK_CEILING_OPEN_ORDERS` | 200 | 4 | Step 8 needs three open orders at once |
| `max_orders_per_minute` | 60 | `RISK_CEILING_ORDERS_PER_MINUTE` | 120 | 10 | |
| `max_combo_notional` | \$5,000 | `RISK_CEILING_COMBO_NOTIONAL` | \$25,000 | \$4 | One combo buy at the \$3 stake (rows 16.33 and 16.37); security P8-07, gate G9. Raised only by the PF5 stake search |
| `max_combo_exposure` | \$25,000 | `RISK_CEILING_COMBO_EXPOSURE` | \$100,000 | \$8 | Room for one small combo position (P8-07) |
| `max_combo_legs` | 10 | `RISK_CEILING_COMBO_LEGS` | 50 | 3 | The run uses two legs (P8-07) |
| `duplicate_window_ms` | | | | 2000 | Step 10c relies on it being longer than the 350 ms double-click guard |
| `price_band_ticks` | | | | 10 | Steps 4 and 10b rely on it |
| `exit_slippage_ticks` | | | | 5 | |
| `arm_idle_minutes` | | | | 10 | Step 12 lowers it to 1 |
| `heartbeat_enabled` | | | | off | Step 13 turns it on and off |
| `cancel_on_shutdown` | | | | on | Steps 13 and 14 rely on it |

A data directory created before these defaults keeps any limit you changed; only limits
still at the old defaults ($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](#6-after-the-run).

Stake presets: the defaults are $1,000, $2,500, $5,000, $10,000 and $25,000. Replace them **before** you lower `max_order_notional`: set preset 1 to **$3\*\* and remove the others or
set them to $4 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`.

| Market | Requirements |
| - | - |
| **A** | Not a sports game. `neg_risk: false`. Tick 0.01. An outcome trading between 0.20 and 0.55 with a spread of one or two ticks and at least 50 shares on the best bid and ask. Not resolving within the next day |
| **B** | `neg_risk: true` (for example one outcome of a multi-outcome event). Same price and liquidity rules as A |
| **C** (optional) | An in-play sports market with `seconds_delay` above 0, for the delay check in step 16 |

### 1.3 Capture the logs

Start the app with its output saved to a file inside `data/`, which git ignores:

```powershell theme={null}
task dev *>&1 | Tee-Object -FilePath data\live-verification.log
```

`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](#3-if-a-step-fails).

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](#2-recording-results)), 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](../security/findings-phase-3-recheck.md). 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](../adr/0012-session-key-scope.md)). Open the ADR's
[Gate 5 decision](../adr/0012-session-key-scope.md#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](../adr/0013-working-balance-cap.md) 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](#6-after-the-run)).

**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](#3-if-a-step-fails), 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](incident.md#2-fills-or-orders-you-did-not-make) 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](../security/findings-phase-8.md#gate-before-combos-go-live).
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.

| # | Check | How |
| - | - | - |
| G1 | Gate 5 and the working-balance cap | PF1 and PF2 hold. Combos add no exception: the combo positions count toward the cap at cost |
| G2 | The Session Key has the combos scope, and no builder key is on the host | The key was generated on this host and authorised `["CLOB","COMBOSRFQ"]` (preferred: `ALL` also covers venues Polymarket adds later) or `["ALL"]`; the old `CLOB`-only key is revoked ([Session Key setup §8.2](session-key-setup.md#82-issue-a-key-with-the-combos-scope)). After the start, row 16.41: the `session key confirmed` line shows the scope, and `GET /api/trading` `combos_reasons` is empty once Armed. No `POLY_BUILDER_*` or `POLYMARKET_BUILDER_*` variable is set in `.env` or the host's environment, and no Builder API key is stored on the host. With a `CLOB`-only key every quote request is refused `combos_unavailable` before anything is sent: record that under row 16.40 and stop here |
| G3 | The Exchange V3 approvals are set, by the owner on polymarket.com | pUSD (`0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB`) `allowance(wallet, 0xe3333700cA9d93003F00f0F71f8515005F6c00Aa)` is above 0 for buys, and the PositionManager (`0x006F54F7f9A22e0000CC2AB60031000000ae9fEF`) `isApprovedForAll(wallet, 0xe3333700cA9d93003F00f0F71f8515005F6c00Aa)` is true for sells. Read both with a read-only chain query, for example the contracts' Read tab on polygonscan.com, and record the values. The app cannot set or read them ([Session Key setup §8.3](session-key-setup.md#83-exchange-approvals)); a missing one shows on Accept as `venue_balance`. Set them before the app's first start: if polymarket.com sets them only as part of a combo trade on the site, that trade made after the start is foreign activity (PF4) |
| G4 | The session-wrapped V3 signature (UNVERIFIED) | Row 16.40 during the run. A refusal stops the Combos run; the fix needs an SDK or docs source, not trial and error against the gateway |
| G5 | The gateway bodies (UNVERIFIED) | Rows 16.33, 16.37 and 16.38 during the run, including the 409 before acceptance and the 404 bodies |
| G6 | Security findings P8-01 to P8-05 | Each is fixed and tested, or accepted in writing by the tech lead in the review's Disposition table, with the stated consequence. Record which |
| G7 | Foreign activity with a real fill | Rows 16.42 and 16.43 during the run: the app's own combo fill raises no foreign-activity alert, the order manager does not flag it, and cash is debited once |
| G8 | Leg mapping | Row 16.42 during the run: every leg's `leg_outcome_label` equals the outcome chosen |
| G9 | Run limits | The three combo limits and their ceilings are at the run values in [section 1.1](#11-risk-limits-for-the-run) (`max_combo_notional` $4, `max_combo_exposure` $8, `max_combo_legs` 3) before arming. `GET /api/settings/risk` shows each limit and ceiling. They are restored in [section 6](#6-after-the-run) |
| G10 | Scope lint | `task lint` passes on the build that runs, and `git log -S scope:allow --oneline 0a5b857..HEAD` (0a5b857 is the reviewed commit) lists nothing |

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](../venue/polymarket.md#gaps-a1a17), 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 $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) 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](#5-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:

   ```powershell theme={null}
   $names = 'POLYMARKET_SESSION_KEY','POLYMARKET_CLOB_API_KEY','POLYMARKET_CLOB_SECRET','POLYMARKET_CLOB_PASSPHRASE','SESSION_SECRET','UI_PASSWORD_HASH','TELEGRAM_BOT_TOKEN'
   $values = Get-Content .env | ForEach-Object {
     if ($_ -match '^\s*([A-Z_]+)\s*=\s*(.+)$' -and $names -contains $Matches[1]) { $Matches[2].Trim().Trim("'").Trim('"') }
   } | Where-Object { $_.Length -ge 8 }
   $found = $values | Where-Object { Select-String -Path data\live-verification.log -SimpleMatch -Pattern $_ -Quiet }
   if ($found) { 'A secret value is in the log. Do not share it.' } else { 'No configured secret value found in the log.' }
   ```

   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](incident.md#1-a-key-may-have-leaked) 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](#3-if-a-step-fails)) and see
the [incident runbook](incident.md#4-an-order-stuck-in-unknown).

### 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](../architecture/browser-protocol.md#reject-codes).
   * `unresolved_placements` is 0 (see [section 4](#4-steps)).

**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 (or $4 if 10a was skipped; then use
`size` "20" so the notional is above \$4).

```powershell theme={null}
$base = 'http://localhost:5173/api'
$origin = 'http://localhost:5173'
$sec = Read-Host 'UI password' -AsSecureString
$pw = [System.Net.NetworkCredential]::new('', $sec).Password
$login = Invoke-RestMethod -Method Post -Uri "$base/auth/login" -SessionVariable s `
  -ContentType 'application/json' -Headers @{ Origin = $origin } `
  -Body (@{ password = $pw } | ConvertTo-Json)
$pw = $null
$ms = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds().ToString('x12')
$r = -join (1..18 | ForEach-Object { '{0:x}' -f (Get-Random -Maximum 16) })
$v = '89ab'[(Get-Random -Maximum 4)]
$id = '{0}-{1}-7{2}-{3}{4}-{5}' -f $ms.Substring(0,8), $ms.Substring(8,4), $r.Substring(0,3), $v, $r.Substring(3,3), $r.Substring(6,12)
$order = @{ client_order_id = $id; token_id = '<pm:token id of a market A outcome>'; side = 'buy'; price = '<a price far from the market>'; size = '10' } | ConvertTo-Json
Invoke-RestMethod -Method Post -Uri "$base/orders/place" -WebSession $s -ContentType 'application/json' `
  -Headers @{ Origin = $origin; 'X-CSRF-Token' = $login.csrf_token } -Body $order
```

* 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](../venue/polymarket.md#7-heartbeat-dead-man-switch)). 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](../adr/0011-audit-first-order-path.md)). 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](../venue/polymarket.md) 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):**

| Row | Item | How to check | Venue notes |
| - | - | - | - |
| 16.1 | `/data/orders` response is the page form with `MA==`/`LTE=` cursors | Step 4: Orders lists the order after a restart (the startup snapshot reads `/data/orders`). The log has no `/data/orders` decode error | §14 item 1 |
| 16.2 | Order `status` values | Record every status you see in Orders and the log: `live`, `matched`, `cancelled`, and `delayed` if you run the market C check. An unknown value makes the open-orders read fail with a logged error | §14 item 2 |
| 16.3 | `created_at` and `expiration` units on `/data/orders` | The step 4 order's `created_at` in Orders is the time you placed it, not 1970 or far in the future | §14 item 3 |
| 16.4 | Cash with `signature_type=3`; `balance` in 6-decimal base units | Step 2: cash matches polymarket.com to the cent | §14 item 4, §5 |
| 16.5 | L2 `POLY_ADDRESS` is the Session Key address | Steps 1 and 2: credentials `ready` and open orders and cash load | §14 item 5 |
| 16.6 | The CLOB creates or derives API credentials from a Session Key's ClobAuth signature; create, then derive on 400 | Step 1 with `POLYMARKET_CLOB_*` empty: the state goes `pending` then `ready`. After any restart (steps 13 and 14) it reaches `ready` again (the second start takes the derive path) | §14 item 6 |
| 16.7 | 401 for rejected L2 credentials or a bad L1 signature; 403 for a geoblocked L1 call | Not provoked in this run. Record any 401 or 403 seen in the log | §14 item 7 |
| 16.8 | User channel frames: `timestamp` units, `trader_side`, `maker_orders[].owner`, arrays, how a rejected subscribe is signalled | Step 9: the fill toast arrives and the fill shows the right side, price, size, role and time. For a maker fill, use Exit @ join (Portfolio **Join**) instead of Exit on a later fill and check the fill shows role `maker` and the right size. Rejected-subscribe signalling is not provoked | §14 item 8 |
| 16.9 | The P\&L chart uses the same series as polymarket.com's profile chart | Step 2: compare the two charts' shape and latest value | §14 item 9 |
| 16.10 | Activity signs for `CONVERSION` and `MIGRATION`; `REDEEM` amount equals pUSD received | Only if your history has such rows: compare Activity with polymarket.com | §14 item 10 |
| 16.11 | The fee paid on a fill | Step 9: compare the fill's `fee` with the fee in polymarket.com's trade history | §14 item 11 |

**From venue notes §5 to §10 and the Phase 3 plan:**

| Row | Item | How to check | Venue notes |
| - | - | - | - |
| 16.12 | The server accepts a Session Key's wrapped type-3 order signature (no golden vector exists for the wrapper) | Step 4: both orders accepted. A rejection here stops the run | §10 |
| 16.13 | Neg Risk exchange domain | Step 4, market B order accepted | §1, §4 |
| 16.14 | The order `signer` rule ("the order signer address has to be the address of the API KEY") versus type 3, where `signer` is the wallet | Step 4 accepted. If rejected, record the venue message from the log | §6 |
| 16.15 | Minimum order size is in shares, not pUSD | Step 4: an order of 5 or more shares with a notional below `min_size` in pUSD is accepted. Record which layer rejected, if any | Other rules |
| 16.16 | The conditional-token allowance must be refreshed before a token's first sell | Step 9 Exit: accepted, or rejected with `venue_balance`. Record which | §5 |
| 16.17 | Response bodies of venue rejections | Record the venue message (log only) for any venue reject you see | §6 |
| 16.18 | `unmatched` status meaning (the two docs pages disagree) | Only if seen: record when it appeared | §6 |
| 16.19 | Heartbeat request and response, timing, and scope to this key's credentials | Step 13. Record how long after stopping the order was gone. Record the error key (`error_msg` or `error`) if an invalid-ID reply appears in the log | §7 |
| 16.20 | Session-signers check (`GET /v1/user/session-signers`): response shape, the key listed with `CLOB` scope, `valid_until` | Step 1: state `ready`; the logged expiry is the authorisation date plus 180 days | §10, security P2-02(c), P2-03 |
| 16.21 | Clock skew from the CLOB server time | Step 1: `clock_skew_ms` is small on an NTP-synced host | PLAN Phase 3, security F16 |
| 16.22 | Foreign activity is seen through the Data API (security F5, P2-04) | After step 15, restart the app and arm it. Buy the minimum on polymarket.com, on any market. Within a short time (defined by implementation) the app shows an alert and drops to SAFE with `foreign_activity`. Record the delay. Sell the position on polymarket.com afterwards | PLAN Phase 3 |
| 16.23 | Replace is cancel, then place | Step 5: two venue calls, and the new order only after the cancel is confirmed | §6 |
| 16.24 | Cancel of an order that already matched | If a cancel raced a fill, record the reason in `not_cancelled` | §6 |
| 16.25 | Sports delay: `delayed` status, delay end time, queued cancel | Optional, market C: buy the minimum at the ask and press cancel at once. Expect `delayed` with a countdown, the cancel shown as queued (`cancel_pending`), and then either a fill or a cancel when the window ends. Record which, and whether a delay end time was shown. Exit any position | §6 |
| 16.26 | 425, 503 cancel-only, 503 post-only and 429 responses | Cannot be provoked. Record any seen, with the time, and the toast shown | §6, §9 |
| 16.27 | Session Key revocation cancels its orders, and how fast | Not in this run. Check at the next rotation ([Session Key setup §5](session-key-setup.md#5-rotation)) | §10, secrets policy §5 |
| 16.28 | Capture `GET /data/order/{orderID}` for the parser test (security P3-03, recheck C5). The committed fixtures are documented examples, marked temporary | After step 9, read three ids with a one-off read-only request from this host (the capture method is defined by implementation): the step 6 order (cancelled), the step 9 buy (matched), and `0x` followed by 64 zeros (unknown). Record each HTTP status and keep the exact body bytes. The body's `owner` field is the CLOB API key, a secret, so the raw capture is a secret: never paste it anywhere, and hand it to BE-Venue the way secrets are handled. BE-Venue commits each body byte for byte except the `owner` value, which becomes a fixed placeholder of the same length, and records that substitution in `testdata/SOURCES.txt`. The captures replace `testdata/clob_order_*.json`. Then delete the raw capture from every place it was kept | §15 UNVERIFIED items 14 and 15 |
| 16.29 | How long Data API positions lag behind a fill (security P3R-01, P3R-09) | Step 9. Before the buy, run the snippet below this table in a second PowerShell window. It reads the public Data API by wallet address once a second and prints the UTC time and the outcome's `current_size`. Note the fill's time in Fills (the venue's match time, whole seconds). The lag is the first printed time showing the new size minus the fill time. Measure the exit sell the same way (the size falls, or the row goes to `none`). Record both lags in seconds with both fill times; if the size has not changed after 10 minutes, record "over 10 min". Stop the snippet with Ctrl+C | PLAN §3.7 (positions) |
| 16.30 | Whether the CLOB cash balance subtracts resting buy orders (security P3F-04) | Step 8. Note Cash before placing the three resting orders and again while they rest. If Cash drops by their notional while nothing has filled, the balance subtracts open orders: stop, disarm and report it, because the cash-drop check would then treat every resting buy as foreign activity | PLAN §3.7 (cash) |
| 16.31 | Whether a filled buy on a fee-bearing market lowers pUSD by more than price × size (security P4-02, P4R-02) | Required before trading fee-bearing markets: a market whose taker fee rate is above 0; if you find none, record "not seen" and do not trade fee-bearing markets until it is done. Do it twice: once as a taker (buy the minimum size at the best ask, as in step 9) and once as a maker (rest a minimum-size buy one tick below the best bid and wait for it to fill; cancel it after 10 minutes if it has not). Note Cash just before the click and again once the fill shows in Fills. Record the drop, the fill's price × size, and the difference in pUSD (0 when no fee was taken from cash). When it is 0, record whether the fee was taken in shares: the position size in Positions against the fill's size. Record the fill's `fee_rate_bps`: the app does not show it, so read the trade with a one-off read-only request from this host, as in row 16.28. Exit the position | PLAN §3.7 (cash) |
| 16.32 | Capture `GET /data/trades` for the fill backfill parser (security P4R-01). The committed fixture is the documented example, marked temporary | After step 9 and row 16.31, read `/data/trades?maker_address=<wallet>&after=<Unix seconds an hour before the run>` with a one-off read-only request from this host, as in row 16.28. Check that the page holds the step 9 taker buy and the row 16.31 maker fill (so `maker_address` returns taker trades too), and record for each the `match_time`, `price`, `size` (shares or base units), `fee_rate_bps`, `status` and `next_cursor`. The body carries the API key as `owner`, so handle and commit it as in row 16.28; the capture replaces `testdata/clob_trades_openapi.json` | PLAN §3.7 (own fills), venue notes §15 |

**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 | Item | How to check | Venue notes |
| - | - | - | - |
| 16.46 | A cricket frame's `metadataGameId` equals the Gamma main event's `eventMetadata.gameId` | Pick a live cricket game on polymarket.com whose main event's `eventMetadata.gameId` starts with `id` (`GET https://gamma-api.polymarket.com/events/slug/<slug>`). Record every text frame from `wss://sports-api.polymarket.com/ws` for 5 minutes, verbatim, one per line, then `GET /events/<id>` for that main event. Record whether a frame's `metadataGameId` equals the event's `eventMetadata.gameId`, and whether that frame's `score` and `period` match the event's. Hand both captures to BE-Venue to commit as testdata | Sports channel |

Row 16.29 snippet. The request is read-only and needs no credentials:

```powershell theme={null}
$wallet = '<Deposit Wallet address>'
$token = '<token id of the market A outcome, without the pm: prefix>'
$url = "https://data-api.polymarket.com/v2/positions?user=$wallet&status=OPEN&limit=500"
while ($true) {
  $row = (Invoke-RestMethod -Uri $url).data | Where-Object { $_.token_id -eq $token }
  $size = if ($row) { $row.current_size } else { 'none' }
  '{0:HH:mm:ss} {1}' -f [DateTime]::UtcNow, $size
  Start-Sleep -Seconds 1
}
```

**From venue notes §16 (Phase 8 Combos):**

Run these rows after step 15, once the [PF5 gate](#14-pre-flight) 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.

| Row | Item | How to check | Venue notes |
| - | - | - | - |
| 16.33 | Create answer with a BUY quote: the shape, the echo of the request, `expires_at` in ms, `net_receive_e6` present or absent (gate G5) | In Safe, turn on combo mode, use as legs two outcomes of different games, at least one of a market whose outcomes are not Yes/No (for row 16.42), and press `Get quote` at the PF5 stake. The slip shows the quote and a timer. Capture the body; it replaces `rfq_create_quote_docs.json` | §16 fixtures item 1 |
| 16.34 | Create answer with a SELL quote (Exit of a combo position) | After row 16.37 fills: Portfolio › Combos, the position's `Exit`. Capture the body. Accepting it (Armed) also checks the combo position approval (G3): record `Filled`, or `Rejected · not enough cash or allowance` (`venue_balance`). To keep the position, press `Decline` | §16 fixtures item 1, A14 |
| 16.35 | Create answer `FAILED` `NO_QUOTES`, and `SIZE_TOO_LARGE` if seen | Only if seen (the PF5 stake search may show it): a quote request the market makers do not answer. Capture the body; it replaces `rfq_create_no_quotes_docs.json`. `CONTRADICTORY_LEGS` cannot be provoked through the app: risk refuses it first | §16 fixtures item 1 |
| 16.36 | Timings: the acceptance window (`expires_at` minus the answer time) and how long the create call is held | Row 16.33: record both in ms. The docs give 5 s and 10 s windows | §16 timings |
| 16.37 | Accept answer `EXECUTING` with `taker_order_hash`, and whether the hash equals the audit's order hash (gate G5) | Arm. Get a new buy quote on the same legs as row 16.33 (the one captured there has expired) and press `Accept` within its window. The audit's `signed` row has the order hash; record whether the venue's `taker_order_hash` equals it (a mismatch is logged as `combo order hash differs`; security P8-06). Record the quote's `yes_position_id` (the `pmc:` token of the RFQ in `GET /api/combos/rfqs`) and, after the fill, check it is the combo position's `token_id` (P8-06). Capture the body; it replaces `rfq_accept_executing_docs.json`. Record `AWAITING_MAKER_CONFIRMATION`, `FAILED` `MAKER_DECLINED` and 409 `EXPIRED_RFQ` if seen. A `Checking…` status line means the answer was lost: wait for it to settle, do not accept another quote meanwhile, and record the outcome and the status answers seen | §16 fixtures item 2 |
| 16.38 | Status answers: 409 before acceptance, `EXECUTING`, `MINED`, `CONFIRMED` and `FILLED` with `tx_hash`, 404 `UNKNOWN_RFQ` (gate G5; security P8-01 and P8-03 depend on the 409 and 404 bodies) | The app reads the status every 500 ms after row 16.37; record each status seen and which of `CONFIRMED` or `FILLED` came last. With a one-off read-only request from this host, read the status of an RFQ you did not accept (row 16.33's; expect 409) and of a made-up id (expect 404). Capture the bodies; they replace `rfq_status_filled_docs.json`, `rfq_status_confirmed_docs.json` and `rfq_error_docs.json` | §16 fixtures item 3, A16 |
| 16.39 | Error bodies for 401, 403 (`ADDRESS_MISMATCH`, and the geoblock answer), 429 `RATE_LIMITED` and 503 | Cannot be provoked safely. Record any seen, with the time and the reject shown | §16 fixtures item 4 |
| 16.40 | Session key: the gateway takes the key's L2 credentials and its session-wrapped Exchange V3 accept signature (480 bytes; no SDK vector covers it) (gate G4) | Rows 16.33 and 16.37 pass with the Session Key. If the accept is refused, record the status and code: it stops the Combos run. With a `CLOB`-only key, the trading state's `combos_reasons` lists `combos_unavailable` and no request reaches the gateway, so whether the gateway itself refuses such a key is not checked | §16 fixtures item 5, §10 |
| 16.41 | The session-signers answer lists `COMBOSRFQ` (or `ALL`), and the credential status reports the combos scope (gate G2) | Step 1 with the combos-scoped key: the log line `session key confirmed` carries `scopes` with `CLOB` and `COMBOSRFQ` (or `ALL`), `GET /api/account` `credentials.scopes` includes `combos`, and once Armed `GET /api/trading` `combos_reasons` is empty | §16 fixtures item 5 |
| 16.42 | The app's own combo trade in the Data API: its `is_combo` row in `/v2/activity` and its `/v2/positions/combos` row, linked to the RFQ by `tx_hash`; the legs bought are the legs chosen (gates G7 and G8) | After row 16.37 fills: the trade shows in Activity as a combo, the position in Portfolio › Combos at cost, cash drops once by the cost with fees, and no foreign-activity alert fires. In the position row, every leg's `leg_outcome_label` equals the outcome chosen in the slip, including the leg whose market is not Yes/No (security P8-05). Record which `/v2/activity/combos` rows the Accept created (for example `SPLIT`) and whether they also appear in `/v2/activity` (P8-10). Capture the rows with a read-only request (no credentials) | §16 fixtures item 6 |
| 16.43 | Whether the combo fill shows on the CLOB user channel, or in the CLOB trades backfill (gate G7) | After row 16.37: record any fill toast or user channel frame for the combo token. Restart the app and record whether the fill backfill (`/data/trades`, read at start) returns the combo trade: a `foreign activity detected` log line naming it, or a second cash debit for it, means it does. Either one fails G7 until security P8-04 is fixed | §16 A15 |
| 16.44 | Combo position statuses `REDEEMABLE` and `RESOLVED_PARTIAL`, and the `redeemable` flag | Only if a held combo resolves: record the status and flag, and what Portfolio › Combos shows | §16 A7, PLAN Phase 8 |
| 16.45 | The fee on a combo buy: `total_required_e6` minus `maker_amount_e6` against 0.05 × p × (1 − p) × shares | Row 16.33's capture: record both amounts, `blended_price_e6` (p) and `taker_amount_e6` (shares) | §16 amounts and fees |

## 5. Results table

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

```markdown theme={null}
Commit: <sha>    Date (UTC): <date>    Country (geoblock): <country>
Market A: <pm:...>    Market B: <pm:...>    Market C: <pm:... or none>
Session Key address: <0x...>    Expiry: <date>

| Step | Check | Result | Evidence (client_order_id, order id, code, time) | Notes |
| --- | --- | --- | --- | --- |
| PF1 | Gate 5 decision recorded in ADR 0012: statement, relayer test or accepted risk | | | |
| PF2 | ADR 0013 stage 1 Accepted; cash plus position cost at or below the cap | | | |
| PF3 | No open orders; start log `baseline orders adopted orders=0` | | | |
| PF4 | Foreign activity from your own site actions noted | | | |
| PF5 G1–G10 | Combos gate: one line per item G1 to G10, then the smallest stake quoted | | | |
| 1 | Geoblock clear, credentials ready, skew under 2 s, SAFE | | | |
| 2 | Cash, positions, open orders, P&L match | | | |
| 3 | Arm | | | |
| 4 | GTC buy far from the market, markets A and B | | | |
| 5 | Replace by one tick | | | |
| 6 | Cancel | | | |
| 7 | Cancel-market; other market untouched | | | |
| 8 | Cancel All while SAFE | | | |
| 9 | Fill, toast, position, exit | | | |
| 10a | max_order_notional rejected | | | |
| 10b | price_band rejected | | | |
| 10c | duplicate_intent rejected | | | |
| 10d | API request over the limit rejected | | | |
| 10e | Step-up and ceiling enforced | | | |
| 11 | Double-click makes one order | | | |
| 12 | Idle to SAFE | | | |
| 13 | Heartbeat on cancels; off keeps | | | |
| 14 | Restart is SAFE; cancel on shutdown | | | |
| 15 | audit-verify passes; audit matches Polymarket | | | |
| 16.1–16.46 | One row each (see the runbook) | | | |
```

## 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](#11-risk-limits-for-the-run) 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](../adr/0013-working-balance-cap.md) 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](session-key-setup.md#6-revocation)).
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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.