Source: jay’s direct request in chat (finalize the local run steps in README, drop obsolete ones). No task/design doc.
Rewrote “Run locally” as one numbered flow — anvil → dev-local.sh (Postgres + schema + env
db:reset demoted to a “clean slate”
note. Deleted the “v1 — DB-only, no contracts” section: since the Jul-07 CTF work,
seed.ts deploys contracts via forge, so a no-contracts flow no longer exists. Also removed
the retired Deploy.s.sol / CLI-alternative comment block (CLI has its own section), added
Docker to prerequisites, and added packages/cli to the Packages table.Per jay: fund the demo accounts during setup instead of relying on the trade-time
auto-faucet. Added a top-up step to seed.ts (step 2b): mint each of wallets #1–5 up to
DEMO_WALLET_USDC (1,000 USDC, matching AUTO_FAUCET_USDC). Top-up, not blind mint, so
re-running against a reused backbone can’t inflate balances. Verified end-to-end: ran
dev-local.sh twice — balances exactly 1,000 after each run (cast against the new mint),
and the API serves the fresh state after a tsx-watch reload. Gotchas confirmed on the way:
(a) the API caches ChainConfig for the process lifetime — superseded the same day by the
chain.ts fix below (no restart needed anymore); (b) seeding against a reused backbone
(env addresses) would revert in prepareCondition (Polymarket CTF rejects duplicates) —
pre-existing limitation, fresh deploy is the supported re-run path.
jay hit fillOrder reverts trading after a self-run re-seed. Root cause: loadChain()
cached the ChainCtx for the process lifetime, so the running API kept signing/filling
against the previous backbone’s exchange, where the new market’s token ids were never
registered → every fill reverts. Fixed by re-reading ChainConfig per call and rebuilding
the ctx when the addresses change — a re-seed is now picked up with no API restart.
Full analysis below.
Clicking Buy in the web UI failed with:
The contract function “fillOrder” reverted.
The trade panel looked healthy: “Demo wallet #1 · $1,000 USDC”, markets rendered
normally. Reproducible every time via POST /trade — the API returned
{"error":"The contract function \"fillOrder\" reverted."} with no reason string
(the route handler returns viem’s shortMessage only).
| Step | Actor | Effect |
|---|---|---|
Seed run 1 (dev-local.sh) |
Claude | Backbone A deployed, wallets #1–5 pre-funded (new [2b] step) |
| Seed run 2 (re-run test) | Claude | Backbone B deployed; API restarted around this time and cached B |
Seed run 3 (dev-local.sh) |
jay | Backbone C deployed — DB ChainConfig now points at C |
| Buy in web UI | jay | Revert — the running API still held B |
packages/api/src/chain.ts — loadChain() built the chain context (USDC/CTF/Exchange
addresses + bound viem clients) once and cached it for the process lifetime. The old
comment even documented it: “re-seed requires an API restart (fine for local dev)”.
After jay’s re-seed, requests flowed like this:
web UI ──POST /trade──▶ API (ChainCtx = backbone B, cached)
market row from DB = backbone C (token ids registered on C's exchange)
fill executed on = backbone B's exchange
→ B's exchange has never seen C's token ids → fillOrder REVERTS
The revert is the exchange rejecting an unregistered token id — not a balance, allowance, or funding problem. The UI’s “$1,000 USDC” was equally misleading: the wallet endpoint read balances on B’s USDC token while trades were built from C’s DB rows. Two views, two different chains-of-record, one process.
trade.ts runs unconditionally, independent of the faucet branch.✓ seeded 10),
balances confirmed at exactly 1,000 via cast balanceOf.touch (no content change) did not
reliably restart it, and even a real edit to an imported file (chain.ts) was missed.
Only editing the entry file (src/index.ts) triggered a restart. This unreliability is
why “just restart the API after re-seeding” was never a dependable workflow.The decisive experiment: running executeTrade() directly via tsx (fresh process →
fresh ChainConfig read) succeeded while the same request through the long-running API
reverted — proving the process state, not the chain state, was broken. The fresh
process’s trade also left a fingerprint (990 USDC + a YES position on C) that the API
couldn’t see, confirming it was reading a different backbone.
loadChain() now re-reads the ChainConfig row on every call and rebuilds the context
whenever the stored addresses differ from the cached ones (cheap: one indexed PK lookup
on local Postgres per request). A re-seed is picked up by the running API automatically —
no restart, no tsx-watch dependency.
POST /trade → filled (tx 0x149eaf75…)dev-local.sh (backbone D)POST /trade again, no restart → filled (tx 0x3ee75af6…), faucetMinted: falsetsx watch restarts are best-effort; a fix that removes
the need for a restart beats a procedure that requires one.shortMessage, which drops the revert reason.
Worth a follow-up: include e?.cause?.reason/metaMessages in dev-mode error responses
so the next revert self-describes.Follow-up from the fillOrder analysis (Lessons §”Error surfacing”): the /trade catch
returned only viem’s shortMessage, hiding the revert reason. Added revertDetail() —
walks the error’s cause chain for reason/errorName, falls back to metaMessages —
and a detail field in the error response. Verified live: an oversized BUY now returns
detail: "SafeMath: subtraction overflow"; normal trades unchanged. (Side effect of the
test: wallet #9 holds ~51k faucet USDC until the next re-seed — not visible in the UI.)
Per jay’s request, wrote tasks/details/jul-20-portfolio-resolution-design.md
(moved into the new details/ folder per jay; linked as Task 3 in the jun-19 design’s task map):
(1) Portfolio page for demo wallets (positions + redeem), (2) resolution as inline admin
controls on the market page when operator #0 is the active wallet — recommended over a
separate admin page since “admin” is only account-#0-by-convention. Key finding: SDK
already has reportPayouts/redeem (proven in the CLI demo) and the DB already carries
Market.status/resolvedOutcomeId — the work is API routes + UI wiring, plus a missing
trade guard for resolved markets. Design only; implementation awaiting jay’s go.
Implemented tasks/details/jul-20-portfolio-resolution-design.md.
API: resolve.ts (POST /markets/:slug/resolve operator-only via reportPayouts,
POST /redeem clearing both index sets), walletSummary extended with costBasis/pnl
(Σ BUY − Σ SELL from Trade) + marketStatus/won; trade-guard for non-OPEN markets already
existed. Web: wallet picker gains “Operator #0 (admin)”, MarketSidePanel switches
TradePanel ↔ ResolvePanel ↔ resolved-note, RESOLVED badge on the market header, new
/portfolio page (balance, positions value, open P&L cards; per-position cost/value/P&L,
WON/LOST badges, one-click Redeem). Verified end-to-end in the browser: resolved
fed-rate-below-3-dec-2026 YES as #0 → wallet #1 showed WON +$17.78 (cost $10, value
$27.78) → Redeem → balance $1,009.02 → $1,036.80. eth-above-10k-2026 was also resolved
YES during API testing — two markets are now RESOLVED until the next re-seed.
Per jay: the portfolio should also show trade history (buys/sells) and winning/losing
results. Added REDEEM to the TradeSide enum (db push, additive) and redeemPosition
now snapshots per-outcome holdings and writes REDEEM trade rows (price = payout 1/0), so
redemptions leave a record. New GET /wallet/:index/history returns the full feed with
realizedPnl on REDEEM rows (redeem proceeds − outcome net cost). Portfolio page gains a
4th stat card (Realized P&L) and an Activity card (BUY/SELL/REDEEM chips, won/lost
amounts, timestamps); the market activity feed labels REDEEM rows “Redeemed”. Verified
live on the losing path: bought YES $20 on btc-dominance, resolved NO, redeemed $0 →
history row realizedPnl: -20, card shows −$20.00 red. Note: redemptions made before
this change (eth, fed-rate) predate the recording and aren’t in the feed — gone at next
re-seed anyway. walletSummary cost basis now explicitly excludes REDEEM rows.
Per jay: a dedicated script that wipes all data and deploys fresh contracts. Thin wrapper:
checks anvil reachability (eth_chainId probe) and the verex-pg container, then runs
db:reset (migrate reset + seed → new backbone, 10 OPEN markets, wallets pre-funded).
Explicitly fresh-deploy — reusing the old backbone is unsupported (prepareCondition
reverts on duplicate conditions; DB/chain snapshot mismatch — see today’s analysis).
Verified live twice: 10/10 OPEN, wallet #1 = 1,000/0 positions/empty history, and a trade
filled through the SAME running API process (no restart, thanks to the chain.ts reload).
README’s clean-slate note now points at the script.
jay’s redeem 500’d with invalid input value for enum "TradeSide": "REDEEM". Root cause:
REDEEM was added via prisma db push (no migration file), and reset.sh runs
migrate reset, which rebuilds from migrations — wiping the enum value. Three fixes:
(1) real migration add_redeem_trade_side so resets keep it; (2) the history write in
redeemPosition is now try/catch best-effort — the on-chain redeem is final by then, so a
DB failure must not 500 a successful redemption (that’s exactly what bit jay: the burn
succeeded, +$227.27 arrived, but the API reported an error); (3) backfilled jay’s missing
REDEEM row with the real PayoutRedemption tx hash from anvil logs — history shows
realized +$127.27. Verified with a fresh cycle on wallet #2 (buy $8 → resolve → redeem →
REDEEM row, realizedPnl +7.38). Lesson: never db push a schema change in a repo whose
reset path replays migrations — always write the migration.
All on the running local stack (anvil + verex-pg + api + web), via curl, cast, and the browser — none of it left as “should work”:
dev-local.sh twice; wallets #1–5 exactly 1,000 USDC after
each run (cast balanceOf), proving top-up idempotency.0x149e…, 0x3ee7…)./trade returned
detail: "SafeMath: subtraction overflow"; normal trade unaffected.realizedPnl: −20, red −$20.00 card in the UI.realizedPnl: +7.38; jay’s
backfilled eth redemption shows realized +$127.27 with the real on-chain tx hash.The script header still described the v1 DB-only flow; updated it to state that the seed deploys the CTF backbone and needs anvil running first — matching what the script actually runs today. Not executed end-to-end this session (doc change only); flow derived from seed.ts / package.json / script contents.