Skip to content

test(header): repoint nav tests at the consolidated Trade group (3 failures on playground) - #2459

Merged
dcccrypto merged 85 commits into
playgroundfrom
fix/header-test-nav-consolidation
Aug 1, 2026
Merged

test(header): repoint nav tests at the consolidated Trade group (3 failures on playground)#2459
dcccrypto merged 85 commits into
playgroundfrom
fix/header-test-nav-consolidation

Conversation

@dcccrypto

Copy link
Copy Markdown
Owner

What broke

Header.test.tsx fails 3/3 on playground (5550a19d):

Unable to find an accessible element with the role "menuitem" and name `/Wallet/i`
  ✗ shows Wallet link inside Trade dropdown
  ✗ dismisses Trade dropdown on Escape
  ✗ dismisses Trade dropdown on outside click

Confirmed on the untouched base, so this is not from any open PR.

Why

The nav consolidation (5550a19d) slimmed the Trade group to Markets/Portfolio/Earn — Dashboard, Wallet, My Markets and Stake became tabs of the /portfolio and /earn hubs. Per that commit's own description, the old routes "stay working standalone"; they are simply no longer top-level dropdown entries.

That commit repointed Portfolio.test but not Header.test, which still probes for a Wallet menuitem.

Fix — two different situations, not one blanket rename

test what it actually asserts fix
shows Wallet link inside Trade dropdown a nav entry that was deliberately removed repointed to Portfolio → /portfolio, preserving the intent: an open dropdown exposes its links as menuitems with correct hrefs
dismisses … on Escape dismissal behaviour; Wallet was only a probe for open/closed probe swapped to Portfolio, behaviour assertions untouched
dismisses … on outside click same same

Verification — the dismissal tests still bind

Swapping a probe can quietly make a test vacuous, so I mutation-tested it:

state Escape / outside-click tests
fix applied pass
+ NavDropdown's if (e.key === 'Escape') setOpen(false) and its mousedown listener neutralised FAIL
restored pass

Full suite (app/) with the open test-repair PRs stacked: 2805 passed, 0 failed.

Note

Invisible to CI for the same reason as #2457/#2458 — see #2447: Unit Tests / Integration Tests / Coverage Gate report SKIPPED while the Merge Gate reports SUCCESS.

🤖 Generated with Claude Code

dcccrypto and others added 4 commits July 21, 2026 20:55
Fifth slice of the 65 CI failures #2450 exposed. Fixes 8, adds 10. Test files
only, no product change.

Each suite asserted a contract the route had deliberately moved away from:

- funding-global (4): the route is no longer a pass-through proxy. It is a
  three-tier chain (Railway proxy -> local indexer Postgres -> graceful empty)
  that intentionally does NOT surface upstream errors, so the funding UI
  degrades to "no data" rather than an error state when the backend is down —
  the live situation whenever Railway is dead (#2443). Tests now assert the
  degradation, and cover the indexer-db fallback tier, which had none: it
  serves rows, applies the blocklist, and falls through to empty when the
  query itself fails.

- middleware-rate-limit (3): these set BLOCKED_MARKET_ADDRESSES and expected a
  404. middleware.ts imports `@/lib/blocklist-edge`, which is edge-pure and
  intentionally omits env-var overrides — importing the env-reading
  `@/lib/blocklist` co-bundled Node-only code into the Edge chunk and Vercel's
  deploy validator rejected it. The tests now pin that boundary explicitly
  instead of failing on it: env-injected slabs are blocked at the Node API
  layer only, and blocking at the Edge requires editing blocklist-edge.ts.

- markets-supabase-outage-fallback (1): the devnet static directory was
  migrated to the single v17 wrapper program, so filtering by an old
  per-slab-tier program id matched 0 rows, not 3. Now asserts the filter's
  behaviour rather than a hardcoded count, plus the negative case — without it
  a no-op filter returning everything would still pass.

Also adds __tests__/unit/blocklist-edge-sync.test.ts. blocklist-edge.ts carries
a "KEEP IN SYNC with HARDCODED_BLOCKED_SLABS" comment and nothing enforced it.
The two lists agree today (35 entries each), but drift would leave a slab
blocked at one layer and reachable through the other — and entries there
include a wrong oracle_authority (price-manipulation risk, GH#837). Includes a
size assertion so emptying one set cannot make the diffs pass vacuously.
Mutation-verified: removing one address from the edge list fails it.

95 -> 87 failures, 28 -> 25 files. tsc --noEmit clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pper program

The 2026-07 fee-split merge re-migrated the static DEVNET_MARKET_DIRECTORY_FALLBACK
program from 69VUZ7a2… to the fee-split wrapper DhSkE7uTb8…, so the positive
'filters by program_id' test queried a program no longer in the directory and got
0 rows (expected >0). Updates both program-id constants to the current value; the
negative-case test (unknown program → 0 rows) still guards against a no-op filter.
The nav consolidation (5550a19) slimmed the Trade dropdown to
Markets/Portfolio/Earn — Dashboard, Wallet, My Markets and Stake became tabs of
the /portfolio and /earn hubs. Their standalone routes still work; they are just
no longer top-level dropdown entries. That commit repointed Portfolio.test but
not Header.test, which still probed for a 'Wallet' menuitem and so failed 3/3:

  Unable to find an accessible element with the role "menuitem" and name /Wallet/i

Two different situations, fixed accordingly:
- 'shows Wallet link inside Trade dropdown' asserted a nav entry that was
  deliberately removed. Repointed to Portfolio -> /portfolio, keeping the real
  intent: the open dropdown exposes its links as menuitems with correct hrefs.
- The Escape and outside-click tests only used Wallet as a PROBE for
  open/closed; their subject is dismissal behaviour. Probe swapped to Portfolio,
  behaviour assertions untouched.

Verified the dismissal tests still bind, not just pass: neutralising
NavDropdown's `if (e.key === 'Escape') setOpen(false)` and its mousedown
listener makes both FAIL, and restoring them passes.
@vercel

vercel Bot commented Jul 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
percolator-launch Ready Ready Preview Aug 1, 2026 1:33am
percolator-mainnet Ready Ready Preview Aug 1, 2026 1:33am
percolator-playground Ready Ready Preview Aug 1, 2026 1:33am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7a016325-0450-480d-8ce4-775cd08bd5e7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/header-test-nav-consolidation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…own left

Supersedes this branch's first commit. That one repointed the Trade-dropdown
probe from Wallet to Portfolio, which was correct for 5550a19 but is now wrong:
d6156f5 flattened the desktop nav and REMOVED the Trade and Build dropdowns
outright. On the new base all 4 nav tests fail with

  Unable to find an accessible element with the role "button" and name /Trade/i

New shape: Trade / Earn / Create a Market are plain links, Portfolio moved to the
right beside the wallet, and Community is the only remaining NavDropdown (it
absorbed Developers + Faucet from the old Build group).

Tests updated to match, keeping every behaviour that still exists:
- new 'renders the flat top-level links' pins Trade/Earn/Create/Portfolio as
  links with correct hrefs (coverage the old dropdown tests used to give).
- 'renders Community as the only dropdown trigger' also asserts Build is NOT a
  button any more, so a silent regression back to dropdowns would fail.
- open / Escape / outside-click now exercise Community — the same NavDropdown
  component, just the group that survived.

Mutation-verified, not merely passing:
- neutralising NavDropdown's Escape branch + its mousedown listener FAILS both
  dismissal tests; restoring passes.
- renaming the Portfolio link FAILS the flat-links test; restoring passes.
@dcccrypto

Copy link
Copy Markdown
Owner Author

Updated — the nav changed again under this PR

d6156f5c (feat(nav): flatten top nav) landed after I opened this, and it makes the original fix here wrong, so I've updated the branch rather than leave a misleading PR open.

What changed: that commit removed the Trade and Build dropdowns outright. My first commit repointed the Trade-dropdown probe from Wallet → Portfolio, which was right for 5550a19d but assumes a dropdown that no longer exists. On the new base all 4 nav tests fail, one more than before:

Unable to find an accessible element with the role "button" and name `/Trade/i`
  ✗ renders dropdown group triggers        ← newly failing
  ✗ shows … link inside Trade dropdown
  ✗ dismisses Trade dropdown on Escape
  ✗ dismisses Trade dropdown on outside click

New nav shape: Trade / Earn / Create a Market are plain links, Portfolio moved to the right beside the wallet, and Community is the only remaining NavDropdown (it absorbed Developers + Faucet from the old Build group).

Tests now match, keeping every behaviour that still exists:

test covers
renders the flat top-level links (new) Trade/Earn/Create/Portfolio as links with correct hrefs — the coverage the old dropdown tests used to provide
renders Community as the only dropdown trigger also asserts Build is not a button any more, so silently reverting to dropdowns would fail
open / Escape / outside-click now exercise Community — same NavDropdown component, just the group that survived

Mutation-verified, not merely passing:

mutation result
neutralise NavDropdown's Escape branch + mousedown listener both dismissal tests FAIL
rename the Portfolio link flat-links test FAILS
both restored pass

Full suite with the other open repair PRs stacked: 2806 passed, 0 failed.

dcccrypto and others added 15 commits July 26, 2026 06:26
Fixes the "old vaults" bug and redesigns both Earn tabs to the terminal look.

Data: useEarnStats no longer seeds its list from the hardcoded PLAYGROUND_SLAB_META
(the July-10 SOL/JUP/TRUMP/PENGU/BURNIE born-immortal ghosts). It now sources the
market list from the live /api/markets (same as /markets), unioned with
user-launched registered markets, deduped, gated on a real on-chain LP Vault
Registry read — so the LP Vault tab shows the same live markets as /markets with
real TVL. PLAYGROUND_SLAB_META kept only as a symbol/logo fallback. Fixes a
double "-PERP-PERP" ticker too.

Design: loud "IN DEVELOPMENT" banners → compact honest inline NOTE (APY/APR
genuinely 0%, deposits = counterparty backing / insurance funding). Sharp
terminal styling on vault cards (clear Deposit/Withdraw CTA), the Stake hero
toned down + accent-token button, page-level min-h-screen removed so it sits
cleanly in the Earn hub tab (still renders standalone at /stake).

tsc 0; 22 earn/stake unit tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e, mirrors LP Vault

The Stake page read like a marketing landing page (big "Stake. Earn. / Back the
Fund." hero, DEPOSIT NOW / Learn More CTAs, centered layout). Rebuilt it as a
dense, functional app page in the site's terminal idiom, a sibling of the LP
Vault tab:
- compact "// insurance lp" eyebrow + "Insurance Staking" title (no hero);
- honest inline NOTE (APR genuinely 0%, flushes reduce staked value);
- stats strip (Total Staked / Your Deposits / Active Pools / Avg APR);
- 2-col main+sidebar: pool cards (VaultCard idiom) + Deposit/Withdraw panel +
  Your Positions in the main column; "What Staking Backs" + "How It Works" +
  Risk Notice sidebar (copy matches the verified loss-order: stake pre-funds
  insurance via admin flush);
- sharp corners (rounded-md → rounded-sm/none), mono numbers, uppercase
  micro-labels, accent-only CTAs; compact empty states.
All on-chain logic (deposit/withdraw, cooldown, positions, decodeStakePoolV1)
untouched. Renders in the Earn hub tab and standalone at /stake; no min-h-screen.

tsc 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
OI was rendered ~600× too large on sub-cent markets. buildMarketVaultInfo did
`total_open_interest / 10^decimals`, treating the raw base-asset "Q" quantity
(scale 1e6) as USD — on Percolator ($0.00165) that showed ~$589K instead of the
real ~$971. Now prefer the indexer's `total_open_interest_usd` (already notional
USD), falling back to (raw / 1e6) × last_price. Fixes the OI figure + the
OI-capacity utilization on the Earn page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…p-dex layout)

Thorough LAYOUT redesign, not copy tweaks. Both Earn-hub tabs now mirror the
trade terminal (main content + right action rail), the shape Hyperliquid/GMX/
Drift use for vaults:
- MAIN: a dense, scannable table (LP: Market · TVL · Utilization · Fee · Your
  Deposit; Stake: Pool · TVL · Cooldown · Your Stake · APR), selectable rows
  (accent left-border on the selected row), search + TVL/Volume/Util sort.
- RIGHT RAIL (OrderTicket-analogue): clicking a row binds that vault/pool into
  a sticky Deposit/Withdraw rail with its key figures + your position. First
  row auto-selects so the rail is always bound. Deposit/withdraw logic reused
  unchanged (useInsuranceLP / DepositWidget / cooldown / partial withdraw).
- Explanatory content (How It Works / Insurance / Risk) demoted from a tall
  competing sidebar to a slim secondary strip below the table.
- The "in development" NOTE is removed on both; the honest 0% APY/APR is now a
  small muted caption. Sharp corners, mono numbers, uppercase micro-labels.
New: VaultRow, VaultDepositRail. useInsuranceLP now sources slab context-first
so the rail binds to the selected row (/earn/[slab] route path unaffected).

tsc 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The indexer is now history-only (see percolator-indexer #189). Neutralize reads of
dropped tables/columns so the API stops 500-ing against the new Supabase project;
live OI/price/insurance are already read from chain on the pages.

- funding_history: queryFundingHistory/queryFundingGlobal -> no-op [] (table dropped);
  /api/funding history + global degrade to empty.
- insurance_snapshots: stake/pools computeAprs() -> 0 APR without querying (dropped).
- Trimmed dropped market_stats columns (OI/insurance/vault/mark/index/funding etc.)
  from the SELECTs in markets/stats/prices/stake routes; kept last_price/volume_24h/
  trade_count_24h + registry cols. trades/candles/leaderboard unchanged (kept tables).

tsc clean. Points at the new PercolatorIndexer Supabase project (env set on Vercel).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VkS8j4NDFV7BnpDLLjbbQ
…dentity

Two problems, both fallout from the indexer reduction.

1. The market list was EMPTY in production — /api/markets returned
   {total:0, zombieCount:4} with four live markets in the DB.

   isZombieMarket() keys off vault_balance, c_tot and total_accounts. The
   reduction stopped mirroring those into market_stats (they're read live from
   chain now) and SELECT_FIELDS stopped requesting them, so the keys are ABSENT
   on the row. Every branch read absence as death: `vault_balance ?? null`
   collapses undefined to null, `total_accounts ?? 0` to 0, and the null-vault
   branch then returned true for everything.

   Absent is not zero. isZombieMarket now declines to classify when none of the
   three liveness signals was supplied, and both call sites (/api/markets and
   /api/stats) forward undefined for columns the query never selected instead of
   coercing them to null. Genuinely dead markets are still excluded upstream —
   the indexer's sweep sets status=closed + indexer_excluded on dust-vault slabs
   and both the view and the query filter those. The 56 existing zombie tests
   still pass, so real detection is unchanged.

2. Six unrelated devnet markets all displayed as "SOL/USDC Perpetual".

   v17 admin-oracle markets carry no on-chain pointer to a base asset — no Pyth
   indexFeedId, no DEX pool, no mainnet CA — so the indexer's hyperp branch
   always fell through to its hardcoded baseSymbol="SOL" default. That default
   was the only branch that ever ran for them.

   Market identity now lives in the database, as the one place it can:

   - markets.metadata_source ('auto' | 'manual') marks who wrote symbol/name/
     logo_url. The indexer only ever INSERTs missing slabs, so a human's edit is
     never clobbered by a later discovery cycle.
   - PATCH /api/markets/[slab] sets symbol/name/logo_url and flips the row to
     'manual'. Authed with the same HMAC-SHA256 scheme as the keeper register
     route: secret never crosses the wire, signed timestamp bounds replay, body
     read as raw text and verified before parsing. Fails closed when the secret
     is unset. Values are length-checked and stripped of control characters and
     markup; logo_url goes through the same allowlist the launch flow uses, and
     an explicit null clears it.
   - markets_with_stats had to be dropped and recreated, not CREATE OR REPLACEd:
     it selects m.*, which is expanded to a fixed column list at CREATE time, and
     re-expanding it inserts metadata_source mid-list, which REPLACE rejects.
     Done in a transaction with grants restored.

   The indexer no longer guesses. It resolves the base asset via the DEX pool
   when one exists — taking the base asset's logo, not the collateral's — and
   otherwise writes a neutral UNKNOWN / "Market <prefix>" placeholder rather
   than inventing SOL. A one-off migration resets the already-guessed rows,
   under a predicate narrow enough that it cannot rename a market that really
   is SOL (auto + no pool + no CA + no logo).

Verified against the live database: the guessed rows reset to placeholders, the
rebuilt view exposes metadata_source with grants intact, and it still returns
the four active markets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VkS8j4NDFV7BnpDLLjbbQ
…it SHA

The SDK was pinned to github:dcccrypto/percolator-sdk#497ec33. That commit is
now published as @percolatorct/sdk@4.3.0 — the registry tarball's shasum matches
the artifact built from that exact commit — so this is the same code by a more
reliable route, not a version bump.

Git dependencies are fetched and built on every install, which is why
PLAYGROUND.md carried a standing "if pnpm install fails fetching the SDK, it's
almost always a transient GitHub hiccup — just re-run" caveat and a matching
troubleshooting row. Installing from the registry removes that failure mode, so
both are gone rather than reworded.

Docs updated only where they described the old mechanism (setup section,
troubleshooting row, CLAUDE.md golden rule); nothing else touched.

Verified: resolves from .pnpm/@percolatorct+sdk@4.3.0 rather than a git checkout,
tsc clean, production build succeeds. The test suite is unchanged at 29 failing
files / 92 failing tests — identical to the counts before this change, all in
waitlist / Privy / chart-drawing / stake-hook areas that predate it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VkS8j4NDFV7BnpDLLjbbQ
…and dead proxy

/api/markets had TWO alternative paths — an on-chain one that computed live
price and OI, and a Supabase one that did not — and the Supabase path wins
whenever the DB is configured. So configuring Supabase silently downgraded the
endpoint. Every symptom traced back to that: activeTotal: 0 with four live
markets, totalOpenInterest: 0 while /api/open-interest reported 588,928,150,764
from the same slab, and liveness heuristics reading absent columns as evidence
of death.

The fix is to stop branching. Registry data (identity, logo, 24h volume) comes
from Postgres because only Postgres has it; live state (price, OI, insurance,
vault, c_tot) comes from the chain because only the chain has it. One merged
row, one loader, shared by both routes.

1. lib/live-market-state.ts — batched live read

   One getMultipleAccountsInfo per 100 slabs, using addresses the registry
   already supplied. This REPLACES the getProgramAccounts LP-capital scan that
   ran in the list request path: that call scans the entire program, so its cost
   tracks protocol size rather than market count, and it is the first thing an
   RPC provider throttles. Discovery of new markets belongs to the indexer,
   which already does it.

   vault_balance now carries the engine vault from the market-group header
   rather than LP-portfolio capital. In this route it is only a liveness and
   phantom-OI guard, never a rendered column, and the engine vault is the value
   those guards were written against. The trade-page detail view keeps its own
   single-market LP lookup, which is a different figure.

2. lib/market-registry.ts — one loader, shared

   /api/stats used to HTTP-fetch /api/markets to stay consistent with it (they
   had drifted badly enough to render "161 markets / $237K OI" beside pages
   showing 6). Correct in result, but it costs a second serverless invocation
   and its cold start on every stats request, and duplicates all the RPC work.
   Sharing the loader gets the same guarantee structurally, in process.

3. Third instance of absent-is-not-zero, and the one that hid OI

   isPhantomOpenInterest(accountsCount, vault) treats accountsCount === 0 as
   phantom. total_accounts is no longer mirrored, and callers coerced the absent
   value to 0 — asserting "this market definitely has no accounts". That made
   EVERY market's OI phantom and zeroed it. Unknown now abstains; a KNOWN zero
   still suppresses, and the dust-vault condition (which carries the guard now
   that vault is read live) is untouched. Tests pin both directions.

4. Dead percolator-api proxies removed

   /api/prices and /api/prices/[slab] called ${NEXT_PUBLIC_API_URL}/prices on a
   host that answers "Application not found" — a guaranteed-failing round trip
   on an endpoint useLivePrice polls every 10s per market. The Pyth and
   GeckoTerminal fallbacks were already doing the work and are now the primary
   path. The indexer that replaced that service is ingest-only: /health and the
   Helius webhook, nothing else.

5. stale-while-revalidate 30s -> 60s

   A true split by volatility would need separate identity/live endpoints; not
   worth doubling client requests at this market count. Widening the revalidate
   window is the lever that matters — inside it the CDN answers from cache and
   refreshes in the background, so only the first request after expiry waits on
   origin.

Verified against the live database via a local server on the production env,
diffed field-by-field against the deployed response: no key or market removed,
insurance_fund/insurance_balance added, activeTotal 0 -> 4, marketsWithPrice
0 -> 4, prices and OI populated. Percolator's merged OI is 588928150764 —
an exact match with the dedicated /api/open-interest endpoint reading the same
slab, which independently confirms the parse. Full suite unchanged at the
pre-existing 29 files / 92 tests failing, plus 7 new passing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VkS8j4NDFV7BnpDLLjbbQ
Hides all six rows of the `markets` table from the API and UI: Percolator,
Jimothy, TROLL, the dead SOL slab, the empty BLAHwD5w slab, and ANSEM.

Blocklisted rather than deleted from the DB because neither DB lever holds.
StatsCollector.syncMarkets() re-inserts any on-chain market missing from the
table every COLLECT_INTERVAL_MS (60s) via insertMarketRow({status:"active"}),
so a DELETE reappears within a minute as an UNKNOWN placeholder, minus the
metadata it destroyed. indexer_excluded is self-reverting too: the auto-close
sweep flips it back to false for anything still in the discovery map with
vault > $1 or numUsedAccounts > 0 (StatsCollector.ts:901) — which covers the
one visible bad market, BPgSUbDs (vault 60,496,656,666). This blocklist is the
only lever independent of on-chain state.

Each entry records its verified condition. The two that were actually broken:

- Jimothy — LP vault bankrupt (capital $0.00, pnl -$2,479). Sole negative-PnL
  account, so negative_pnl_account_count=1 pins bankruptcy_hlock_active=1,
  try_clear_bankruptcy_hlock_if_healthy() can never fire, h_lock_lane returns
  HMax, and every favorable action reverts Custom(21).
- SOL/USDC — asset0.slot_last 1,143,246 slots behind (~5.3 days), oracle_epoch=0,
  no matcher-enabled LP portfolio so the recovery cranker has never cranked it.

Percolator and TROLL were healthy at time of writing and are retired with the
lineup rather than for a fault; Percolator's LP was draining ($240.35 ->
$215.91 in ~12 min), on the same trajectory as Jimothy.

Verified: tsc --noEmit clean; markets-slab-blocklist-symbol 10/10. The suite's
29 failing files / 92 failing tests are pre-existing and identical with and
without this change (confirmed by stashing it and re-running).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VkS8j4NDFV7BnpDLLjbbQ
…slabs

394f856 ("blocklist the entire devnet-2.0 lineup") added entries to
lib/blocklist.ts but not to lib/blocklist-edge.ts. Those are two separate
hardcoded sets, deliberately duplicated so the Edge middleware gets an isolated
bundle (the file's own header says KEEP IN SYNC), and middleware.ts imports the
EDGE one:

  app/middleware.ts:26  import { BLOCKED_SLAB_ADDRESSES } from "@/lib/blocklist-edge";

So the canonical set had 41 entries while the Edge set had 35, and the 6 newly
blocklisted devnet-2.0 slabs were still reachable through the middleware:

  7FBXdrm1vQ4ktQJjMwurq4cAHkVB1gKoZ7Hx3CAQv6P4
  8SHhSKuY9cun15Y2Q9p9SNEV86zzSWbeP4e59xLAv99h
  BLAHwD5wZ3Wo6naHD4GTT6zpYFcyLWAviEWR4zT7C36p
  BPgSUbDsxZ9bkauWgd6eQ8oLHVx6pSsvfAjPGsS2Sso8
  CseeeuKKbgNU38VRukG38mTdcPJ4KWci5GmFikEtp1X5
  gHey79gB1xGQyXne8yEHoKmGi6jrEVigLwxSXQrYkD3

Adds those 6, leaving both sets at 41 with zero divergence in either direction.
Entries re-sorted case-insensitively so future diffs stay readable.

Found by the blocklist-edge-sync test in #2456, which exists precisely to catch
this class of drift; verified against that test here (it fails if any one of the
6 is removed again, passes with all present).
…ocklist instead

394f856 blocklisted the entire devnet-2.0 lineup. DEVNET_MARKET_DIRECTORY_FALLBACK
is now a SINGLE entry and that entry is itself blocklisted, so the outage fallback
legitimately returns zero rows on devnet — which broke this test's
`markets.length > 0` premise.

Dropping the emptiness assertion rather than propping it up: it was never the
point, and it would re-break the moment the directory is repopulated. The
filter's real contract — everything returned matches the requested program — is
kept.

To make sure removing it did not leave a vacuous test, added a guard that pins
the behaviour that now actually matters: requesting the blocklisted entry's OWN
program must still return nothing, proving the blocklist filter runs ahead of
the program filter rather than the emptiness being incidental.

Mutation-verified: disabling the fallback's BLOCKED_SLAB_ADDRESSES filter fails
the new guard; restoring it passes.

Note (not fixed here, it is a product call): during a Supabase outage the devnet
fallback now serves zero markets. Flagged separately.
The leverage slider computed its max with BigInt division:
`Number(10000n / initialMarginBps)`, which discards the remainder. For the
standard 1500-bps (15%) market the real engine cap is 10000/1500 = 6.6667x,
but the truncation showed 6x — silently denying traders the last ~0.67x the
engine actually permits. Meanwhile the position panel's "Risk Lev." computed
the honest value and displayed 6.7x, so a max order read as "6" on the slider
and "6.7" once open. Same market, two numbers.

Fix (client-side only; the on-chain cap is unchanged):
- maxLeverageFromOnChain now uses float division, floored to 2 decimals so a
  max order lands at 1501.5 bps >= the 1500 required (clears the margin check
  with a hair of buffer rather than sitting exactly on the edge). Displays 6.7x.
- Made the size math fractional-safe. The old `BigInt(leverage)` and
  `BigInt(Math.round(maxLeverage))` throw / overshoot on a non-integer like
  6.66 — buyingPower and notionalNative now scale by 100 before the BigInt.
- All leverage displays route through formatLeverageValue: integers stay clean
  (1x/3x/5x/6x), the max shows 6.7x.

The market list / wizard still advertise the round floored 6x; only the trade
ticket exposes the true achievable ceiling, as intended.

Verified: tsc clean; leverage-display + wizard suites (28 tests) green; the
exact arithmetic checked directly (1500bps -> "6.7x" -> 1501.5bps -> accepted;
BigInt paths no longer throw on 6.66).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VkS8j4NDFV7BnpDLLjbbQ
Root cause, verified against the deployed engine (2026-07-27):
`withdraw_not_atomic` (percolator/src/v16.rs:14412) returns V16Error::Stale
(Custom 19) UNCONDITIONALLY whenever the account's active_bitmap is non-empty —
i.e. whenever ANY position is open — regardless of oracle/crank freshness. It is
a by-design refusal to release collateral backing a position, NOT a stale-data
condition. Proven: an account whose four cert epochs all match the header still
reverts Custom(19) while a leg is open, and a maintainer crank never clears it.

The app was doing two wrong things:
- Surfacing "the market's data is too stale; needs a maintainer crank" for
  Custom(19) on withdraw — actively misleading; the real remedy is "close first".
- Believing "free margin" (capital − position margin) was withdrawable while a
  position stayed open, and prepending a PermissionlessCrank to "un-stale" the
  withdraw. Neither works: the engine locks the whole account until flat.

Fix (frontend only; the engine behavior is intended and unchanged):
- Block the doomed withdrawal up front when the account has an open position,
  with the true reason ("close your position first"), instead of letting it
  revert on-chain.
- Rewrite the Custom(19) withdraw message and the generic errorMessages.ts copy
  for 19 and 21 to state the real causes (position-open / transient-lag /
  recovery) instead of always asserting "the market needs a re-seed".
- Remove the useless v17 crank-prepend (and its now-unused import + the
  always-false CU flag): it could never help a position withdraw.

NOT a keeper change: the earlier "keeper should crank user accounts" plan was
based on a wrong stale-cert diagnosis and is dropped — cranking cannot empty the
bitmap. Closing (a self-certifying trade) then withdrawing is the supported flow
and works today.

Verified: tsc clean; errorMessages + withdraw suites (76 tests) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VkS8j4NDFV7BnpDLLjbbQ
…uardrails

The launch wizard hardcoded two families of parameters that are written ONCE at
market creation and can never be changed (the matcher has no update instruction;
max_price_move lives in the engine config). Both were verified harmful on devnet.

1. maxPriceMoveBpsPerSlot=1 / maxAccrualDtSlots=500 froze NEW positions for as
   long as the settlement price lagged the oracle — ~17 min after a 26% move.
   Proven causally: a converged market opens fine; push -30% and the same trader
   is refused Custom(21) until the gap closes, with the gap shrinking
   monotonically under cranking (2461 -> 2205 bps).

2. The LP had NO guardrails: impactKBps 0, maxFillAbs and maxInventoryAbs at
   i128::MAX, skewSpreadMultBps 0 — a free, unlimited, fixed-price counterparty.
   That is how Jimothy's LP reached $0 capital / -$2,479 pnl.

lib/market-params.ts now derives all of it from the only things a creator picks
(leverage + fee split). Trading fee stays fixed; price-move, accrual window and
LP caps are never shown or typed in.

The solvency envelope makes this a TRADE-OFF, not free headroom. An earlier
reading of `<= 10_000` was wrong: that bound applies only to a special-case
early return. The binding limit is scaled by maintenance margin — bisected
on-chain at `price_move x window <= 500` for 1500-bps margin, with every
combination at 750+ REJECTED by InitMarket. The rate limit is a solvency
guarantee (it stops a price move outrunning liquidation), so higher leverage
buys less of it. MAX_PRICE_MOVE_BY_MARGIN encodes the bisected maxima.

Also removes the basis for the 6.67x leverage floor: the July bisection
concluded "10x fails", but re-testing shows 10x fails ONLY when paired with the
old 1x500 budget. With a compatible budget (4x100) 10x is accepted. Leverage was
never the problem, so creators can now be offered 2x-10x.

Verified on devnet:
- every leverage 2x..10x produces an InitMarket the program ACCEPTS (bisected).
- a market launched with the derived config reads back on-chain as
  max_price_move=6, max_accrual_dt=100, max_fill_abs=600000000,
  max_inventory_abs=2400000000 (was i128::MAX).
- the fill cap BITES: a 300M order lands, a 900M order is refused, so the LP can
  no longer be loaded without limit in a single trade.
- tsc clean; wizard + leverage-display suites (28 tests) green.

Not yet wired: the leverage picker UI (creator still supplies initialMarginBps),
and skewSpreadMultBps is derived but unverified on-chain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VkS8j4NDFV7BnpDLLjbbQ
dcccrypto and others added 23 commits July 30, 2026 18:12
… 1.5s

resolveTokenLogo runs two sequential 5s-timeout fetches, and it sat immediately
before the DB write — so a slow logo API could add up to 10s to a launch. This
route is awaited between M3a and M4, so that was squarely on the critical path.

It now starts before the two RPC round-trips this route already makes (slab
ownership + pool classification) and is awaited with a 1.5s deadline at the
write, by which point it is normally already resolved. Past the deadline the
market registers without a logo rather than making the user wait — the success
screen already offers a manual upload.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fc1ENe39ToovDfqGWw2mLT
…devnet launch

The Step 3 gate read `wizard.oracleType === "keeper"`, but nothing sets that
field until handleParametersContinue runs — the Continue handler itself. Its
initial value is "admin", so on devnet:

  registrable = false -> step3Valid = false -> Continue disabled
  -> the click that would have set "keeper" can never fire

Every devnet market launch has been impossible since the gate landed, and
silently so: notRegistrableReason was computed but only ever surfaced in an
alert() at launch, which the dead button made unreachable.

Derive the oracle once, from detection (`resolvedOracleType`), and have both
the gate and the Continue handler read it. Same rule, evaluated before the
click instead of by it. A token with no supported pool is still refused —
that part was correct and is unchanged.

Also surface the reason: StepParameters takes `blockedReason` and renders it
above the buttons. Every other blocker on that form is visible at the field
that causes it; this one has no field, so a dead button was the whole UI.

Fixes isAdminOracle too, which read the same stale field and so labelled every
keeper market as admin-priced.

tsc clean; 2846 tests pass (14 pre-existing Privy/wallet failures unchanged,
verified by re-running them with these two files stashed).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fc1ENe39ToovDfqGWw2mLT
Step 3's Continue could be greyed out with no explanation anywhere on the page.
Most of step3Valid's clauses are visible at the field that causes them, but an
invalid fee split, a fee/margin conflict, an unresolved opening price and an
unregistrable token are not — from the user's side that is indistinguishable
from the app being broken.

Adds a reason for every clause, in the order step3Valid evaluates them, routed
through the blockedReason banner StepParameters already renders.

Also drops a duplicate prop I had just added for the same purpose: blockedReason
was already declared, rendered and passed — I did not check before adding
notRegistrableReason alongside it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fc1ENe39ToovDfqGWw2mLT
Two bugs, both from treating "keeper" as if it were neither admin nor pyth.

1. A PYTH ORACLE ACCOUNT ON EVERY KEEPER CRANK. The gate was
   `!isAdminOracle && !isHyperpOracle`, which is TRUE for a keeper market — and
   a keeper market's `oracleFeed` is the mainnet DEX POOL address, not a Pyth
   hex feed id. So every keeper launch derived a push-oracle PDA from a pool
   address and appended that account to its crank. Keeper markets are
   AUTH_MARK/admin-mode on chain with no Pyth account at all. Now gated on
   `oracleMode === "pyth"`, in both the batched and sequential paths — the
   source-scan test caught that the first fix had only covered one of them.

   The v12 `!isV17Slab` branch has the same shape but is unreachable for v17
   markets, so it is left alone.

2. oracle_authority WAS NULL FOR EXACTLY THE MARKETS THE KEEPER DRIVES. The
   registration payload gated on `isAdminOracle` (oracleMode === "admin"), so a
   keeper market recorded no authority — despite the delegation to the crank
   wallet being the entire point of the mode. Fauci's row shows the symptom.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fc1ENe39ToovDfqGWw2mLT
…d Meteora

Two reasons a newly launched market had no live price.

1. THE FEED'S MARKET LIST WAS PINNED AT DEPLOY TIME. local-price-ws-server
   hardcoded six slabs — four Pyth majors plus two pump.fun pools — from the
   2026-07-10 re-seed. Any market created afterwards was in neither map, so the
   socket streamed nothing for it and the price only moved when the page
   re-read on-chain. From the user's side that is indistinguishable from a
   broken feed, and it is why the ticker "stopped working" on new markets while
   the old ones were fine.

   The list is now the seed plus every keeper_status='active' row in the
   database, refreshed every 60s — the same source of truth the oracle keeper
   reads, so a market ticks as soon as it registers with no redeploy. Pool
   dexType is resolved from each pool's on-chain owner and cached; a query
   failure keeps the previous list rather than going silent.

2. THE SAME WSOL->USD BUG AS THE KEEPER, ON THE DISPLAY SIDE. dexPoolReader
   called computeDexSpotPriceE6 for meteora-dlmm with no solPriceE6, and the SDK
   only applies that conversion for pumpswap — so a WSOL-quoted Meteora market
   would have rendered a SOL-denominated price, low by the whole SOL/USD rate
   (~80x), exactly as the on-chain feed did before 2026-07-29. Converted via the
   e12 reading for the same precision reason as the keeper fix.

Verified locally against the live database: "market list: 2 -> 4 (2 from db)",
then both new markets streamed — CATE $0.004441 against DexScreener $0.004496.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fc1ENe39ToovDfqGWw2mLT
…Overview

A closed position still has a portfolio account in its market (positionSize 0,
rendered "Flat", often holding idle capital), so `usePortfolio().positions`
returns it. The dashboard Overview counted/listed that raw array unfiltered, so
a market the user had closed kept showing as an active position — e.g.
"OPEN POSITIONS (2)" and "ACTIVE POSITIONS 2" with a Flat row for a closed
market.

The canonical open/flat split already exists elsewhere: PortfolioPositionsView
separates open positions from idle deposits via `account.positionSize !== 0n`,
and the site-wide PositionsBar filters the same way. The dashboard components
simply never applied it.

Fix: add a shared `isOpenPosition(pos)` helper in usePortfolio (keyed on
`account.positionSize !== 0n`, matching the existing canonical definition so all
open-position surfaces agree) and apply it in the four Overview components that
count or list positions:
  - PositionSummary — the "Open Positions" list + count
  - DashboardHeader — the "Active Positions" stat
  - PnlChart       — the "Across N positions" caption + empty-state gate
  - StatsBar       — stats + per-market fee readout + empty-state checks
    (win/loss already ignored flat rows via the >0/<0 PnL test; this also
    corrects the length-based cases and is memoized to stay referentially stable)

Filtering is done consumer-side, not at the hook, so surfaces that legitimately
need every account (idle deposits, my-markets) are unaffected. A funded-but-flat
account (idle deposit) is correctly treated as NOT open; an all-flat book now
shows the proper empty state.

Tested: tsc clean; new isOpenPosition unit suite (6 cases: open long/short,
closed size-0, funded-but-flat idle, missing account, mixed-book filter) green;
the one pre-existing AtRiskBanner test failure reproduces identically on the
untouched baseline (unrelated to this change).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CATE's LP was drained to $0 by real on-chain bot churn (two-level ±1.6%
oscillation) that the engine ratchets into one-way losses; the market sits
in a bankruptcy hlock that can never clear and blocks every user's close
and withdraw. Percolator never traded but was launched on the same raw-spot
feed. Both retired so the board restarts on the median-smoothed AuthMark
keeper (percolator-oracle-keeper 2a3aa87/cbeddf0). Both lists + edge copy
updated; sync tests 24/24.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CsKicDEup62NamSzUnEGXJ
…ied 2026-07-30)

Applied to the live DB during the registration consolidation; the file was
never committed. Recorded so the migration history matches the schema.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CsKicDEup62NamSzUnEGXJ
…-cap rejections

The ticket already blocked orders over the matcher's per-trade cap
(maxFillAbs), but the SECOND ceiling — the LP's net-inventory cap
(maxInventoryAbs) — was invisible: a one-sided market fills up and from then
on same-direction orders revert with the same bare InvalidAccountData while
the user "just keeps trying and doesn't know what's wrong".

- lib/matcherCaps.ts: cache the matcher-context ADDRESS (immutable like the
  caps) so live reads are one getAccountInfo; parseMatcherInventory reads
  the LP's signed inventory_base (ctx-relative 96, i128); getMatcherInventory
  fetches it fresh — never cached, every fill moves it.
- lib/marketCapacity.ts (new): remainingSideCapacityQ /
  wouldExceedInventoryCap with the vamm.rs sign conventions
  (lp_inventory_delta = -fill_size) documented and pinned by exact-value
  tests, including crossing zero to the far bound and the live CATE numbers.
  Mutation-verified: flipping the long/short formula fails 7 tests.
- hooks/useMarketFillCap.ts: returns caps + live inventoryBase (immediate
  read + 20s pollWhenVisible, in-flight guarded).
- OrderTicket: a "Max per trade / {side} capacity left" row lives under the
  size input so the limits are visible BEFORE they're hit (values turn red
  when the current order violates them), and a direction-aware blocking
  banner explains the inventory case ("market can only absorb ~X more long
  exposure — reduce size or wait for shorts/closes") instead of letting the
  matcher reject.

Suite: marketCapacity 13/13, blocklist 24/24, tsc clean. The 4 failing files
(ConnectButton/Header/Portfolio/useWallet) fail identically on the base
commit — pre-existing, untouched by this change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CsKicDEup62NamSzUnEGXJ
…l the phantom warmup bar

Two halves of "the user just keeps trying and doesn't know what's wrong":

CLOSES COULD NOT EXCEED THE FILL CAP. A position can legitimately be 4x the
matcher's per-fill cap (maxInventoryAbs = 4 x maxFillAbs; it was built over
several trades), but Close sent ONE TradeCpi for the full size — the matcher
clamps without partial-fill, the wrapper rejects, and "Close 100%" on any
large position failed forever with a bare InvalidAccountData.

- lib/closeChunks.ts: near-equal leg split (no dust leg to trip min-size
  floors), sign-preserving, exact-sum — pinned by exact-value tests
  including the 4x structural maximum and CATE-scale numbers.
- useTrade: optional `sizes` legs; >1 leg builds BatchTradeCpi (tag 67) —
  same 7 accounts, several matcher fills, ONE signature. Sum invariant
  enforced at runtime; compute budget scales 600k + 250k/extra leg.
- useClosePosition: chunks via getMatcherCaps (cached ctx address, one
  account read); caps-read failure degrades to the old single-leg path.
- ClosePositionModal: "Closes as N fills" explainer (per-market mounts pass
  maxFillAbs; cross-market lists hide the line but still chunk correctly).

THE PHANTOM "TIMER". WarmupProgress was built for the v12 engine; on v17 the
/api/warmup endpoint 501s permanently, but only 404 stopped the 5s poll — so
every v17 position flashed the loading skeleton every 5 seconds forever: a
timer that appears and vanishes eternally. 501 is now terminal like 404, and
the skeleton only shows while the FIRST answer is pending. The v17 engine
has no per-position warmup (verified in v16.rs: h lanes gate margin credit
under stress, not a time ramp), so rendering nothing is the honest state.

tsc clean; closeChunks 7/7 + marketCapacity 13/13; full suite unchanged
(the 4 failing wallet/header files fail identically on the base commit).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CsKicDEup62NamSzUnEGXJ
…ed-cap sentinel, caps TTL+invalidation, poll hygiene, DataView reads

From tonight's three-agent audit of the session's changes:

- useDeposit throws on blocklisted slabs: the trade page still renders on a
  direct URL and deposits into a retired market can be UNRECOVERABLE (a
  bankruptcy-hlocked market rejects every withdrawal — exactly how CATE
  died). Withdrawals stay ungated; no new money gets in.
- marketCapacity: maxInventoryAbs == 0 means UNLIMITED on-chain (vamm.rs
  v3-compat), not zero capacity — returning 0 hard-blocked every order on
  such a market in both directions. Now an unlimited sentinel; capacity row
  hides, nothing blocks.
- matcherCaps: the forever-cache premise was wrong — SetMatcherConfig (68)
  can re-point/disable the LP's matcher config any time, leaving closes
  chunked by a dead cap for the whole session. Caps + ctx address now carry
  a 5-min TTL and invalidateMatcherCaps() heals immediately on any trade or
  close failure (wired into useTrade's catch). Reads converted to DataView:
  Buffer.readBigUInt64LE does not exist on the webpack Buffer polyfill, and
  the resulting throw was swallowed into "no caps" — silently disabling the
  entire safety layer in a webpack build while Node tests stayed green.
- useClosePosition humanizes errors with the "trade" context — its failures
  (including the single-leg fallback) surfaced the exact bare
  "Invalid account data" copy this workstream exists to kill.
- useMarketFillCap: state resets on market switch (market B briefly
  validated against market A's caps), and the 20s inventory poll starts only
  once caps resolve — a matcher-less market (v12/mock/broken) was running a
  full getProgramAccounts scan every 20s per mounted component, forever.
- useTrade: multi-leg batches hard-fail on non-v17 markets (tag 67 does not
  exist on the v12 wrapper; unreachable today, cheap to make structural).

tsc clean both repos; marketCapacity+closeChunks 20/20; full suite unchanged
(4 pre-existing wallet/header failures, identical on base).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CsKicDEup62NamSzUnEGXJ
… crowd can hit the LP exposure cap too

Audit F4: chunking only handled the per-fill cap; a close that pushes LP
inventory FURTHER (closing a long while the LP is already long) still gets
clamped by the matcher and rejected bare. Now the close pre-checks the
chosen side's remaining capacity and fails with the real reason and the
closeable percentage. Capacity errors rethrow; read failures degrade to the
old single-leg path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CsKicDEup62NamSzUnEGXJ
… longer masquerades as live

Audit F1.3/#2: /trade/<retired-slab> rendered the full live terminal with a
silently frozen price. The page still renders (it is the only self-serve
surface for funds stuck in a retired market) but now carries a close-and-
withdraw-only banner, and the OrderTicket blocks new positions with the real
reason as its highest-priority validation issue.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CsKicDEup62NamSzUnEGXJ
…oss-repo blocklist tests

Audit F1.1 (HIGH): every portfolio surface filters through the blocklist,
so a user with deposits in a RETIRED market saw 'no positions' and a total
that silently excluded their money (the CATE depositors' ~$990).
useRetiredHoldings bypasses market discovery entirely — one wallet-wide
getProgramAccounts scan — and the RetiredHoldings section renders only when
blocklisted markets actually hold the user's capital or an open position,
linking to the trade page's close/withdraw-only mode.

Plus two audit-verification suites: matcherCaps cache TTL/invalidation
semantics (fake-timer driven, counts real RPC calls; a forever-cache
regression fails) + byteOffset-robust DataView parsing; and the cross-REPO
blocklist sync check (app ⊆ indexer, mutation-verified — removing one
indexer entry fails it; skips loudly when the sibling checkout is absent).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CsKicDEup62NamSzUnEGXJ
… the blocklist

Audit #6: a client subscribing to a RETIRED slab was replayed the last
pre-retirement price stamped with a fresh timestamp — a frozen price
presented as live — and pinned seed entries streamed forever regardless of
retirement. Removal now evicts the cache, and the seed list is filtered
through the blocklist on every refresh.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CsKicDEup62NamSzUnEGXJ
… slate complete

78enGzvj… and CZBmJF8m… are small-tier (8538B) shells abandoned 2026-07-22
with vault=0, insurance=0, c_tot=0 — no user funds, never traded. With these
all 33 market slabs on wrapper DhSkE7uT… are retired, so the board is empty
from discovery down and only newly-launched markets can appear.

(The indexer's '62 markets' double-counts: the v17 and v12 scanners both run
over the same program, so 33 unique slabs surface twice.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CsKicDEup62NamSzUnEGXJ
…elete the duplicated copy

Answering 'why am I editing so many files': the app kept TWO hand-maintained
copies of every blocked address. The reason was real but fixable — the Edge
middleware cannot import lib/blocklist.ts (it layers process.env overrides on
top and is pulled in by many Node-runtime route handlers, so Vercel's Edge
validator rejects the build), and the old workaround was to duplicate the
whole array in blocklist-edge.ts with a test to catch drift.

Now lib/blocklist-data.ts holds the addresses as PURE DATA (zero imports,
zero side effects, no process.env) and both consumers import it. The edge
bundle stays clean because there is nothing in that module to make it dirty,
and drift is impossible by construction rather than caught after the fact.
blocklist-edge.ts is 23 lines and holds no addresses; blocklist.ts keeps only
the env-override layering.

Verified: tsc clean, all 20 blocklist tests pass unchanged, and a full
 compiles the middleware — the exact check the duplication
existed to satisfy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CsKicDEup62NamSzUnEGXJ
fix(nav): point the "Trade" nav link at the markets browser, not a default market

Reviewed: diff read in full, no overlap with the 2026-07-31/08-01 blocklist,
retired-market and matcher-caps work on this branch. Merged locally after
verifying tsc + the full app suite, because the PR's own CI reported the
Vercel checks as 'Authorization required to deploy' and the test jobs as
'skipping' — no usable signal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(trade): hide the analytics dock while the footer is on screen

Reviewed: diff read in full, no overlap with the 2026-07-31/08-01 blocklist,
retired-market and matcher-caps work on this branch. Merged locally after
verifying tsc + the full app suite, because the PR's own CI reported the
Vercel checks as 'Authorization required to deploy' and the test jobs as
'skipping' — no usable signal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(theme): brighten the dim dark-mode text tiers for legibility

Reviewed: diff read in full, no overlap with the 2026-07-31/08-01 blocklist,
retired-market and matcher-caps work on this branch. Merged locally after
verifying tsc + the full app suite, because the PR's own CI reported the
Vercel checks as 'Authorization required to deploy' and the test jobs as
'skipping' — no usable signal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(portfolio): exclude closed (size-0) positions from the dashboard Overview

Reviewed: diff read in full, no overlap with the 2026-07-31/08-01 blocklist,
retired-market and matcher-caps work on this branch. Merged locally after
verifying tsc + the full app suite, because the PR's own CI reported the
Vercel checks as 'Authorization required to deploy' and the test jobs as
'skipping' — no usable signal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
a42d9eb repointed the Trade CTA from /trade (which auto-redirected into one
arbitrary default market) to /markets, and relabelled it 'Trade — browse all
markets'. This test still asserted the old aria-label and href, so it failed with

  Unable to find an accessible element with the role "link" and name /Trade terminal/i

Updated to the new label and target. Matching the full label rather than a bare
/Trade/i keeps it from also matching the mobile menu's own 'Trade' entry.

Mutation-verified: reverting the CTA's href to /trade fails this test; restoring
/markets passes.
@dcccrypto
dcccrypto merged commit cdc5129 into playground Aug 1, 2026
14 checks passed
dcccrypto added a commit that referenced this pull request Aug 1, 2026
ci: actually run the app test suite, and stop the Merge Gate certifying skips (#2447)

Merged AFTER the test-fix PRs (#2451/#2458/#2459) so the newly-enforcing
Merge Gate switches on against a green suite rather than the 4 files that
were failing before them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
dcccrypto added a commit that referenced this pull request Aug 1, 2026
ci: enable CodeRabbit auto-review on PRs based on `playground`

Merged AFTER the test-fix PRs (#2451/#2458/#2459) so the newly-enforcing
Merge Gate switches on against a green suite rather than the 4 files that
were failing before them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dcccrypto
dcccrypto deleted the fix/header-test-nav-consolidation branch August 1, 2026 16:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants