Read-only runs-history + scored-trader-leaderboard dashboard over a backend “scout” pipeline that scrapes Polymarket public leaderboards (DAY/WEEK/MONTH/ALL), intersects wallets that appear on ≥min_intervals boards, scores them from resolved-market W/L, and ranks them. Now the default /polymarket landing tab; Copy-Lane and Longshot were KEPT as secondary tabs (?tab=copy / ?tab=longshot) — nothing deleted. This is the third replacement of the polymarket landing this week (Paper-Trading → Copy-Lane → Scout). Merged to master locally as commit e315dba (push pending), branch feat/pmv2-scout. Built via the superpowers brainstorm→spec→plan→subagent-TDD→verification workflow.
For Agents
- Route:
/polymarketis a 3-tab segmented-control shell (web/src/pages/polymarket/index.tsx). Default tab = Scout;?tab=copyand?tab=longshotdeep-link the secondary dashboards.resolveTabhelper maps the URL param.- Scout page:
web/src/pages/polymarket/scout/index.tsx(master-detail; defaults to the latest run) +scout/components/{RunsRail,RunParamsChips,ScoutSummary,LeaderboardTable}.tsx.- Domain core (pure, unit-tested):
web/src/lib/pmv2.ts.- Hooks:
web/src/lib/hooks/polymarket/{usePmv2Runs,usePmv2Traders,usePmv2RunWallets}.ts(30s poll; query keyspmv2Runs/pmv2Traders/pmv2RunWallets).- Data: 4
pmv2_*tables in the CRM’s single Supabase (mkofmdtdldxgmmolxxhc, internally “mgmt” — NO separate client, useuseSupabase(); see paper-trading-dashboard). All RLS-on with*_auth_readpolicies forauthenticated→ app reads directly, NO migration.- Read-only. Runs/traders are produced by the external backend “scout” pipeline (polymarket-fetch).
What it is
A master-detail view answering “which Polymarket leaderboard wallets are consistently good, scored on resolved markets?“. Master = runs history (each row a pipeline run with its params + counts + status). Detail = the scored trader leaderboard for the selected run (defaults to the latest run). No trading, no writes — the backend owns scraping, scoring, and ranking; the dashboard surfaces it honestly.
Architecture (three layers, repo-standard Page → hook → Supabase)
1. Domain — web/src/lib/pmv2.ts (pure, framework-free)
parsePmv2Run/parsePmv2Trader—Number()-coerce numeric-as-string columns (num/numOrNullhelpers).buildWalletNameMap/mergeTraderNames— client-side name enrichment join (see below).deriveRunSummary— KPI rollup for the selected run.- Formatters:
formatScore(score × 100→ signed percent),formatRunDuration,shortWallet,traderDisplayName. - Constant:
LOW_SAMPLE_N = 10— the small-sample mute threshold.
2. Hooks — web/src/lib/hooks/polymarket/
usePmv2Runs— runs history (master rail).usePmv2Traders— scored leaderboard for a run.usePmv2RunWallets— wallet→name rows frompmv2_leaderboard_entriesfor enrichment.- All 30s
refetchInterval; query keyspmv2Runs/pmv2Traders/pmv2RunWallets.
3. Page — web/src/pages/polymarket/scout/index.tsx
Master-detail; defaults to latest run. Components: RunsRail (master list), RunParamsChips (params jsonb as chips), ScoutSummary (KPI rollup), LeaderboardTable (scored traders, low-n rows muted).
Data model (Supabase mkofmdtdldxgmmolxxhc, all 4 pmv2_* tables RLS-on with *_auth_read)
For Agents
No migration was written — these tables already exist server-side. The app reads them directly. They are absent from
database.types.ts(see gotcha below).
pmv2_runs—run_idbigserial PK;started_at/completed_at,status(running/completed/failed),paramsjsonb{top_n, intervals, market_scope, score_method, min_intervals},n_leaderboard_entries,n_intersected,error_message.pmv2_traders— PK (run_id,proxy_wallet), FK→runs CASCADE;rank_day/week/month/allnullable,lt_strength/st_strengthsmallint,tier(core/proven/emerging),wins/losses/n_resolved(can be 0),scorenumeric nullable,rank_in_run.pmv2_leaderboard_entries— PK (run_id,interval,proxy_wallet);rank/pnl/vol/user_name/x_username— read ONLY for wallet→name enrichment.pmv2_market_resolutions— upstream, not read by the dashboard.
Gotchas & reusable lessons
scoresemantics drift between runs — VERIFY against live data, not the spec snapshotRun 1 used
score_method='wl_rate'(a 0–1 win rate); later runs have SIGNED scores (≈ −0.12..+0.07).formatScoredoesscore × 100→ renders signed percents like-11.5%under a “Score” header. The display is correct but mislabels if the score isn’t a win rate. The spec’s run-1 assumption became stale by merge time. When you touch this: confirm the metric semantics across ALL runs in the live data, not just the snapshot you designed against.
New tables are absent from
database.types.tsHooks use
as neveron.from()/.eq(value)to bypass the missing generated types; row typing is preserved via an explicitsupabaseQuery<RawX[]>generic. Matches repo precedent (useDayPlan/useTimesheetExport). Cleaner follow-up: hand-add the 4pmv2_*tables topackages/shared/src/database.types.ts+types.ts(same pattern copy-lane used forpm_copy_config/pm_copy_wallets).
PostgREST numeric (incl.
score) returns as a STRINGAlways
Number()-coerce (num/numOrNullinpmv2.ts).n_resolvedcan be 0 → guardwinRateagainst divide-by-zero.
Small-sample honesty pattern (reused from copy-lane)
Surface
n_resolvedand visually MUTE low-n rows (n < LOW_SAMPLE_N = 10) rather than re-ranking or hiding them. The backend owns scoring/rank — the UI’s job is honesty, not second-guessing.
Name enrichment via a second cheap query
The scored table (
pmv2_traders) lacks display names. Join client-side to a wallet→{user_name, x_username}map built frompmv2_leaderboard_entries(buildWalletNameMap+mergeTraderNames). ≈49/50 wallets resolve to a name; fall back toshortWallet.
Test-mock trap — mock the awaited seam, not
.from()Hooks build
.select().order().limit()eagerly before awaiting the query seam, so afrom: () => ({})mock throws (those chained methods don’t exist on{}). MocksupabaseQuery(the single awaited seam) directly, or use a self-returning chainable builder stub.
If you touch this next
- Confirm
scoresemantics first (the load-bearing gotcha): inspectparams.score_methodand the actualscorerange for every run before trusting the “Score” column header —wl_rate(0–1) vs signed methods render very differently under the same× 100formatter. - Hand-add the 4
pmv2_*tables todatabase.types.ts+types.tsto drop theas nevercasts in the three hooks. - The three landing dashboards (Scout / Copy / Longshot) now coexist in one tab shell — when adding a 4th, extend
resolveTabinweb/src/pages/polymarket/index.tsx, don’t replace the default.
Related
- copy-lane-dashboard — now a secondary tab (
?tab=copy); same tab shell, same small-sample-honesty +as never/supabaseQueryprecedent - paper-trading-dashboard — the Longshot tab (
?tab=longshot) and the canonical “single mgmt Supabase, no separate client” correction - polymarket-fetch — the external backend pipeline that scrapes leaderboards and writes the
pmv2_*tables - integrations — Polymarket integration index entry
- tech-debt —
database.types.tsfollow-up for thepmv2_*tables