Тема
Multi-Wallet + Strategy-Config Migration Runbook (2026-05-03)
Step-by-step migration of a single-wallet
.envdeployment to the encrypted multi-wallet store (wallets.db, FE-1) plus the SQLite strategy-config store (config.db, FE-2 + FE-5). Authoritative for the X2.2..X2.4 layer shipped 2026-05-03.Companion documents:
- ADR —
docs/adr/adr-015-multi-wallet-and-strategy-config.md- Operator smoke runbook —
multi-wallet-smoke-runbook-2026-05-03.md- Production-readiness verdict —
production-readiness-2026-05-03.md- Tech-debt history —
tech-debt-backlog.md(FE-1 / FE-2 / FE-5 / TD-7 / TD-27 rows)- Dashboard smoke (UI runbook) —
dashboard-smoke-runbook-2026-05-03.md
1. Purpose (LLM-oriented)
Deliver a deterministic migration path for an operator running pm-bot against a single hot wallet via env/.env.<preset> to the encrypted multi-wallet store + SQLite strategy-config store. The migration is fully reversible — the env-var entry point remains first-class and bit-identical to pre-X2 behaviour when BOT_WALLET_LABEL and BOT_STRATEGY_LABEL are unset (ADR-015 D1, D10). No fail-fast knob is added; no deprecation deadline is published; no schema migration on the trading hot path; the trading binary keeps the prior behaviour byte- for-byte until the operator opts into the new selectors.
2. Invariants
| # | Invariant | Source |
|---|---|---|
| INV-1 | BOT_WALLET_LABEL and BOT_STRATEGY_LABEL are semantic-Optional. Unset ⇒ pm-bot reads WALLET_PRIVATE_KEY / POLY_* / per-strategy env block from the --env file. | ADR-015 D10 + crates/pm-bot/src/main.rs:214,469 |
| INV-2 | When BOT_WALLET_LABEL is set, WALLETS_MASTER_PASSPHRASE MUST be set and non-empty; missing ⇒ startup aborts with the same fail-fast pattern as validate_required_env_or_panic. | crates/pm-bot/src/main.rs:112-116 |
| INV-3 | When BOT_WALLET_LABEL AND BOT_STRATEGY_LABEL are both set, the strategy row's wallet_label MUST equal BOT_WALLET_LABEL. Mismatch ⇒ startup aborts. | crates/pm-bot/src/main.rs:210-228 |
| INV-4 | wallets.db is encrypted at-rest (Argon2id KDF + AES-256-GCM, per-DB salt). config.db is plaintext (audit-log-friendly, ops-repo-friendly). The two SQLite files are physically separated by security tier (D1). | ADR-015 §D1 |
| INV-5 | The FE-5 budget predicate is a passive synchronous check at provisioning time only (pm-cli strategy add/enable/update), NOT a runtime actor. Trading hot path observes the configured budget_limit_usd exactly as the env-block path observes BOT_TOTAL_BUDGET. | ADR-015 §D6 |
| INV-6 | The original .env file is never edited by any pm-cli subcommand. Migration prints a removal hint listing the keys the operator should strip; the operator's audit trail stays explicit. | WalletCommand::Migrate / StrategyCommand::Migrate |
| INV-7 | Trading hot path is bit-identical to pre-X2 behaviour when both selector env vars are unset. 1071 pm-bot lib tests preserved across X2.1..X2.5; baseline 1014 → 1071 is purely additive (multi-wallet + config tests). | Test suite + ADR-015 §D11 |
3. Pre-flight
Before starting the migration:
Backup the existing
.env:bashcp env/.env.bucket_arbitrage env/.env.bucket_arbitrage.pre-x2.bakConfirm the workspace builds clean:
bashcargo check --workspace cargo test -p pm-bot --lib # expect 1071 passed / 0 failed / 1 ignoredStop the live pm-bot (if running). The new SQLite files lock at write time — there must be no live writer when the migration runs:
bashpkill pm-bot # or systemd / supervisor stopPick the test wallet for dry-run validation:
- Live test wallet:
0xba561b30f68530485491547fb75baf6f03d0b7f4(canonical — seefeedback_live_test_wallet). - Use the same wallet for the smoke procedure in
multi-wallet-smoke-runbook-2026-05-03.md.
- Live test wallet:
Choose a master passphrase for
wallets.db:- Recommended: 32+ bytes of operator-only entropy. The Argon2id KDF accepts any non-empty string; longer passphrases extend the attacker's brute-force budget.
- Store the passphrase in your operator-grade secret manager (1Password / pass / sops-encrypted file). The pm-cli does NOT persist the passphrase to disk — losing it means the wallet blobs are unrecoverable from
wallets.dbalone (see § 7 "Recovery procedures"). - Decide the delivery mechanism for pm-bot startup: shell env (
export WALLETS_MASTER_PASSPHRASE=…) or a systemd-styleEnvironmentFile=/etc/pm-bot/secrets.envwith mode 0400.
Decide the database paths:
- Default
./wallets.dband./config.dbresolve relative to pm-bot's working directory. - Override via
--db-pathflag (per pm-cli subcommand) orBOT_WALLETS_DB_PATH/BOT_CONFIG_DB_PATHenv vars. - Recommended deployment layout:
/etc/pm-bot/wallets.db(mode 0600, owned by trading user) +/etc/pm-bot/config.db(mode 0644, ops-repo-friendly).
- Default
4. Step 1 — Wallet migration
4.1 Migrate the existing .env private key
bash
pm-cli wallet migrate \
--label hot-1 \
--from-env env/.env.bucket_arbitrage \
--db-path /etc/pm-bot/wallets.dbThe subcommand reads WALLET_PRIVATE_KEY, POLY_API_KEY, POLY_SECRET, POLY_PASSPHRASE, and (if present) POLY_FUNDER_ADDRESS from --from-env. It derives the wallet address from the private key, prompts twice for WALLETS_MASTER_PASSPHRASE (no echo), encrypts both blobs with AES-256-GCM, and INSERTs the row into wallets.db.
Stdout (success):
[migrate] read 5 keys from env/.env.bucket_arbitrage
[migrate] derived address 0xba561b30f68530485491547fb75baf6f03d0b7f4
[migrate] enter master passphrase: ****************
[migrate] confirm passphrase: ****************
[migrate] inserted row label=hot-1 address=0xba56…d0b7f4
[migrate] reminder — strip the following keys from env/.env.bucket_arbitrage:
WALLET_PRIVATE_KEY POLY_API_KEY POLY_SECRET POLY_PASSPHRASE
(POLY_FUNDER_ADDRESS preserved as it is not stored separately)The runbook does not edit env/.env.bucket_arbitrage. The operator removes the listed keys manually after step 4.3 confirms the wallet is loadable.
4.2 Verify the wallet appears in the store
bash
pm-cli wallet list --db-path /etc/pm-bot/wallets.dbExpected output (table — metadata only, no decrypt):
label address chain_id clob_url created_at
------ -------------------------------------------- -------- ---------------------------------- -------------------
hot-1 0xba561b30f68530485491547fb75baf6f03d0b7f4 137 https://clob.polymarket.com 2026-05-03T11:02:14ZIf the row is missing, list returns the empty table; re-check the --db-path value (the most common mistake is operating on a fresh ./wallets.db in the cwd while the migration wrote to /etc/pm-bot/wallets.db).
4.3 Boot pm-bot once with the new wallet (dry-run)
bash
WALLETS_MASTER_PASSPHRASE='<the passphrase you typed in 4.1>' \
BOT_WALLETS_DB_PATH=/etc/pm-bot/wallets.db \
BOT_WALLET_LABEL=hot-1 \
cargo run --release -p pm-bot -- --dry-run \
--strategy bucket-arb \
--env env/.env.bucket_arbitrageLook for these markers in the early stdout:
[pm-bot] BOT_WALLET_LABEL=hot-1 — unlocking from wallets.db
[pm-bot] wallet hot-1 unlocked address=0xba56…d0b7f4
[pm-bot] strategy=bucket-arb (env-block path; BOT_STRATEGY_LABEL unset)When this works the wallet portion of the migration is complete. Stop the dry-run (Ctrl-C) and proceed to step 2.
4.4 Optional — strip the legacy keys from .env
After 4.3 confirms the new path works, remove the four keys (WALLET_PRIVATE_KEY, POLY_API_KEY, POLY_SECRET, POLY_PASSPHRASE) from env/.env.bucket_arbitrage. Keep POLY_FUNDER_ADDRESS if your deployment uses a separate funder.
To audit the strip:
bash
diff env/.env.bucket_arbitrage env/.env.bucket_arbitrage.pre-x2.bakExpected: 4 deleted lines, no insertions.
If you prefer to keep the .env whole as a rollback artefact, leave the keys in place — pm-bot ignores them when BOT_WALLET_LABEL is set (the wallets.db unwrap is the source of truth).
5. Step 2 — Strategy migration (per strategy)
Repeat this section once per strategy you intend to drive through the config.db path. Strategies that should keep using the env-block path need no migration — pm-bot resolves them via BOT_STRATEGY + the per-strategy env block exactly as before.
5.1 Migrate per-strategy env block to config.db
bash
pm-cli strategy migrate \
--label bucket-prod \
--type bucket-arb \
--from-env env/.env.bucket_arbitrage \
--wallet hot-1 \
--budget 50.00 \
--db-path /etc/pm-bot/config.dbThe subcommand picks every BUCKET_ARB_* line (or the matching <PREFIX>_* block per --type: DIRECTIONAL_, MAKER_, MAKER_REBATE_, MERGE_ARB_) and stores them as a flat JSON object inside the definition column of a new row. The five known prefixes are wired in crates/pm-cli/src/main.rs:506-523.
Stdout (success):
[migrate] picked 17 keys with prefix BUCKET_ARB_ from env/.env.bucket_arbitrage
[migrate] inserted strategy id=1 label=bucket-prod type=bucket-arb wallet=hot-1 budget=50.00 enabled=false
[migrate] audit row id=1 op=migrate
[migrate] reminder — strip the following lines from env/.env.bucket_arbitrage when ready:
BUCKET_ARB_*
BOT_STRATEGY=bucket-arb (when ready to switch to BOT_STRATEGY_LABEL=bucket-prod)5.2 Enable the row + run the FE-5 predicate
The migration inserts the row with enabled = 0 so the operator has a chance to inspect the JSON and run the budget predicate before the strategy starts trading.
bash
pm-cli strategy enable \
--label bucket-prod \
--wallet-balance 250.00 \
--db-path /etc/pm-bot/config.db--wallet-balance is the operator-supplied USD balance for the wallet (query via pm-cli → on-chain pUSD.balanceOf or read from a recent bot_sessions.wallet_balance_usd_at_open row). When supplied, the FE-5 predicate runs:
budget_limit_usd <= wallet_balance + Σ(other enabled strategies' limits on wallet_label)Failure path:
[enable] FE-5 predicate FAILED:
strategy bucket-prod budget_limit_usd=50.00
wallet hot-1 balance=10.00
peer budgets on hot-1=0.00
net headroom=-40.00
refusing to enable — raise --wallet-balance or lower budget via `pm-cli strategy update`Pass path:
[enable] FE-5 predicate PASSED (headroom=200.00)
[enable] strategy bucket-prod enabled=1
[enable] audit row id=2 op=enableWhen --wallet-balance is omitted, the predicate is skipped with a single WARNING line; the row enables anyway. This is intentional for operators who validate wallet balances out-of-band (e.g. a pre-deploy spreadsheet). The WARNING keeps the audit trail honest.
5.3 Verify the row
bash
pm-cli strategy list --db-path /etc/pm-bot/config.dbExpected:
id label type wallet budget enabled updated_at
-- ------------ ---------- ------ ------ ------- -------------------
1 bucket-prod bucket-arb hot-1 50.00 1 2026-05-03T11:14:02Z5.4 Repeat for additional strategies
A typical multi-strategy operator layout:
bash
# directional (harvester preset)
pm-cli strategy migrate --label harvester-prod --type directional \
--from-env env/.env.harvester --wallet hot-1 --budget 30 \
--db-path /etc/pm-bot/config.db
pm-cli strategy enable --label harvester-prod --wallet-balance 250.00 \
--db-path /etc/pm-bot/config.db
# maker-rebate (paired emission off by default — TD-7 X5.x)
pm-cli strategy migrate --label rebate-prod --type maker-rebate \
--from-env env/.env.maker_rebate --wallet hot-1 --budget 80 \
--db-path /etc/pm-bot/config.db
pm-cli strategy enable --label rebate-prod --wallet-balance 250.00 \
--db-path /etc/pm-bot/config.dbThe FE-5 predicate runs against the rolling sum of enabled rows on the same wallet. Adding harvester-prod (30) and rebate-prod (80) to a wallet with bucket-prod (50) requires a wallet balance of at least 160.00 before the third enable succeeds (50 + 30 + 80 = 160).
6. Step 3 — Startup verification
6.1 Boot pm-bot through both selectors
bash
WALLETS_MASTER_PASSPHRASE='<passphrase>' \
BOT_WALLETS_DB_PATH=/etc/pm-bot/wallets.db \
BOT_CONFIG_DB_PATH=/etc/pm-bot/config.db \
BOT_WALLET_LABEL=hot-1 \
BOT_STRATEGY_LABEL=bucket-prod \
cargo run --release -p pm-bot -- --env env/.env.bucket_arbitrageNote: --env is still required when ANY env var (e.g. RTDS_*, BOT_LOCAL_API_*, BOT_BUDGET_PER_MARKET) lives in the .env file. The strategy block is consumed from config.db; everything else remains in .env. This is intentional per ADR-015 D10 — the multi- wallet layer migrates secrets and strategy bag, not cross-cutting operator knobs.
Expected boot markers:
[pm-bot] BOT_WALLET_LABEL=hot-1 — unlocking from wallets.db
[pm-bot] wallet hot-1 unlocked address=0xba56…d0b7f4
[pm-bot] BOT_STRATEGY_LABEL=bucket-prod — loading from config.db
[pm-bot] strategy bucket-prod type=bucket-arb wallet=hot-1 budget=50.00 enabled=true
[pm-bot] cross-check: row.wallet_label='hot-1' matches BOT_WALLET_LABEL='hot-1' — OK
[pm-bot] action_executor: started6.2 INV-3 cross-check failure path
If the strategy row's wallet_label does not match BOT_WALLET_LABEL, pm-bot aborts:
ERROR — strategy bucket-prod has wallet_label='hot-2' but
BOT_WALLET_LABEL='hot-1' — refusing to start to avoid
signing with the wrong keyThis is the INV-3 enforcement at crates/pm-bot/src/main.rs:210-228. The fix is one of:
- Update the strategy row to point to the correct wallet:
pm-cli strategy update --label bucket-prod --field wallet_label --value hot-1. - Set
BOT_WALLET_LABELto match the row. - Unset
BOT_WALLET_LABEL— pm-bot then refuses with a different error: "BOT_WALLET_LABEL is unset — refusing to start (would fall back to env-path while strategy row pins a different wallet)". This is intentional belt-and-braces.
6.3 Smoke validation (mandatory before declaring ready)
Run the operator-grade smoke procedure in multi-wallet-smoke-runbook-2026-05-03.md. The smoke covers 21 probes (CLI + WebUI + pm-bot startup + trading hot-path bit-identity). Do NOT declare the migration complete until every probe is [OK].
7. WebUI alternative (X2.4)
For operators who prefer a graphical flow, the WebUI configurator shipped in X2.4 (commit fe880bc) covers the same surface as steps 4–5 above. The CLI and WebUI write to the same SQLite files; either path is authoritative.
7.1 Boot the dashboard stack
bash
# Terminal 1 — pm-bot (no LIVE_API needed for configurator alone, but
# enabling it gives the SPA the live feed for /strategies sanity check)
BOT_LOCAL_API_ENABLED=true \
BOT_LOCAL_API_BIND=127.0.0.1:31339 \
BOT_LOCAL_API_TOKEN=$(openssl rand -hex 16) \
cargo run --release -p pm-bot -- --dry-run \
--strategy bucket-arb --env env/.env.bucket_arbitrage
# Terminal 2 — pm-dashboard backend (REST + SSE proxy)
DASHBOARD_BIND_ADDR=127.0.0.1:31337 \
BOT_WALLETS_DB_PATH=/etc/pm-bot/wallets.db \
BOT_CONFIG_DB_PATH=/etc/pm-bot/config.db \
cargo run --release -p pm-dashboard
# Terminal 3 — Vue 3 SPA (dev — Vite hot reload)
cd frontend/pm-dashboard-web && pnpm dev
# production: pnpm build && serve dist/ via Caddy per ADR-012Open http://localhost:5173/ (dev) or the Caddy-proxied production URL.
7.2 Add a wallet via WebUI
- Click Wallets in the top-bar (route
/wallets). - Click Add wallet.
- Fill the form:
- Label —
hot-1. - Private key — paste hex (
0x…-prefixed or bare). - Master passphrase — paste from the operator's secret manager.
- Chain ID —
137(default). - CLOB URL —
https://clob.polymarket.com(default).
- Label —
- Submit. The dashboard backend forwards the payload to
POST /api/wallets; the server-side handler runs the same Argon2id- AES-256-GCM encryption as the CLI before writing to
wallets.db.
- AES-256-GCM encryption as the CLI before writing to
- The form clears and the new row appears in the list.
The master passphrase is held in the SPA only for the duration of the form submission — the response does NOT echo it back, and the Pinia store does not cache it. Subsequent sessions prompt again.
7.3 Add a strategy via WebUI
- Click Strategies in the top-bar (route
/strategies). - Click Add strategy.
- Pick a strategy type from the dropdown — the form below renders the per-type structured fields (5 strategy types ⇒ 5 distinct schemas, see
frontend/pm-dashboard-web/src/views/StrategyAddView.vue). - Fill the structured form (e.g. for
bucket-arb:BUCKET_ARB_MIN_PROFIT_PCT,BUCKET_ARB_DRIFT_CANCEL_MARGIN, …). - Set wallet label (dropdown — populated from
/api/wallets). - Set budget limit USD — the FE-5 predicate runs server-side.
- Submit. On predicate failure the form surfaces a 422 response with a structured error body (
{code:"fe5_predicate_failed", headroom:-40.00, peer_budgets:…}) and the form remains editable. On success the row entersenabled = false; the operator clicks the row's Enable action to flip it on (and re-run the predicate).
7.4 Rotate master passphrase via WebUI
The Wallets view exposes a "Rotate master" action gated behind a paranoid double-confirm (operator must type the literal label string twice). The handler at POST /api/wallets/rotate-master runs the same re-encrypt-everything-in-one-transaction logic as the CLI (pm-cli wallet rotate-master).
8. Recovery procedures
| Scenario | Recovery |
|---|---|
| Lost master passphrase | The encrypted blobs in wallets.db are unrecoverable. Restore from the .env backup (step 3.1), re-import via pm-cli wallet add --label hot-1 with a NEW master passphrase. Update the systemd / supervisor secret store. The on-chain wallet itself is untouched — only the encrypted SQLite copy is gone. |
Corrupted wallets.db (file truncated, journal-replay failure, sqlite header mismatch) | First, copy the file aside for forensics: cp wallets.db wallets.db.corrupt-2026-05-03. Restore from the most recent backup if you keep one (recommended cadence: daily). Otherwise re-create from .env: re-run pm-cli wallet migrate --from-env <backup> for each wallet. The audit history of WHEN keys were added is lost; the keys themselves are recoverable. |
Corrupted config.db | Same playbook. config.db is plaintext + ops-repo-friendly, so most operators will track it in a private git repo (gitignored from the public workspace). Restore from git history OR re-run pm-cli strategy migrate --from-env <backup> per strategy. |
BOT_WALLET_LABEL does not match strategy row's wallet_label | INV-3 abort. Either update the row (pm-cli strategy update --label X --field wallet_label --value Y --wallet-balance Z) or change the env var. Re-run pm-cli strategy list to confirm. |
WALLETS_MASTER_PASSPHRASE rejected as wrong | The Argon2id KDF derives a key that fails AES-GCM authentication. Likely causes: typo, wrong env file sourced, recent pm-cli wallet rotate-master not yet propagated to the operator's secret store. Use pm-cli wallet export --label <X> interactively to validate the passphrase against a single row before retrying pm-bot startup. |
FE-5 predicate keeps failing on enable | Re-check --wallet-balance value (live on-chain pUSD vs USDC.e — the predicate budgets are USD denominated; pUSD = USD 1:1). Reduce a peer strategy's budget_limit_usd via pm-cli strategy update --label <peer> --field budget_limit_usd --value <new>. Predicate re-runs on every enable / update / add. |
| WebUI configurator shows blank wallet dropdown | The GET /api/wallets request 503'd because BOT_WALLETS_DB_PATH was not set on pm-dashboard or the file is unreadable. Check the dashboard log for the bind-time line; verify file permissions (mode 0600 owned by the dashboard process user). |
| Want to roll back entirely | See § 9. |
9. Rollback
The migration is reversible at any step.
Stop pm-bot:
bashpkill pm-botUnset the selector env vars:
bashunset BOT_WALLET_LABEL unset BOT_STRATEGY_LABEL unset WALLETS_MASTER_PASSPHRASE # optional but tidyRestore the original
.env(if you stripped keys per step 4.4):bashcp env/.env.bucket_arbitrage.pre-x2.bak env/.env.bucket_arbitrageBoot pm-bot the legacy way:
bashcargo run --release -p pm-bot -- --env env/.env.bucket_arbitrage
The trading hot path is byte-for-byte identical to pre-X2 — confirmed by the 1014 → 1071 baseline (additive only, no behavioural change in the env path). The wallets.db and config.db files remain on disk but are not consulted; you can rm them at leisure or keep them for a later re-attempt.
10. Operational checklist (post-migration)
After the migration is complete, the operator should:
- [ ] Confirm
pm-cli wallet listshows every expected wallet. - [ ] Confirm
pm-cli strategy listshows every expected strategy with the correctwallet,budget, andenabledflag. - [ ] Confirm
BOT_WALLETS_DB_PATHandBOT_CONFIG_DB_PATHare set in the systemd / supervisor unit file (or the operator's wrapper shell script). - [ ] Confirm
WALLETS_MASTER_PASSPHRASEis sourced from the secret store, NOT hard-coded in the unit file or shell history. - [ ] Confirm file permissions:
wallets.dbmode 0600,config.dbmode 0644 (or stricter), both owned by the trading-user. - [ ] Run the smoke procedure
multi-wallet-smoke-runbook-2026-05-03.mdto validate the full stack. - [ ] Schedule the first nightly backup of
wallets.db(e.g.cron-based copy + age-encrypt + S3 upload). - [ ] Update the operator's runbook bookmark list to include this migration runbook + the smoke runbook.
- [ ] Document the rollback drill in the team's incident-response wiki (operators should rehearse §9 once before relying on the new path in production).
11. Cross-references
- ADR —
docs/adr/adr-015-multi-wallet-and-strategy-config.md(Accepted 2026-05-03; D1..D11 design decisions). - Smoke runbook —
multi-wallet-smoke-runbook-2026-05-03.md(operator-grade end-to-end probe procedure). - Production verdict —
production-readiness-2026-05-03.md(READY for sustained 24/7 production after X2.5). - Tech-debt history —
tech-debt-backlog.md(FE-1, FE-2, FE-5 closure in X2.2/X2.3; TD-7 X5.x; TD-27 X1.x). - Dashboard smoke (UI) —
dashboard-smoke-runbook-2026-05-03.md(X1.5b risk widgets validation). - Local-bot runbook —
local-dashboard-runbook.md(ADR-005 SSE+REST integration). - Memory anchor —
feedback_encrypted_wallets,feedback_live_test_wallet.