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: /polymarket is a 3-tab segmented-control shell (web/src/pages/polymarket/index.tsx). Default tab = Scout; ?tab=copy and ?tab=longshot deep-link the secondary dashboards. resolveTab helper 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 keys pmv2Runs / pmv2Traders / pmv2RunWallets).
  • Data: 4 pmv2_* tables in the CRM’s single Supabase (mkofmdtdldxgmmolxxhc, internally “mgmt” — NO separate client, use useSupabase(); see paper-trading-dashboard). All RLS-on with *_auth_read policies for authenticatedapp 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 / parsePmv2TraderNumber()-coerce numeric-as-string columns (num / numOrNull helpers).
  • 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 from pmv2_leaderboard_entries for enrichment.
  • All 30s refetchInterval; query keys pmv2Runs / 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_runsrun_id bigserial PK; started_at/completed_at, status (running/completed/failed), params jsonb {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/all nullable, lt_strength/st_strength smallint, tier (core/proven/emerging), wins/losses/n_resolved (can be 0), score numeric nullable, rank_in_run.
  • pmv2_leaderboard_entries — PK (run_id, interval, proxy_wallet); rank/pnl/vol/user_name/x_usernameread ONLY for wallet→name enrichment.
  • pmv2_market_resolutions — upstream, not read by the dashboard.

Gotchas & reusable lessons

score semantics drift between runs — VERIFY against live data, not the spec snapshot

Run 1 used score_method='wl_rate' (a 0–1 win rate); later runs have SIGNED scores (≈ −0.12..+0.07). formatScore does score × 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.ts

Hooks use as never on .from() / .eq(value) to bypass the missing generated types; row typing is preserved via an explicit supabaseQuery<RawX[]> generic. Matches repo precedent (useDayPlan / useTimesheetExport). Cleaner follow-up: hand-add the 4 pmv2_* tables to packages/shared/src/database.types.ts + types.ts (same pattern copy-lane used for pm_copy_config/pm_copy_wallets).

PostgREST numeric (incl. score) returns as a STRING

Always Number()-coerce (num / numOrNull in pmv2.ts). n_resolved can be 0 → guard winRate against divide-by-zero.

Small-sample honesty pattern (reused from copy-lane)

Surface n_resolved and 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 from pmv2_leaderboard_entries (buildWalletNameMap + mergeTraderNames). ≈49/50 wallets resolve to a name; fall back to shortWallet.

Test-mock trap — mock the awaited seam, not .from()

Hooks build .select().order().limit() eagerly before awaiting the query seam, so a from: () => ({}) mock throws (those chained methods don’t exist on {}). Mock supabaseQuery (the single awaited seam) directly, or use a self-returning chainable builder stub.

If you touch this next

  1. Confirm score semantics first (the load-bearing gotcha): inspect params.score_method and the actual score range for every run before trusting the “Score” column header — wl_rate (0–1) vs signed methods render very differently under the same × 100 formatter.
  2. Hand-add the 4 pmv2_* tables to database.types.ts + types.ts to drop the as never casts in the three hooks.
  3. The three landing dashboards (Scout / Copy / Longshot) now coexist in one tab shell — when adding a 4th, extend resolveTab in web/src/pages/polymarket/index.tsx, don’t replace the default.
  • copy-lane-dashboard — now a secondary tab (?tab=copy); same tab shell, same small-sample-honesty + as never/supabaseQuery precedent
  • 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-debtdatabase.types.ts follow-up for the pmv2_* tables