GUNZscope
v0.37.0 // EARLY ACCESS

Version History

Early Access · Public Development Log

GUNZscope is under active development. This page tracks every meaningful release since the first commit. Features ship fast — if something’s missing, request it.

v0.37.0

Current

September 8, 2026

  • v1 supply: itemId and assetKey added to free and pro item shapes. New optional resolveRetired=1 on /supply/item and batch resolves rarity labels retired by upstream relabels (recorded in rarityHistory) to the surviving item, returned under its current rarity with matchedVia and queriedRarity. Default behaviour unchanged. Dedupe on itemId when querying multiple rarities per name.
  • feat(api-v1): ItemIdentityFields (itemId = game_items.id, assetKey) on FreeItemDto and ItemDto in lib/server/apiV1Supply.ts, selected by both select constants, so /item, /batch and /rankings rows carry them in both namespaces. Background: the 2026-09-04 fragmentation merge (v0.29.0) left 198 of one partner's itemName plus rarity keys pointing at labels that now live only in game_items.rarityHistory; the read-only investigation of 2026-09-08 found no catalogue loss and no stale canonical rarity
  • feat(api-v1): parseResolveRetired, MatchedDto, matchRarity and loadBatchResolvingRetired in lib/server/apiV1SupplyRoutes.ts. With the param on and a rarity given, the exact name's rows are read once with rarityHistory alongside (never a scan without the itemName predicate); live rows under the label win outright, otherwise the row whose history carries the label with the latest lastSeenAt is returned as matchedVia "rarityHistory" with queriedRarity, counters untouched. Batch keys a fallback row under the name plus the rarity the caller sent. Rankings are not affected. Off-path queries and memory cache keys are byte-for-byte unchanged; the on-path gets its own cache key
  • test: apiV1RetiredRarity.test.ts covers parsing ("1" and "true" only), the unchanged default query shapes, live match provenance, single and two-candidate history resolution (latest lastSeenAt wins), live-beside-history (Kochi Tactical Flash Hider rarity=Epic returns the live Epic SKU only), name-only queries, and batch match-back by queried key with the param on and off
  • docs(developers): free and pro /item and /batch entries document resolveRetired, matchedVia, queriedRarity, the two identity fields and the dedupe-on-itemId warning; samples restate every new field
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.37.0, package.json and the two package-lock.json root slots to 0.37.0

v0.36.0

September 8, 2026

  • feat(origins): Gone Before the Splash Lands 71 (PRO) and Vulture Adaptive Barrel 74 (FREE), keyed on row id, both already under swampjaw-bp so neither changes release. They were the last two rows still carrying a claim ordinal (27 and 28) rather than a printed tier. Nothing created, nothing deleted; counts stay nullface-bp 14, swampjaw-bp 14, dunewalker-bp 17, total 756
  • feat(origins): the modelled pass is complete. 45 chapter rows, 45 printed tiers, zero rows left on a claim ordinal. 44 of those tiers come from PRINTED_TIERS; the 45th is the SwampJaw Jetpack, created with its tier by NEW_ITEMS in PR #412. Repeating Rewards (121 to 130) stays unmodelled, holding boost cards and cases only
  • note(origins): tierIndex integrity assertion (b), no unlabelled row sharing a number with a printedTier in the same release, is now VACUOUS on all three chapters, since no claim ordinals remain to collide. It is retained deliberately and must not be removed: it fires again the first time a release seeds rows on claim ordinals alongside transcribed ones, which is how it caught Nullface Pants sitting on printed tier 11 in v0.35.0. Assertion (a), no duplicate tierIndex, still does real work
  • note(origins): isPremium remains null for every printed tier at or below 32. The client shows the FREE/PRO badge only while a tier is locked and swaps it for a claim checkmark once claimed, and the transcribing account had claimed through 32. Closing that gap needs an account that has not claimed those tiers, not another screenshot from the same one
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.36.0. package-lock.json touched in the two root slots only; a blanket version replace would have caught 17 @img/sharp packages that also sit at 0.35.0

v0.35.0

September 8, 2026

  • feat(origins): printed tiers 1 to 70 transcribed from the game client. 23 rows gain a printedTier with tierIndex set to the same value, keyed on item_origin_items.id. Nullface chapter 1 to 40 is now fully transcribed at 14 of 14 rows; SwampJaw gains 9 more. Combined with PR #412 the pass carries 43 printed tiers across the three chapters
  • feat(origins): The Bog Remembers Its Own (42) and Still Waters, Louder Truths (46) move nullface-bp to swampjaw-bp with their ids intact. Both sat under Nullface on a claim ordinal while their printed tiers fall in the SwampJaw chapter; the client confirms the boundary, tier 40 reads NULLFACE and tier 41 reads SWAMPJAW. Same class of error as the three Dunewalker rows in #412, in the other direction. nullface-bp 16 to 14, swampjaw-bp 12 to 14, total unchanged at 756
  • fix(origins): the ordering assertion (max claim ordinal strictly below min printedTier) is removed, not worked around. It encoded a property that held only because #412 labelled the TAIL of the pass, so every printed tier happened to sit above every claim ordinal. Labelling the head makes it fail by construction, since no claim ordinal can sort below printed tier 1, and it would have rejected correct data
  • feat(origins): replaced by two tierIndex integrity checks per release, both blocking and both walking every release that has any printed tier: (a) no two rows share a tierIndex, (b) no row still on a claim ordinal sits on a number that is a real printedTier in the same release. (b) caught a genuine collision on its first run, Nullface Pants on claim ordinal 11 against the real printed tier 11 of Between One Breath and the Next, and refused the apply until that row had a real tier
  • docs(origins): provenance recorded on the schema fields and the seed structure. printedTier and isPremium are transcribed by hand from screenshots and exist nowhere on chain, in the metadata service, in rawTraits or in any tokenURI, so there is no drift check against a source of truth. printedTier is cross-checked only by ordering and chapter boundaries; isPremium has no cross-check at all, since a claim mint is byte-identical for a FREE and a PRO tier
  • feat(origins): isPremium is null for every printed tier at or below 32 by necessity. The client shows the FREE/PRO badge only while a tier is locked and swaps it for a claim checkmark once claimed, and the transcribing account had claimed through 32. Tier 33 (Voidbite: Blackout) is the lowest tier carrying a non-null track. Two swampjaw-bp rows remain unlabelled, Gone Before the Splash Lands (27) and Vulture Adaptive Barrel (28)
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.35.0

v0.34.0

September 7, 2026

  • feat(schema): item_origin_items gains three nullable columns, migration 20260908000000. assetKey pins an origin row to one game_items row; printedTier is the tier number read off the game client; isPremium is the FREE/PRO track. tierIndex keeps its meaning as the ordering key. Generated offline with prisma migrate diff against the HEAD datamodel: three ADD COLUMN clauses, no index, no constraint, no backfill
  • fix(sets): loadReleaseItemMap resolved item_origin_items to game_items on lower(itemName) alone, which is ambiguous once one name covers two rows. An origin row with a non-null assetKey is now matched on that key and the name is not consulted; a null assetKey keeps the name-only match, which is 735 of 756 rows. A pinned row matching nothing resolves to nothing rather than falling back to the name. originMatchesGameItem is the pure mirror and a drift guard asserts the emitted SQL carries both halves of the predicate
  • feat(origins): season 3 printed tiers. OTG Vanguard is one 130-tier pass (Nullface 1 to 40, SwampJaw 41 to 80, Dunewalker 81 to 120, Repeating Rewards 121 to 130 not modelled), not three chapters numbered from 1. PRINTED_TIERS carries 19 transcribed rows keyed on item_origin_items.id with printedTier, isPremium and the release the row belongs under; tierIndex is set to the printed value so ordering and label agree. Rows outside 71 to 120 keep their claim ordinal with printedTier null rather than carry a guess
  • feat(origins): Partisan Adaptive Stock (81), Second Sunrise (84) and Ichnya Adaptive Barrel (86) move swampjaw-bp to dunewalker-bp with their ids intact. releaseSlug is in no unique index so the move cannot collide, and dunewalker-bp held no row under those names. swampjaw-bp 14 to 12, dunewalker-bp 14 to 17, total 755 to 756
  • feat(origins): SwampJaw Jetpack is a new origin row at tier 76 pinned to CustomizationItemTemplate_CIT_Jetpack_BasicV2_04B_2, and the existing SwampJaw Vest row is pinned to CIT_Outerwear_Vanguard_04B_2. game_items.itemName is NOT renamed on either row: it is part of the ingestion identity triple and upstream still sends "SwampJaw Vest" for jetpack tokens, so a rename would make the next jetpack mint miss the triple and fork a third row. The readable name lives on the origin row
  • feat(origins): the apply script refuses a pin whose assetKey does not match exactly one game_items row, printing every match before exiting 1. game_items.assetKey is not unique across item names: 3,756 rows carry 3,594 distinct keys and 158 keys are shared by two or three names, so an unguarded pin could claim rows across unrelated items
  • docs(seed): prisma/seed.ts records that printedTier, isPremium and assetKey are deliberately not written by the reseed path, since those structures key on row id and a fresh database has no ids to key on. scripts/apply-origin-tiers.ts is the only source for the three columns
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.34.0

v0.33.19

September 7, 2026

  • fix(seed): prisma/seed.ts upserted BACKFILL_ITEMS and RELEASE_ITEMS on releaseSlug alone, so a fresh npm run db:seed wrote the 20 redemption-wrath-bp rows (the only tiered items living in RELEASE_ITEMS rather than TIERED_ITEMS) with tierIndex null. Both loops now read the array tierIndex column and write it in update and create, null included for untiered rows, so a reseed mirrors scripts/apply-origin-tiers.ts, which remains the prod source of truth and was never affected. Verified on a scratch database (CREATE DATABASE on the Neon project, prisma db push, seed, read back, dropped): redemption-wrath-bp 9 + 11 and mr-fuckles-bp 11 + 11 tiers identical to prod, 95 non-null tierIndex rows across six releases, zero case-duplicate names. Prod untouched: 755 rows before and after, no apply-script run
  • fix(seed): inline item Squall Honksplatter (unmapped) renamed to Squall HonkSplatter; the unique key is case-sensitive, so a reseed created two rows where prod has the single tiered one
  • test(seed): lib/sets/__tests__/seedTierIndex.test.ts reads prisma/seed.ts with the TypeScript AST and asserts every tiered-array loop writes tierIndex in both halves of the upsert, that the 95 tiered rows match the prod read-back, that no name sits in two arrays, and that inline names collide with tiered names only with exact casing
  • docs: docs/notes/discord-announcer.md states that #updates routing keys off the changelog entry consumer field, not the updates.ts title; docs/notes/database-maintenance.md records the apply script as the tierIndex source of truth, the seed mirror rule, and the scratch-database seed-check recipe
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.33.19

v0.33.18

September 7, 2026

  • feat(changelog): apiImpact: boolean is a required field on VersionEntry (this page) and its TechnicalEntry mirror in lib/announce/releaseSources.ts. True only when the release touches the documented public API (app/api/v1, the v1 DTO modules lib/server/apiV1Supply.ts and siblings, or the /developers route table); nothing else counts. Backfilled on all 163 entries in one mechanical pass: true for v0.29.0, v0.31.0 and v0.32.0, false everywhere else. The AST reader accepts only the true/false keyword and throws MissingApiImpactError naming the version when an entry lacks it; apiImpactGuard.test.ts type-checks a virtual entry through the compiler API (TS2741) and asserts the page interface declares the field non-optional
  • feat(announce): three-channel routing. routeRelease() sends a release to #updates when its changelog entry has consumer text, to #api-changelog when apiImpact is true (both may apply), and to the new private #build-log (DISCORD_WEBHOOK_BUILD_LOG, slot build-log, --only build-log) when neither. The #api-changelog embed is the API-relevant bullets only (/api/v1 paths, apiV1* modules, /developers, api-scoped labels; every bullet with a warning when none matches) with no consumer block; #build-log is "<version> - build log" with every bullet; the #updates embed is unchanged. Every run prints one routing line per channel and a skipped channel needs no webhook
  • feat(announce): sequential posting. Without --version, --force or --mark-announced, resolveReleaseCatalog() pairs every version in both sources oldest first and planSequence() walks, per channel, every version newer than the recorded slot: routed versions are posted, others advance the slot with a null messageRef, and the slot is written after each step so a failed post resumes there. A slot with no recorded version starts at the newest qualifying entry. --max N (default 5) caps posts per run and reports the remainder with exit 0. A channel with a recorded slot, no webhook and something to post exits 1 before posting; a channel with no slot and no webhook is skipped with a warning. compareVersions() moved to releaseSources.ts and announceX shares it
  • docs: docs/notes/discord-announcer.md (apiImpact, routing table, sequential runs, four-slot dedupe table), push-to-main-workflow.md step 4, .env.example
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.33.18

v0.33.17

September 7, 2026

  • fix(origins): Techno Gimp Mask deleted from mr-fuckles-bp through the apply script DELETE_ITEMS path and dropped from prisma/seed.ts. It resolves to game_items CIT_Facewear_FullFaceMask_03 (Uncommon), first minted 2025-04-26 with 23,874 mints to 18,695 wallets, a base-game item against a 2025-10-06 pass launch and a 1,851-mint ceiling for the pass. mr-fuckles-bp 23 to 22 items
  • fix(origins): Red Ant Shorts deleted from red-ant-bp (10 to 9), a relabelled Red Ant Pants variant sharing assetKey CIT_Zero7Assassin_Pants_Long_11A with 25 mints from 2025-09-12. Boomslang Second Amender, firework farts & freedom and Taste of Freedom moved from save-democracy to yankee-doodle through MOVE_ITEMS with ids preserved (4 to 1, 3 to 6). The first two carry 55A keys first minted 7 s and 12 s after the Yankee Doodle Damage set; Taste of Freedom is a PH14 music track with no release code first minted 33 min after it, moved on that same-day evidence like the other in-pass music tracks. save-democracy keeps Ichnya Covfefe and is not deleted
  • feat(seed): mr-fuckles-bp added to TIERED_RELEASES with its existing name, shortName and category (the description is the only new field) and tierIndex set on its 22 items as two independent 1-based sequences in on-chain claim order: Gimp track (71A) tiers 1..11, Mr Fuckles track (69A) tiers 1..11. Ballgag Beat (PH20) and Big Top Beat (PH21) carry no code and sit at tier 5 of their track by claim position. The ten mr-fuckles rows leave BACKFILL_ITEMS since the tiered list carries them; the join stays on lower(itemName), no schema change. Applied to prod with --execute over the unpooled URL: read-back 22 / 9 / 1 / 6 rows on the four releases, 11 + 11 tiers, no nulls, no duplicates within a track, computeSetCompletion setSize 22
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.33.17

v0.33.16

September 7, 2026

  • feat(seed): prisma/seed-origin-tiers.ts sets tierIndex on the 20 redemption-wrath-bp items as two independent 1-based sequences in first-buyer claim order: Confessor track (assetKey 98A_01) tiers 1..9, Dark Apostle track (98A_02) tiers 1..11. Source is the 2026-09-05 claim-sequence investigation (pure grind signature, no premium bundle); the pass is not viewable in-game so the order is inferred, which the release description now states. In-game item ids sit in trailing comments for provenance (Tainted Blessing and Dark Apostle Cassock share id 81); the join stays on lower(itemName), no schema change
  • seed.ts reads the same TIERED_ITEMS list, so the reseed path mirrors automatically; scripts/apply-origin-tiers.ts already diffs tierIndex and upserts the description, so it is unchanged. Applied to prod with --execute over the unpooled URL; read-back 9 + 11 tiers, no nulls, no duplicates within a track
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.33.16

v0.33.15

September 6, 2026

  • feat(pnl): components/pnl/PnlPanel.tsx derives isOwner via new lib/pnl/ownerState.ts isTrustedOwner(address, primaryWallet, profile.portfolioAddresses): owner when the viewed wallet is the session primary wallet OR the profile holds a PRIMARY/VERIFIED claim on it; SELF_REPORTED never confers owner rendering. Client mirror of the server rule in lib/pnl/ownership.ts sessionOwnsWallet (TRUSTED_STATUSES = VERIFIED, PRIMARY), which stays authoritative on POST compute. Salvaged from #189 (closed); the item-metadata half was superseded by 97231cc1
  • test(pnl): lib/pnl/__tests__/ownerState.test.ts covers primary match, VERIFIED and PRIMARY secondary claims, SELF_REPORTED rejection and no-session; PnlActivation.test.tsx gains the forbidden-state case (a wrongly rendered CTA surfaces the 403 copy and keeps the CTA)
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.33.15

v0.33.14

September 6, 2026

  • feat(pwa): public/apple-touch-icon.png (180x180), public/icon-192.png and public/icon-512.png, the GUNZscope mark centred on solid #0A0A0A with the mark inside the maskable safe zone. app/[locale]/layout.tsx metadata.icons.apple points at the 180x180 PNG instead of /gs-icon.svg; public/site.webmanifest replaces the sizes:any SVG entry with the 192 and 512 PNG entries, both purpose any maskable. /favicon.svg stays the primary icon link
  • fix(pwa): app/favicon.ico regenerated from the mark (16, 32 and 48 px frames, 32 bpp, 15,086 bytes). The previous file was a generic triangle-in-a-circle placeholder with a fourth 256 px PNG frame (25,931 bytes), so every browser tab that fell back to the .ico showed the wrong icon
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.33.14

v0.33.13

September 6, 2026

  • chore(changelog): v0.26.0 entry gains the three merges it never credited (#345 grant page accuracy pass, #346 mini-grant vision deck, #347 sitewide swallowed-space sweep); the v0.25.3 catch-up in #348 was closed unmerged, so the bullets land in the release they actually shipped with and no v0.25.3 entry is created. v0.26.0 updates entry gains one line for the #347 spacing fix
  • chore(grant): /team1-mini-grant UNIT_TESTS 1,813 -> 2,264 (the Tests total printed by npx vitest run) with its own UNIT_TESTS_AS_OF = Sep 6, 2026 on the tests card; DEV_STATS_AS_OF and MERGED_PRS stay at Aug 31. grantPageAccuracy.test.ts pins both new constants
  • fix(admin): app/api/admin/lookup/route.ts groups holdings with createItemKey (alias-resolved gameItemId), the key the enrichment engine has used since #394, so the NFT HOLDINGS table reports the same item count as the pre-enrich cursor (217 by name vs 226 by game item for one wallet). RPC-path and unlinked tokens fall back to the name key exactly as the engine does; portfolio totals are quantity-summed and unchanged
  • chore: package.json and both root slots of package-lock.json carry the APP_VERSION value without the v (was 0.8.18 since May); components/ui/__tests__/versionParity.test.ts fails the suite when they drift. Nothing in scripts, lib, app, CI or the indexer reads package.json version
  • fix(announce): a real npm run announce with DISCORD_WEBHOOK_UPDATES or DISCORD_WEBHOOK_API_CHANGELOG absent now throws MissingWebhookError naming the missing vars before any post or state read, and the CLI exits 1, instead of silently downgrading to a dry run. --dry-run and --mark-announced warn and continue; --only needs only its own channel. Four tests replace the old degrade-to-dry-run case; docs/notes/discord-announcer.md and push-to-main-workflow.md updated
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.33.13

v0.33.12

September 6, 2026

  • feat(price): app/api/price/history/route.ts reads gun_price_history first for finalized dates (strictly before yesterday, UTC; lib/pricing/finalizedDate.ts) and serves a coingecko or defillama daily row with the same shape and Cache-Control as today plus cached: true, with no CoinGecko call. On a CoinGecko success for a finalized date it upserts through the existing writeToServerCache helper (same source, confidence and estimated rules); a write failure is logged and never changes the response. Today, yesterday, non-gunz coins and the fallback chain are unchanged
  • lib/server/resolveHistoricalGunPriceServer.ts: toDateStr, tryServerCache and writeToServerCache are exported; no logic change
  • test: app/api/price/history/__tests__/route.serverCache.test.ts (cache hit never calls CoinGecko; miss calls once and writes once; yesterday and today never read or write; failure on a finalized date falls back exactly as before), lib/pricing/__tests__/finalizedDate.test.ts (UTC day boundaries); the fallback-chain suite mocks the new exports
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.33.12

v0.33.11

September 6, 2026

  • feat(schema): announce_state table (slot text primary key, version text, postedAt timestamp, messageRef text nullable), migration 20260906120000_announce_state generated offline with prisma migrate diff, hand-reviewed, and applied with migrate deploy on the unpooled URL
  • feat(announce): AnnounceStateStore is async and per-slot (read(): Promise<AnnounceState>, write(slot, version, messageRef?)). lib/announce/dbStateStore.ts upserts one row per slot with postedAt = now through a standalone PrismaClient + PrismaNeon on DATABASE_URL, no session-level SET, disconnected at the end of the run. scripts/announce-release.ts opens it at its single injection point; the .announce-state.json file store, its readState/writeState helpers and the .gitignore entry are gone. Every run prints the recorded state and the target version first
  • feat(announce): Discord webhooks are posted with wait=true; id and channel_id from the 2xx body are stored as messageRef <channel_id>/<id> and the CLI prints the message link (DISCORD_GUILD_ID from .env.local, otherwise the channel/id pair). X stores the first tweet id. A 2xx without ids still records the version with a null messageRef
  • chore(announce): scripts/import-announce-state.ts imports a checkout .announce-state.json once (dry run by default, --execute to write; postedAt now, messageRef null); run from the main checkout with updates and api at v0.33.9
  • chore(integrity): digest heading renamed from Pending announcements to Announcement state in indexer/src/integrity/alerting.ts; the loader and invariants are unchanged
  • test: lib/announce/__tests__/dbStateStore.test.ts (postedAt set on write, a per-slot write leaves the other slots untouched), message-ref cases in the announcer, X and webhook suites; docs/notes/discord-announcer.md Dedupe state section, push-to-main-workflow.md section 4 and supply-integrity-job.md updated
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.33.11

v0.33.10

September 6, 2026

  • fix(admin): app/api/admin-proxy/[...path]/route.ts exports maxDuration = 120. The catch-all declared no budget, so the vercel.json app/api/**/*.ts default of 30s applied to it while the four routes it fronts that declare 120 (admin/pre-enrich, admin/lookup, admin/lookup/snapshots, admin/pnl/compute) kept running; PRE-ENRICH on wallets past about 100 groups 504ed at the proxy (FUNCTION_INVOCATION_TIMEOUT) with the upstream call still in flight, and whale runs had to hit /api/admin/pre-enrich directly. 120 is the largest upstream value and sits under the Pro plan cap (300s default, 800s maximum with Fluid compute)
  • test: app/api/admin-proxy/__tests__/maxDuration.test.ts imports both route modules (pre-enrich with its db, chain and engine deps stubbed, the same set as its own route test) and asserts proxy maxDuration >= pre-enrich maxDuration, so the two budgets cannot drift again
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.33.10

v0.33.9

September 6, 2026

  • fix(stats): app/api/stats/site/route.ts reads indexerCursor.findUnique({ where: { id: 'gunzchain-nft' } }) instead of an unkeyed findFirst(). indexer_cursors has grown to 43 rows across pnl-compute, holdings-backfill, pre-enrich, email-delivery, supply-alerts, integrity and transfer cursors; the first row in heap order was pnl-compute:0xe42a… with lastBlock 3 (convergence passes, not a block), which the CDN cached for five minutes at a time. A missing live row now yields 0 rather than another row's counter
  • test: app/api/stats/site/__tests__/route.test.ts mocks a cursor table with a pnl-compute row (3) ahead of gunzchain-nft (23725367) and asserts blocksIndexed 23725367; a second case drops the live row and asserts 0. The mock has no findFirst, so a regression to an unkeyed read throws
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.33.9

v0.33.8

September 6, 2026

  • chore(deps): next ^16.2.11 -> ^16.3.4 (16.3.4) and @dynamic-labs/ethereum + @dynamic-labs/sdk-react-core ^4.91.6 -> ^4.98.0 (4.98.0; the 21 SDK‑locked @dynamic-labs/* transitives follow). Closes Dependabot #259: both sharp copies now 0.35.x (hoisted 0.35.0 via @dynamic-labs/iconic 4.98.0, next/sharp 0.35.4). 142 lockfile movers, all attributed to the next/sharp/Dynamic subtrees in the commit body: @img/sharp-* and @next/swc-* platform shards, Dynamic‑coupled scopes (@dynamic-labs-sdk/client 1.27.1, forward-mpc-client 2.0.1, message-transport 5.6.1), @metamask/connect-evm 2.1.0 via @dynamic-labs-connectors/metamask-evm (Dynamic 4.93.0 sets useMetamaskSdk true by default), @marsidev/react-turnstile 1.5.2 added (Turnstile captcha, Dynamic 4.92.x). posthog-js stays 1.427.2. No npm audit fix
  • fix(analytics): lib/utils/stripDynamicOauthParams.ts is now the posthog-js before_send hook. Strips dynamicOauthCode and dynamicOauthState from $current_url, $pathname, $referrer and every URL‑ish key in properties, properties.$set/$set_once and the top‑level $set/$set_once, on every event including $web_vitals. Pure: an event with nothing to strip is returned as the identical reference. DynamicProvider.onAuthSuccess already replaced the address bar via history.replaceState; the landing $pageview and $web_vitals fire before that handler, which is what showed the params in Live events. Vitest: lib/utils/__tests__/stripDynamicOauthParams.test.ts
  • chore(csp): Report‑Only Content‑Security‑Policy gains https://challenges.cloudflare.com in script-src and frame-src for the Turnstile widget the Dynamic SDK loads during social sign‑in. Additive, report‑only; nothing is enforced
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.33.8

v0.33.7

September 5, 2026

  • chore(deps): Dependabot PR B1 closes #194 (@opentelemetry/core < 2.8.0). posthog‑js floor ^1.347.0 to ^1.380.0, resolved 1.427.2; posthog‑js dropped its @opentelemetry/* family between 1.370 and 1.380, so that tree is gone. 24 lockfile movers, all attributed in the commit body: the dropped OTel family, posthog‑js's own subtree (@posthog/core, @posthog/types, @posthog/browser‑common, core‑js, web‑vitals 5 to 6, web‑vitals‑soft‑navs), preact 10.29.8 (shared with @coinbase/wallet‑sdk, in range), and the Sentry peer provision below
  • fix(deps): @opentelemetry/core and @opentelemetry/sdk‑trace‑base declared as root dependencies at ^2.11.0. @sentry/opentelemetry requires both as peers and the root never provided them; they were satisfied only by the copy posthog‑js hoisted, so Sentry had been running against the vulnerable core 2.2.0, and next.config.ts failed to load once posthog‑js stopped hoisting it. Every Sentry copy now dedupes onto the root 2.11.0. Rationale recorded in docs/notes/security‑known‑advisories.md so a cleanup does not drop them as unused
  • web‑vitals 5.1 to 6.2.1 reviewed: soft‑navigation support is the one ingest‑relevant change, and the PostHog project has web‑vitals autocapture on, so client‑side route changes may emit additional $web_vitals events; INP attribution payload shrinks by default; no metric definition changed. The SDK now posts events to /api/ingest/i/v0/e/ (project analytics endpoint), which the tunnel rewrite already covers
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.33.7

v0.33.6

September 5, 2026

  • chore(deps): Dependabot PR A closes 7 of 14 open alerts with no npm audit fix. Root override "mysql2": "^3.23.1" (resolved 3.24.3; prisma‑pinned, dev‑scoped MySQL connector this Postgres app never exercises) closes #314 and #318; in‑range refresh of @humanfs/node 0.16.8 (#317) and both fflate copies, 0.4.9 under posthog‑js (#319) and 0.8.3 under @autonomys/auto‑dag‑data (#320); onchain qs override ^6.15.2 to ^6.16.0 closes #315 and #316. All 15 lockfile movers are enumerated with parent attribution in the commit body; every non‑named mover is a dependency of a named package (mysql2 3.24.3 swapped sqlstring/seq‑queue/denque for sql‑escaper)
  • docs(security): security‑known‑advisories.md re‑baselined from the live Dependabot API (14 open on main, 7 expected after merge). Accepted gains stream‑json 1.9.1 (#321, jayson still declares ^1.9.1 at its latest, trigger: jayson declares stream‑json ^3) and adm‑zip 0.4.16 (#229, onchain non‑shipped, trigger: hardhat 3). Blocked upstream: deepmerge‑ts x2 (#312, #313, @prisma/config pins 7.1.5 through 7.10.0, trigger: prisma 8). Deferred to PR B with the verification each needs: posthog‑js ^1.380.0 for #194 (upstream dropped its OTel deps; six‑step /api/ingest checklist) and next ^16.3.4 plus @dynamic‑labs/* ^4.98.0 for #259 (#357 auth walkthrough). Stale 107‑alert surface and the closed esbuild indexer row removed
  • mysql2 3.16 to 3.24 reviewed from GitHub Releases and Changelog.md (34 releases, no BREAKING sections): mysql_clear_password disabled by default in 3.22.0 (the #314 fix), escaper moved from sqlstring to sql‑escaper, typeCast now called for NULL in the binary protocol (3.23.1), pool reset and per‑connection config changes; none touches code we use, no action taken
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.33.6

v0.33.5

September 5, 2026

  • fix(integrity): chain reconciliation subtracts in‑flight mints before the verdict. facts.ts loadInFlight reads latestTokenId from the chain pinned to the tip and to the persisted cursor block (mint Transfer logs, 2,000‑block span, injected ChainReads; the GI contract has no latest‑tokenId view) and loadSweepWindowUnlinked counts unlinked mints inside the trailing INTEGRITY_SWEEP_WINDOW_BLOCKS (90,000) under a blockNumber range. reconcileSupply prints "in‑flight: cursor lag N tokens (blocks A to B), sweep window M unlinked; adjusted mint drift X" and judges X (and the same subtraction on active drift); raw lines stay. Raw drift had breathed 58,283 to 58,977 across runs while the unlinked gauge held near 52,560
  • fix(integrity): tolerance is residue plus margin (62,600). The 47 unresolvable seed tokens are inside the 52,600 residue and were summed twice (62,647); INTEGRITY_UNRESOLVABLE_SEED is still read for provenance. docs/notes/supply-integrity-job.md gains the two‑term provenance table, the frozen‑residue evidence (50,518 of 52,377 unlinked rows in blocks 4M to 6M, burned before metadata; 272 in the trailing 90 days) and the in‑flight description
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.33.5

v0.33.4

September 5, 2026

  • fix(enrichment): the grouped engine groups by alias-resolved gameItemId. groupNFTsByMetadata takes an optional key function and createItemKey uses the game_items id that wallet_holdings already supplies, falling back to the name when absent; only the engine passes it, so client grouping is unchanged. On 0x2333 the cursor total moves from 1,486 name groups to 1,567 items, and two rarities of one item no longer share an enriched_items identity
  • fix(enrichment): one enriched_items row per held token. After a group resolves, and when a fresh primary is skipped, fanOutGroupMembers upserts every member of the group in one transaction: group-level fields (itemName, listings, enrichedAt) copied from the group, token-level acquisition resolved per token from the latest mint in supply_events or transfer in transfer_events addressed to the wallet (null when none), hasAcquisition left false so the per-token paths can still trace a cost, quantity 1 on member rows. A member row that already carries acquisition keeps every token-level field. selectEnrichmentWork keeps a group pending while any member lacks a row; recordFailedAttempt stamps every token of the group. planGroupRows is pure and tested
  • fix(admin): GET /api/admin/enrichment measures coverage against wallet_holdings. total, enriched, withPrice and stale count rows for currently held tokens, coverage is enriched over holdings, and the response gains holdings and missingRows (holdings minus total). Before this, 0x2333 read coverage 100 with 99 held tokens that had no row; a wallet with no holdings rows keeps the old rows-over-rows figure
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.33.4

v0.33.3

September 5, 2026

  • feat(partners): unlisted /partners page listing products built on GUNZscope data, starting with WalletZero. app/[locale]/partners/page.tsx is a static server component cloned from /credits (Navbar, Footer, narrow main, clip-path row cards) with the DB query dropped; partners live in a typed inline Partner[] (name, slug, tagline, url, xHandle, uses, liveSince) so adding one is a data-only change. Each row: logo slot, name linking out, tagline, "Uses:" and "Live since" mono lines, X handle link. The logo slot checks public/partners/<slug>.svg with fs.existsSync at render time and renders it with next/image at 48x48, otherwise a lime-bordered initial; no logo file is shipped or fetched
  • feat(partners): layout.tsx carries title, description, alternates.canonical /partners and robots noindex/nofollow, the team1-mini-grant unlisted pattern. No sitemap, footer, navbar or messages edits; the page is reachable by URL only until the phase 2 announcement flips it
  • test(partners): partnersPageAccuracy pins the WalletZero url and handle, the typed data array, the static-only shape (no client directive, no prisma, no force-dynamic), the existsSync logo rule, the noindex block and canonical, and that app/sitemap.ts and components/Footer.tsx do not mention /partners; jsxSpacing gains the partners developers-link junction
  • docs(indexer): supply-integrity-job.md scheduling section corrected: the job is a PM2 one-shot (gs-integrity, cron_restart 42 6 * * *, 06:42 UTC), not a 09:00 UTC crontab, alerting via DISCORD_WEBHOOK_INDEXER_ALERTS and INTEGRITY_ALERT_EMAIL
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.33.3

v0.33.2

September 5, 2026

  • chore(integrity): the integrity job reads its Discord webhook from DISCORD_WEBHOOK_INDEXER_ALERTS. The previous name pointed at a deleted webhook (HTTP 404). No fallback to the old name; when the variable is unset the job logs "discord skipped: DISCORD_WEBHOOK_INDEXER_ALERTS not set" on stderr, matching the email path, and the summary line reports the channel as skipped. .env.example and docs/notes/supply-integrity-job.md updated
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.33.2

v0.33.1

September 5, 2026

  • feat(seed): apply-origin-tiers.ts gains MOVE_ITEMS (row moves by lower(itemName), id preserved), DELETE_ITEMS (named rows deleted from one release) and RENAME_RELEASES (slug, name, shortName, category, seasonSlug; the ON UPDATE CASCADE foreign keys carry item and rule rows to the new slug, clearTierIndex nulls tierIndex). Steps 2b to 2d, each printing its plan in dry run and its result in execute
  • feat(seed): anti-cheat-bp: the five quoted Silence pieces move out of don-delulu (seeded there 2026-03-20, code 52A, first mint 2025-06-15), Silence Shirt and Wolf Tamer Skin for the Terikon are added, PermaBan moves out of unmapped. Three don-delulu rows that match no game_items row (Goldchain, Heads you're Liquidated, Rose Gold) are deleted; the real rows GoldChain #1, Heads, You're Liquidated. and Rose Gold #1 stay. Prankster T-Shirt joins prankster
  • feat(seed): redemption-wrath-bp (code 98A, seasonSlug redemption-wrath, date 2026-05-06), 20 items in first-claim order, no tierIndex because the Confessor and Dark Apostle tracks interleave in the claim sequence
  • feat(seed): vanguard-premium renamed to bulwark-pack (Bulwark Content Pack, content_pack, seasonSlug null, tierIndex cleared on its four rows). New content packs grid-runner-pack (03B, 2026-06-25), gunslinger-pack (65A/66A, 2026-05-12) and bride-pack (95A, 2026-03-23), five items each; Bridezilla #1 and Deadbloom #1 are Rare in game_items
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.33.1

v0.33.0

September 4, 2026

  • feat(schema): ItemOriginItem.tierIndex Int?, ItemOriginRelease.seasonSlug String?, ItemOriginRelease.cadenceDays Int?. Migration 20260905000000_item_origin_tiers is three ALTER TABLE ADD COLUMN statements, no index changes; indexer schema untouched (the item origin models are not in the shared set)
  • feat(origins): itemOriginService createRelease / updateRelease accept seasonSlug and cadenceDays, createItem / bulkCreateItems accept tierIndex, new updateItem(itemName, quality, { tierIndex }) matches case-insensitively. ItemOriginValidationError for tierIndex below 0 or non-integer and cadenceDays below 1; the admin route maps it to 400 and PATCH gains entity item
  • feat(seed): prisma/seed-origin-tiers.ts is the single list read by prisma/seed.ts and scripts/apply-origin-tiers.ts. Season 3 (seasonSlug season-03, date 2026-08-27): nullface-bp 16 items at claim slots 1 to 18, swampjaw-bp 14 at 19 to 35, dunewalker-bp 14 at 36 to 51, vanguard-premium 4 at tier 0. Slots come from the first premium buyer minting every reward two seconds apart in supply_events; slots 8, 10, 20, 31, 40 and 43 grant no new NFT
  • feat(seed): loyalty recategorised to category loyalty with cadenceDays 30; nine levels from Cyan Croc skin for the Vulture (1, 2025-11) to Cyber Cat Skin for the Kite (11, 2026-09), levels 6 and 7 empty. Each level recipient set nests inside the previous one. The dead Loyalty Reward row is removed; Snakes in the Sky leaves the unmapped bucket
  • feat(seed): ranked-s3 with the 15 distinct RANK_SN03 names (8 Season 03 avatars, whose 6 banner twins share the names, and 7 Kochi rank skins), covering 21 game_items rows
  • feat(seed): 31 score-3 backfill items across 12 battle passes, matched on the assetKey code shared with each release (for example _17A for ChemTech); Pain Thruster and Squall HonkSplatter move from unmapped to mr-fuckles-bp. Pending owner call, not added: Silence Shirt / Silence #1 / Silence #2 for anti-cheat-bp and Prankster T-Shirt
  • chore(scripts): apply-origin-tiers.ts, dry-run default, --execute to apply: deletes the dead row, clears the three unmapped rows, upserts releases and items with tierIndex / seasonSlug / cadenceDays, prints counts per release before and after. Case-insensitive match; an item already under another release is printed and skipped
  • feat(ui): loyalty added to OriginCategory, CATEGORY_LABELS, CATEGORY_COLORS and the admin release form; the set completion filter derives from CATEGORY_LABELS so it picks the category up
  • test: itemOriginTierIndex.test.ts covers tierIndex persist, 0, null, negative and fractional rejection, updateItem by case-insensitive name, and cadenceDays validation
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.33.0

v0.32.0

September 4, 2026

  • feat(indexer): alias-aware asset-key upsert (#376). Under GS_ITEM_KEY=asset, a key miss in item-identity.ts consults game_item_aliases.retiredAssetKey joined through the survivor for contract and name before creating a row, and lands the mint on the survivor with label fields only (no imageUrl refresh, so the asset-key drift sweep cannot re-key the survivor). findByIdentity gets the same fallback, so a burn of a token carrying a retired image reaches the survivor instead of being dropped as an unknown item.
  • feat(schema): retiredAssetKey (nullable) plus index on game_item_aliases, migration 20260904200000_game_item_alias_retired_asset_key, applied to prod 2026-09-04. The 597 rows from the relabel merge stay null; only curated cross-asset merges fill it.
  • feat(indexer): curated --pairs input for merge-fragmented-items.ts. planCuratedGroups builds groups from [{ survivorId, retireeIds }] (refusing unknown ids, cross-item pairs, self-pairs, unkeyed rows and reused ids), guard (e) is skipped for curated runs, guards (a) to (d) stay, and writeAlias persists each retiree assetKey.
  • ops: curated merge executed on prod 2026-09-04 19:00Z. Scout Cap PartItem1-4 (four zero-event residue rows), Eye Ache Skin for the Partisan AR06_DigitalCamo_05 and Digital Child Skin for the Type 227 SA02_DigitalCamo_16 retired into their survivors: 6 alias rows with retiredAssetKey, 52 mints re-pointed, counters equal events, zero duplicate identity groups.
  • feat(indexer): daily supply integrity job (#381), npm run integrity, src/integrity. Five invariants: chain reconciliation (latest tokenId and the contract totalSupply() against game_items counters, tolerance = 47 + 52,600 + 10,000) with no full-table count of supply_events; row-fork detector (RESIDUE, PER_TOKEN_VARIANCE, POSSIBLE_TRUE_SKU, ASSET_CHANGE, retired keys suppressed); windowed orphan-burn counter with a named-cursor baseline; asset-key drift sweep; tokenId continuity over a 3-day window. Every statement runs under SET LOCAL statement_timeout in its own transaction; runner.test.ts asserts the query shapes.
  • feat(indexer): integrity alerting. Email through Resend with subject SUPPLY INTEGRITY PASS/FAIL - <date>, Discord embed to DISCORD_WEBHOOK_HAKI (green on pass, red on fail, one field per invariant, skipped when unset), --no-email and --no-discord, exit 1 on any alarm. Cron at 09:00 UTC; docs/notes/supply-integrity-job.md.
  • fix(indexer): the health check display count is a trailing-24h count and never fatal, and the boot-time unlinked gauge no longer crash-loops the indexer on a cancelled count. Both were unfiltered count() calls on the ~52M-row table that hit a leaked 60s statement_timeout on 2026-09-04.
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.32.0

v0.31.2

September 4, 2026

  • perf(sets): loadReleaseItemMap resolves the item_origin_items to game_items join on lower(itemName) once per process (release metadata plus each release's sorted distinct game item ids and their union), holds it for 10 minutes and shares one in-flight load between concurrent callers. The join cost about 1.2 s per request on prod. clearReleaseItemMapCache drops the map and every cached wallet result and runs from createRelease, updateRelease, deleteRelease, createItem and deleteItem in itemOriginService, from applyMatchRules when it created rows (the cron path), and from invalidateItemOriginsCache, the invalidator every admin mutation already calls
  • perf(sets): the per-row supply_events mint fetch (5,755 rows, about 0.5 s for the 36k wallet) is replaced by one grouped query returning MIN(timestamp), MAX(timestamp) and COUNT(*) per gameItemId, toAddress still bound as both lowercase and checksummed, type mint, never lower() on the column (about 70 ms). Pace and ETA still run on first unlocks, so a second copy never moves the clock
  • test: two wallets within the TTL perform one join and one releases query, a call after clearReleaseItemMapCache performs a second, the clear also drops the cached wallet result, the mock rejects a non-aggregated mint lookup, and the 3-unlock fixture over aggregated rows matches the projector (paceDays 2, ETA four days after the last unlock)
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.31.2

v0.31.1

September 4, 2026

  • fix(sets): computeSetCompletion drops the getGroupedHoldings call. The route answers only from supply_events (which set items the wallet has ever minted) and returns itemIds per release; heldCount, ownershipPct and ownershipAvailable are removed. The wallet_holdings join that cost 15 to 20 s cold on the 36k-token wallet is gone; the service test asserts no query touches wallet_holdings
  • fix(portfolio): SetCompletionCard computes heldCount from usePortfolioNFTs, the grouped data the gallery renders from, so there is no second holdings fetch. holdingsToNfts carries game_items.id onto each NFT as an optional gameItemId (additive on the NFT type); on the RPC path, where NFTs have no id, held items are matched by name through the item-origins index. While the portfolio is initializing the held column is a skeleton and the unlocked line is populated
  • feat(sets): ownership is the headline for every category: bar and percent are heldCount / setSize, COMPLETE when heldCount >= setSize regardless of mintedCount, secondary line UNLOCKED X OF Y. ETA column: COMPLETE, projected date plus ~N days with an "at your unlock pace" tooltip while active, else INACTIVE. Rows sort by held share, then unlocked share, then name. Investigation (docs pending): every battle pass mint reaches the player wallet directly through a HEX decode (operator mintForUser or player-paid decode), 430,560 mints across 68,106 recipients with no distributor, so mintedCount means unlocked via HEX and not pass progress
  • refactor(sets): progressionPct renamed unlockedPct on the service, route and tests; i18n sets.progressOf renamed sets.heldOf, sets.held and sets.ownershipUnavailable removed, sets.etaTooltip reworded, in all eight locales. No user-facing set completion copy says progress
  • test: countHeld covers id intersection, the 7-of-7 buyer wallet with zero mints, and the name fallback; service suite 10, route suite 4
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.31.1

v0.31.0

September 4, 2026

  • feat(rankings): scarcity position over the default population. New pure helper lib/server/rankingsPosition.ts computes rank (ROW_NUMBER over activeMints ASC, id ASC) and rarerThanPct (100 * items with strictly more active copies / population, one decimal, ties share it) from id + activeMints rows. Population is the default rankings view (activeMints > 0, base limbs excluded) regardless of bracket, category, sort or pagination; a bracket=scarce page reports catalogue ranks, not 1..n. Rows outside the view (excludeZero=false, excludeBase=false) get null for both
  • feat(rankings): one population scan per request path, cached. loadRankingsPositions in lib/server/apiV1SupplyRoutes.ts selects id + activeMints once per 60s TTL and is shared by /api/supply/rankings and both v1 loaders, so no per-item queries and no extra scan per param tuple. The rankings page queries gain id ASC as a secondary orderBy so equal counts page stably; default sort fields unchanged. The internal route selects id alongside the projection but never emits it
  • feat(api-v1): /api/v1/supply/rankings and /api/v1/pro/supply/rankings rows carry rank and rarerThanPct (RankedFreeItemDto, RankedItemDto in lib/server/apiV1Supply.ts, RANKED_ITEM_KEYS); the envelope adds population beside total. /item, /batch, /drops, /events untouched. The internal premium stub adds population: 0 so its shape stays parallel
  • test(rankings): rankingsPosition unit cases (rank from 1, id tiebreak, shared tie percentage, max-supply 0.0, one-decimal rounding, the 196-of-3651 Pioneer Hoodie fixture at 94.6, empty population); loadRankingsPosition proves a bracket-filtered request keeps population at the full count with catalogue ranks, a single population scan, and the id tiebreak; free and pro envelope tests assert population and the exact row key sets; premiumGate stub shape updated
  • docs(developers): free and pro /rankings entries document rank, rarerThanPct and population with a Pioneer Hoodie sample row (rank 196, rarerThanPct 94.6) written so "Rarer than 94.6%" renders straight from the field; the namespace key-set paragraphs note the two added rankings keys. Bracket thresholds untouched
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.31.0

v0.30.0

September 4, 2026

  • feat(sets): lib/sets/computeSetCompletion.ts. Per-wallet completion of every item_origin_releases row. Set membership is item_origin_items joined to game_items on lower(itemName), deduped on game_items.id, zero-item releases skipped. Progression from one supply_events query (type mint, toAddress bound as both lowercase and checksummed, gameItemId = ANY(set ids)), grouped per release in memory on first unlocks so a second copy never moves the clock. Ownership from getGroupedHoldings, null when the wallet is not tracked and the call still succeeds. ETA = lastMintAt + (lastMintAt - firstMintAt) / (mintedCount - 1) * remaining, only with two or more unlocks and the last within 14 days; otherwise inactive; complete at mintedCount >= setSize. 60 s in-memory cache keyed on lowercase address
  • feat(api): GET /api/intelligence/sets?address=0x..[&category=]. Any authenticated session (Bearer or gs_session cookie), 401 otherwise, 400 on a malformed address, private no-store on every branch. Sorted by progressionPct desc then name; category is an equality filter on the release category
  • feat(portfolio): components/portfolio/SetCompletionCard.tsx mounted below the realized PnL panel in IntelligenceClient. Collapsible, category pills defaulting to battlepass (17 releases since #378), per row: release name, origin category badge, ProgressBar, X of Y and percent. battlepass and pro_pack lead with progression and show held X of Y as secondary text; every other category leads with ownership. ETA column renders COMPLETE, INACTIVE, or the projected date plus ~N days with a mint-pace tooltip. View-only sessions get the AuthGate blur and no request
  • i18n: sets namespace added to all eight locales (en, es, fr, tr, ar, bn, ja, zh)
  • test: lib/sets projector and service (10 tests), sets route auth, validation, sort and filter (4 tests)
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.30.0

v0.29.1

September 4, 2026

  • feat(announce): optional consumer field on VERSIONS entries. lib/announce/releaseSources.ts reads it from the changelog page through the TypeScript AST as a plain string literal; any other node type is ignored and the field stays undefined. Never rendered on /changelog
  • feat(announce): the #api-changelog embed opens with a bold "What this means for you" section followed by the consumer text and a blank line when the entry carries one; when absent the description is exactly the bullet list, no heading, no placeholder. npm run announce -- --dry-run warns on stderr when the resolved entry has no consumer field, warning only, never aborts
  • feat(announce): formatTechnicalItem in lib/announce/announceRelease.ts bolds the label before the first colon-space when it is a conventional commit label (fix(indexer), feat, ops, chore!) or a path-like label with no whitespace, a "/" or ".", and at most 40 characters. Bullets with no colon, a mid-prose first colon, or a longer path prefix pass through unchanged. The draft agent list in draftAnnouncements.ts stays plain (model input)
  • test(announce): parser round trip with and without consumer plus a non-literal consumer ignored; heading-first rendering, blank consumer treated as absent, six label-shape cases, and the consumer section surviving the 4096 truncation. lib/announce suite 55 to 64 tests
  • docs(announce): discord-announcer.md Sources gains Consumer field and Bold labels subsections; push-to-main-workflow.md asks every VERSIONS entry for one to three sentences of consumer text
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.29.1

v0.29.0

September 4, 2026

  • fix(indexer): chain backfill of two Phase A ingestion holes (#358 + operator runs). scripts/backfill-block-range.ts rescans ERC‑721 Transfer mint/burn topics per 2,000‑block chunk through the normalizer (createMany skipDuplicates), resolves unlinked mints via resolveAndClassify, recounts touched game_items from supply_events in a scoped UPDATE and rebuilds daily_item_mint_counts. Blocks 9,280,000‑9,645,692 (Oct 11‑16 2025): 477,414 mints added, 37 unresolvable; blocks 985,181‑1,068,209 (May 23 2025): 6,662 added, 10 unresolvable. The March 2026 Phase A run under pre‑#172 code had skipped those chunks and advanced the cursor
  • feat(indexer): tokenId continuity guard in scripts/health-check.ts (#358). src/utils/token-continuity.ts flags intra‑day span excess and inter‑day tokenId gaps against a 5% margin (--continuity-days, --hole-margin-pct); full history now reports zero holes
  • fix(indexer): assetKey derivation is side‑agnostic (#368). deriveAssetKey drops _LeftArm / _RightArm as a whole underscore‑delimited segment; 17 arm items carried both image variants per row and the live metadata refresh made the key oscillate. backfill-asset-key rewrote 70 rows, zero new collision groups
  • fix(indexer): fragmentation merge write order (#369). src/utils/merge-steps.ts re‑points events, writes aliases, rebuilds the survivor rollup, then deletes retirees BEFORE the survivor takes the latest label, so the merge runs under the legacy (contractAddress, itemName, rarity) unique. Stop guard reads the gunzchain‑nft cursor explicitly (liveCursorUpdatedAt); backfill-asset-key cursor is HEAD‑sha stamped, refuses stale resumes, --fresh, deleted on completion
  • ops: fragmentation merge executed on prod. 568 groups, 597 retirements, 1,692,321 mints and 2,596 burns re‑pointed, 231,886 rollup rows rebuilt, game_items 4,344 → 3,751 rows (3,478 names); every retired id resolves through game_item_aliases. Migration 20260903120000_game_item_asset_key_unique applied via migrate deploy (UNPOOLED): rarity unique dropped, assetKey NOT NULL + unique (contractAddress, itemName, assetKey). gs-indexer runs GS_ITEM_KEY=asset; health check exit 0 (0 holes, 0 duplicate variants)
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.29.0

v0.28.0

September 3, 2026

  • feat(indexer): wallet_holdings materialized from indexer Transfer logs (#365). Table (address, contract, tokenId, lastBlock) for every address in wallets or portfolio_addresses, maintained per Transfer log on the live transfer cursor with a lastBlock guard, backfilled by scripts/backfill-wallet-holdings.ts (holdings-backfill:0x… cursor rows). Migration 20260904020000_wallet_holdings applied by direct SQL plus migrate resolve; 20260903120000_game_item_asset_key_unique stays pending until the operator merge
  • feat(portfolio): /api/portfolio/holdings, DB-backed admin lookup and enrichment for registered wallets, pre-enrich offset fix, snapshot gated on pagination completion (#366). lib/server/walletHoldings.ts joins wallet_holdings to the mint row and game_items (alias fallback) in one query; lib/portfolio/holdingsToNfts.ts expands groups to raw NFTs (no mintNumber, per-token Serial Number stripped); useWalletDataFetcher tries the route first and falls back to RPC paging on 404. Unregistered wallets unchanged
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.28.0

v0.27.3

September 3, 2026

  • feat(admin): Tools tab gains an Admin Pages section with Wallet Lookup (/admin/lookup) and Share Tools (/admin/shares) as link cards; the card markup is shared with the Links tab via AdminLinkCard (Links entries unchanged)
  • feat(admin): /admin/lookup and /admin/shares carry a back link above the title. It targets /account: the admin panel is a local toggle on the account page, not a ?tab= value, so there is no deep link to open it directly
  • feat(admin): People expanded card renders the full 0x address (monospace, break-all) with click-to-copy and a transient copied state (CopyableAddress); the collapsed row keeps its truncated display and gains a copy glyph plus a click-to-copy tooltip. Gated by isWalletIdentifier() so email:/oauth: rows are unchanged
  • chore(csp): the two us.i.posthog.com entries (script-src, connect-src) are removed from the Report-Only CSP in next.config.ts; ingest has been tunneled through /api/ingest since v0.26.8 and three releases have shipped since
  • chore(admin): the three Authorization: Bearer getAuthToken() headers on the lookup page fetches are removed along with the import; /api/admin-proxy is cookie-only since v0.27.2
  • test: components/account/admin/__tests__/utils.test.ts (truncateAddress, isWalletIdentifier)
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.27.3

v0.27.2

September 3, 2026

  • fix(admin-proxy): /api/admin-proxy/[...path] is cookie-only. Its parallel Dynamic JWT parser (a second jose JWKS verify) is deleted: it had drifted from lib/auth/dynamicAuth - accepted MFA-pending tokens (scopes requiresAdditionalAuth), fell back to the Dynamic user id as the wallet, read the JWKS environment from a different env var - and the only page that ever attached a Bearer (/admin/lookup) also holds the cookie. Precedence otherwise unchanged: no valid cookie 401, non-admin 403, and isCrossSiteMutation now applies to every mutation. Upstream hop still injects ADMIN_SECRET only; the caller cookie and Authorization header are never forwarded. lib/auth/dynamicAuth is the single Dynamic token parser
  • fix(auth): verifyDynamicToken selects the JWKS signing key by the token header kid (jwt.decode complete). jwks-rsa getSigningKey() without a kid throws whenever the JWKS holds more than one key, which is the state during a Dynamic key rotation, so every Bearer-authenticated route would have failed until the old key was retired. A token with no kid keeps the old call. Audience and issuer checks remain a separate follow-up
  • test: app/api/admin-proxy/__tests__/route.test.ts (Bearer alone is 401, invalid cookie 401, non-admin 403, admin GET forwards with query and secret only, POST without Origin / foreign Origin 403, same-host Origin forwarded with body, missing secret 500, upstream throw 502); lib/auth/__tests__/dynamicAuth.kid.test.ts (two-key JWKS verifies by kid, unknown kid fails, wrong key for kid fails, no-kid single-key regression) with real RSA keys
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.27.2

v0.27.1

September 3, 2026

  • fix(portfolio): lib/server/redactCostBasis.ts is an allowlist (audit finding 7b). Every field of NFTPnLResult and of the PortfolioSnapshot model is classified in a Record<keyof T, 'public' | 'private'> (as const satisfies), so a field added to either type without a classification is a compile error; the redacted object is built from the public keys in source order, private keys are absent (not nulled), and unknown keys are dropped. The old denylist deleted 41 known names (many from shapes no route serves any more) and let anything new through
  • fix(portfolio): classification. NFTPnLResult public: tokenId, currentGunPrice, floorPriceGUN, estimatedValueGUN, estimatedValueUSD, valuationSource, valuationConfidence, comparableSalesCount, hasValuation. Private: acquisitionVenue/Date/TxHash, costBasisGUN/USD, gunPriceAtAcquisition, costBasisConfidence, currentValueUSD, unrealizedGain*, potentialGain*, hasCostBasis, and warnings - newly private, because their text narrates the acquisition (decode fee paid off-chain, no cost data found). Snapshot public: id, address, chain, timestamp, nftCount, gunBalance, nftValueGun, gunPriceUsd, totalMarketValueUsd, nftMarketValueUsd, gunValueUsd. Private: nftsWithPrice, totalGunSpent, totalCostBasisUsd, enrichmentPct. /api/portfolio/snapshots visitor output is byte-identical to before; /api/nft/pnl/[tokenId] visitors lose warnings
  • test: lib/server/__tests__/redactCostBasis.test.ts (exhaustiveness both ways, exact public subset in source order, private absent not null, unknown keys dropped, route-projection byte-identity) and app/api/nft/pnl/__tests__/redaction.test.ts (visitor exact set + gated, entitled owner unchanged)
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.27.1

v0.27.0

September 3, 2026

  • feat(indexer): canonical item identity. game_items is keyed on (contractAddress, itemName, assetKey), where assetKey is the metadata image asset basename without _hd.png (__unknown__ when there is no image) - invariant across the 2026-07-08 and 2026-08-27 rarity relabels that split 567 names into 1,163 rows. Rarity and functionalTier are refreshed attributes; a label change closes the current rarityHistory entry and appends the new one. Genuine tier SKUs (BUZZKILLA Common/Uncommon/Rare/Epic arms) keep distinct assets and distinct rows. Design and evidence: docs/notes/activemints-fragmentation.md
  • feat(indexer): GS_ITEM_KEY=asset feature flag (indexer/src/utils/item-key-mode.ts) selects the new key; unset keeps the legacy rarity key, so a checkout with this code is inert until the operator sets it. All six resolvers (live listener, resolve pass, unlinked sweep, re-resolve, early resolve, block-range backfill) and the supply counter path go through one upsertByIdentity()/findByIdentity() in indexer/src/aggregation/item-identity.ts. Legacy mode is find-then-create/update with a P2002 retry, so it no longer depends on the rarity unique that migration B drops
  • feat(schema): migration B (20260903120000_game_item_asset_key_unique) - DROP the (contractAddress, itemName, rarity) unique, assetKey SET NOT NULL, CREATE UNIQUE on (contractAddress, itemName, assetKey). Deliberately fails while duplicate groups exist: it is applied only after the operator merge (merge-fragmented-items.ts --execute). deriveAssetKey() gains the __unknown__ sentinel so coverage is 100%
  • feat(api): lib/supply/resolveItemId.ts - one shared resolver (live id -> itself, retired id -> survivor via game_item_aliases, unknown -> null) wired into /api/intelligence/item/[id] (adds canonicalId), /api/intelligence/compare, /api/lookup?id= (adds canonicalId), the v1 supply events feed and the supply-alerts cron. Retired ids return the survivor's data under the survivor's id
  • fix(indexer): npm run health reads the live cursor explicitly (indexer_cursors id gunzchain-nft) instead of findFirst(), which picked a "complete" pnl-compute row and failed the check for the wrong reason; the duplicate-variant guard is fatal by default (--fragmentation-advisory downgrades)
  • fix(prisma): prisma.config.ts loads .env.local before .env (dotenv path array), so migrate deploy targets DATABASE_URL_UNPOOLED without a manual export
  • test: item-identity under both flag values (where shapes, label refresh, rarityHistory append, P2002 race), resolveItemId (live, retired, unknown, batch), /api/lookup?id=<retired> returns the survivor, healthExitCode fatal/advisory paths, __unknown__ sentinel
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.27.0

v0.26.8

September 3, 2026

  • fix(analytics): PostHog api_host is now the same-origin tunnel /api/ingest - next.config.ts rewrites /api/ingest/static/:path* -> us-assets.i.posthog.com/static/:path* (static rule first) and /api/ingest/:path* -> us.i.posthog.com/:path*, so ad blockers that drop us.i.posthog.com (observed 2026-09-01: blank status, 0 B, retry_count climbing to 3) no longer drop events. Same /api/ rationale as the Sentry tunnelRoute: the proxy API branch passes these through (no Bearer -> next()), dotted asset paths never enter the middleware, and connect-src/script-src 'self' already cover them. withSentryConfig merges its tunnel rule ahead of user rewrites (confirmed in the installed tunnel.js), so the two coexist
  • fix(analytics): ui_host pinned to https://us.posthog.com so the toolbar and event links resolve while api_host is a proxy; skipTrailingSlashRedirect: true because posthog-js paths carry trailing slashes (/e/, /flags/, /s/) and a redirect on a POST would drop the body. NEXT_PUBLIC_POSTHOG_HOST stays as an override only (.env.example updated). CSP entries for us.i.posthog.com deliberately left for one release so stale bundles still report clean
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.26.8

v0.26.7

September 3, 2026

  • fix(auth): proxy.ts (Edge page gate) now calls verifySessionCookie() instead of inlining jwtVerify - the last of the audit-7a inline copies (softAuth, admin-proxy and the locale layout had already migrated). lib/auth/sessionCookie.ts is the single gs_session verify site; every page gate, cookie-authenticated API, the admin panel and the eager-auth decision run through it. Drift verdict before the change: identical, no divergence yet - but no copy pinned the algorithm
  • fix(auth): jwtVerify pinned to algorithms: [HS256] (the only alg createSessionCookie ever signs), so an HS384/HS512 token signed with the same shared secret is refused. Payload is now typed (SessionPayload { wallet, sv?, iat, exp }) and wallet is validated as a non-empty string; an additive minSessionVersion option (caller-supplied, so the layout stays a pure crypto check) is wired so a future revocation claim is a one-place change. No caller passes it yet. proxy.ts keeps its explicit missing-SESSION_SECRET console.error as the ops alarm
  • test: lib/auth/__tests__/sessionCookie.test.ts pins the verify contract (valid, expired, tampered, wrong secret, missing secret, malformed never throws, HS384-same-secret refused, no/empty wallet refused, minSessionVersion matrix). __tests__/proxy.pageGate.test.ts is the first middleware suite: redirect on missing/invalid/expired/HS384 cookie, pass-through on valid, banned-via-checkAccessEdge redirect, locale-aware redirects, and the /portfolio + /collection ?address= view-only fallthrough on every failure branch (banned still redirects, preserved). Both run under @vitest-environment node: jose rejects jsdom-realm Uint8Arrays
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.26.7

v0.26.6

September 2, 2026

  • fix(auth): new lib/auth/sessionVersion.ts - module-level counter with useSessionVersion() (useSyncExternalStore, SSR snapshot 0), bumped once per validated signed-out -> signed-in transition (inside the v0.26.5 one-shot guard in useAutoLogin, after gs_session is minted - deliberately NOT tied to SESSION_ESTABLISHED_EVENT, which fires on every rolling re-validation) and once per logout (DynamicProvider onLogout, after the cookie clear settles, fired regardless of POST outcome)
  • fix(intelligence): useIntelligenceFetch SWR key is now [url, sessionVersion] - every intelligence module (pulse, feed, radar, movers, activity, notable-drops, release, compare) refetches with the new cookie on sign-in AND sign-out instead of waiting out the 30-120s poll. No keepPreviousData: stale premium payloads must not linger after sign-out
  • fix(supply): useMintPosition and usePortfolioMintPositions module caches cleared on session change via one module-level subscription (the gated error entries had no TTL and blocked refetch after sign-in; real ranks outlived sign-out); mounted instances re-render and refetch via useSessionVersion
  • fix: raw gated fetchers gained the session dep - SupplyLookupClient item detail (prev-ref guard so selection changes do not double-fetch), ReleaseTimeline page-0 effect, and the portfolio sparkline effect in usePortfolioPerformance (getPortfolioSparkline is a plain function, so the dep lives at its calling hook)
  • test: sessionVersion unit contract (bump/subscribe/unsubscribe), useAutoLogin bump-on-transition-only assertions, useMintPosition cache-clear-on-bump coverage (both directions)
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.26.6

v0.26.5

September 2, 2026

  • fix(auth): gs_has_session eager-load flag lifetime aligned to gs_session (was 30d vs 24h) via new lib/auth/sessionConstants.ts SESSION_MAX_AGE_SECONDS - single source imported by both sessionCookie.ts (cookie maxAge AND JWT exp, now derived from the same constant) and DynamicProvider.tsx. sessionCookie.ts is server-only (jose + SESSION_SECRET), so the constant lives in a client-safe module rather than being imported from the client provider
  • fix(auth): useAutoLogin fires router.refresh() once after a witnessed signed-out -> signed-in transition (new sdkHasLoaded param; the flag is set only when the SDK has settled signed-out, consumed on fire), so server components re-render with the fresh gs_session cookie and gated pages un-gate in place. Rolling re-validation on a signed-in page load never sees a settled signed-out state and cannot trigger it; SDK-boot false does not count
  • test: useAutoLogin transition coverage (refresh once on transition, none on rolling re-validation, none across SDK boot); AuthProviders bootstrap assertions updated for the 4-arg call
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.26.5

v0.26.4

September 2, 2026

  • fix(auth): gs_session cookie maxAge and JWT exp shortened 7d -> 24h (lib/auth/sessionCookie.ts, values in lockstep). The token is stateless - no server-side revocation - so a sign-out whose best-effort clear is missed (offline, crash, wallet-extension disconnect) previously left a valid cookie for a week, admin wallets included. useAutoLogin re-validates on every authenticated visit and /api/access/validate re-mints, so active users are unaffected
  • fix(auth): Navbar handleDisconnect logout POST now sends keepalive: true + credentials: same-origin, matching the DynamicProvider onLogout form - the immediately-following router.push could cancel the request in flight and strand the cookie
  • chore(components): removed components/WalletButton.tsx - orphaned since the navbar decomposition, zero imports (its disconnect duties live in Navbar handleDisconnect via WalletDropdown/TerminalStatusBar/ProfileDropdown)
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.26.4

v0.26.3

September 2, 2026

  • chore(api): removed the orphaned /api/portfolio/[walletAddress]/pnl route - a full serial recompute (per-token RPC enumeration + per-token GunzScan transfer tracing + OpenSea valuation) that could not finish a 45-NFT wallet inside the 30s function limit and 504ed on credentialed hits. Zero production callers; /api/pnl/[address] (persisted realized ledger) and the client enrichment pipeline are the live data paths
  • chore(hooks): removed lib/hooks/usePortfolioPnL - the route’s only fetcher, itself unused by any component
  • test: premiumRedaction.test.ts trimmed to the snapshots route cases; the removed route’s six redaction cases went with it
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.26.3

v0.26.2

September 1, 2026

  • fix(portfolio): /api/portfolio/[walletAddress]/pnl now returns Cache-Control: private, no-store on the unredacted branch, matching the redacted stub (#351). premiumRedaction.test.ts asserts the header on the redacted, premium-owner, and admin branches, not just the body
  • fix(middleware): proxy.ts page-gate path normalization (#351). stripLocale matches locale prefixes case-insensitively (default locale included), lowercases the result and strips trailing slashes before PROTECTED_PAGES.has(); extractLocale matches case-insensitively and returns the canonical lowercase locale
  • fix(api): /api/enriched-items GET/PUT bind the address param to the authenticated session via sessionOwnsWallet - same PRIMARY/VERIFIED trust boundary as /api/pnl/compute; 403 on mismatch (#351). /api/enriched-items/public unchanged
  • fix(account): account page adminMode check uses the shared isAdminWallet() helper from lib/auth/dynamicAuth instead of a hardcoded single address (#351)
  • chore(middleware): /admin/lookup, /admin/shares, /strategy added to PROTECTED_PAGES as defense-in-depth - their data routes were already server-gated via /api/admin-proxy/* (#351)
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.26.2

v0.26.1

September 1, 2026

  • fix(monitoring): client Sentry events tagged environment:development in production. instrumentation-client.ts read process.env.VERCEL_ENV, which is server-only - the client bundle only inlines NEXT_PUBLIC_-prefixed vars, so the browser init always fell back to 'development' and prod trace sampling (the === 'production' check) never engaged. Both reads switched to NEXT_PUBLIC_VERCEL_ENV (Vercel system env var, auto-exposed). sentry.server.config.ts / sentry.edge.config.ts untouched - VERCEL_ENV is correct in those runtimes
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.26.1

v0.26.0

September 1, 2026

  • feat(monitoring): @sentry/nextjs 10.x integration for error monitoring with source map upload (#349). withSentryConfig wraps outermost in next.config.ts (over next-intl + bundle analyzer), imported from @sentry/nextjs/config, Turbopack-supported options only: org/project/silent, tunnelRoute, sourcemaps.deleteSourcemapsAfterUpload (maps upload at build time, then never publicly served)
  • feat(monitoring): client/server/edge instrumentation (#349). instrumentation.ts register() loads sentry.server.config.ts / sentry.edge.config.ts per NEXT_RUNTIME and exports onRequestError = Sentry.captureRequestError; instrumentation-client.ts is errors-only (integrations: [], no Replay in v1) and exports onRouterTransitionStart. Every init is DSN-guarded (enabled: !!NEXT_PUBLIC_SENTRY_DSN), so local dev, CI, and Playwright run fully inert - no events, no network. tracesSampleRate 0.1 in production, 0 elsewhere; sendDefaultPii: false
  • feat(monitoring): /api/monitoring tunnel route for client events (#349). Deliberately /api/-prefixed: the proxy API branch skips the next-intl locale rewrite, so the tunnel rewrite applies untouched and connect-src 'self' covers it - zero proxy or CSP changes. Dodges ad-blockers on direct Sentry ingest
  • feat(monitoring): app/global-error.tsx captures to Sentry via useEffect (#349). ErrorObservability’s PostHog react_error_boundary channel is untouched - two parallel channels in v1; Sentry adds the source-mapped stack. Full notes in docs/notes/sentry.md
  • fix(grant): /team1-mini-grant accuracy pass (#345). Items Indexed and the supply-events figure render from the /api/stats/site payload the page already fetches (itemsIndexed, eventsProcessed via formatMints); commits total, last-30-days and the months-active chip computed from the prebuild git-velocity data; the stale $0.0264 GUN fallback replaced with a loading skeleton; MERGED_PRS and UNIT_TESTS pinned with an as-of qualifier. Copy corrected: block range, distinct tokens, scarcity vocabulary (Scarce / Limited / Circulating / Abundant), #1 Discord contributor, Stage 4 Finalist phrasing; roadmap Phase 3 rescoped to indexer redundancy, CI hardening, usage analytics and monitoring; Public Supply API feature card; /avax-build-games date-stamped as a March 2026 snapshot. grantPageAccuracy.test.ts pins the retired strings
  • feat(grant): public/vision_mini_grant.html, the current-narrative successor to the March BUILD Games deck (#346). Every stat carries an as-of qualifier, the timeline runs past March, the moat slide carries the indexer stat row, GTM phases renamed Wave 1/2/3, slide 14 reframed; a <=768px media query makes slides scrollable with 48px touch nav and horizontal swipe. /team1-mini-grant links repointed to the new deck, vision_and_story.html date-stamped, the forbidden-framing test extended to the new deck
  • fix(content): sitewide swallowed-space sweep after inline JSX tags (#347). Turbopack production compilation drops the leading space of some text nodes that follow an inline element (client bundle only; SSR HTML kept the space), so "Verifying</strong> proves" shipped as "Verifyingproves" after hydration. Detected by scanning the compiled client chunks for children:"..."}),"<letter> junctions; 7 junctions in 3 files (account/page.tsx, brand/sections/ComponentLibrary.tsx, roadmap/page.tsx) get the {' '} idiom, six junctions in the frozen avax-build-games pages left untouched by policy. Source-idiom tripwire tests plus one render test
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.26.0

v0.25.2

August 31, 2026

  • fix(velocity): full-month date parsing + build-time throw guard (#342). lib/data/buildVelocity.ts toISO() did an unguarded MONTHS lookup with 3-letter keys only, while updates.ts carries 17 full-month date strings ("July 12, 2026" onward) - emitting "2026-undefined-31", which sorts last, making last.date an Invalid Date: DAYS_ACTIVE = NaN, DATE_RANGE = "JAN 19 - INVALID DATE NAN, NAN", broken scaleTime domain on every consumer page. MONTHS extended with full-name keys (3-letter keys untouched); toISO(), the single lookup site behind all three parseUpdateDate branches, now throws naming the offending input - the module evaluates at build time, so a future unknown month fails the build instead of corrupting the timeline. 10 new tests pin full-month/3-letter/range parsing, the throw, and NaN-free real-data constants
  • fix(velocity): shallow-clone-proof commit generator (#342). scripts/generate-git-velocity.mjs detects shallowness (git rev-parse --is-shallow-repository) and unshallows before reading history (git fetch --unshallow --filter=blob:none, plain --unshallow as second try, every failure path falling back to the committed-data guard). Vercel clones ~33 commits, which always tripped the total < existingTotal guard and pinned production to the tracked gitCommits.json frozen at total 1130 / 2026-05-14; the series now regenerates complete on every deploy
  • fix(admin): computePeopleStats sum-to-total invariants (#343). statusCounts gains the expired slot, so its keys cover the whole PersonStatus union and the redundant in-guard that silently dropped expired persons (prod: 36 counted vs 40 total) is gone; authMix gains neverSignedIn, incremented in the else branch of the discord > walletOnly > email precedence chain - the fall-through shape is a whitelist-seeded row with no UserProfile (empty socials/wallets, null email, raw 0x address key; 12 in prod). Both blocks sum to totalUsers by construction, pinned by invariant tests plus fixtures for the two new buckets
  • fix(admin): non-wrapping composite metric tiles (#343). PeopleAdmin STATUS shows five slots (reg / wl / trial / ban / exp) and reconciles with USERS; AUTH MIX shows four (14W/2D/12E/12N with a letter-key caption). Both render through CompactStatValue, a whitespace-nowrap span with a responsive size ladder (14/9/12/16/20px) carried in the StatColumns ReactNode value slot (the 5-column panel grid and ReactNode value extension landed with the metrics row in #333) - sized to the real cell geometry, where the grid jumps from 2-up to 5-up at sm with ~77px of content per cell. Verified one-line at 384/640/768/1024/1440px
  • chore(admin): LINKS tab sectioned into ADMIN_LINK_SECTIONS buckets - Admin, Grants, Docs & Decks, Public, Extras - replacing the flat ADMIN_LINKS array (#340)
  • fix(content): swallowed spaces after inline tags on the grant pages (#341, pages from #334/#340). JSX collapses whitespace-only lines between an inline element and trailing text; explicit {' '} makes the spacing trim-proof
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.25.2

v0.25.1

August 31, 2026

  • feat(account): always-visible API tab with quiet empty state (#339). buildAccountTabs no longer gates the API tab on apiRequestStatus being truthy - the tab is always present so developer discovery does not depend on already having an ApiAccessRequest row (previously the account page was a dead end: no request meant no tab, even though /account?tab=api rendered the request form fine via the activeTab body switch, which never read the tab list). apiRequestStatus now drives styling only: a new optional quiet field on AccountTab is true when no request exists, and the tab bar renders a quiet inactive tab in --gs-gray-2 (the --gs-text-ghost level) instead of gray-3, no badge/dot/count, hover unchanged; with a request the tab renders exactly as before (Pending / Rejected / Suspended badge, none for approved). Verified non-dependencies: buildAccountTabs has one consumer (account/page.tsx tab strip), the tab-defaulting effect returns early on any tab param, showFocusedOnboarding / deep links / ApiAccessSection / /developers untouched. Tests pin the tab present-and-quiet with no request, present-and-not-quiet across all four statuses, the six-tab count for a standard user, and API as the only tab that can be quiet
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.25.1

v0.25.0

August 31, 2026

  • feat(developers): partner attribution kit (#330). Two standalone mark SVGs under public/brand/, geometry byte-for-byte from the canonical icon path in components/Logo.tsx (viewBox 0 0 296 323, no fixed dimensions, no external refs): gunzscope-mark.svg (fill #90F700) and gunzscope-mark-mono.svg (fill currentColor). New ATTRIBUTION_KIT constant and attribution-kit Card on /developers directly after Terms Of Use: the approved credit string "Data by GUNZscope" in CodeBlock style linking to gunzscope.xyz, download links for both SVGs, four placement rules, a Do / Dont example pair, and a live rendered credit preview (mono mark at 16px as a CSS mask taking the link color). Section id registered in SECTIONS
  • feat(developers): API access panel embedded on /developers (#332). New components/developers/DeveloperAccessPanel.tsx gates on useUserProfile() profile rather than user, so the getAuthToken() mirror is guaranteed populated before ApiAccessSection’s one-shot mount fetch (a user-gated mount can fetch without a Bearer, 401, and park on the permanent retry state). Signed-in: ApiAccessSection via next/dynamic ssr:false; signed-out: DeveloperCta, and the section’s chunks are never downloaded. Loading fallback replicates the section’s own card shell so chunk download and the internal fetch read as one continuous loading state. Mounted after Getting Started; api-access added to SECTIONS; /account?tab=api remains the secondary route
  • feat(developers): sticky Jump To nav with scrollspy (#337). JumpTo becomes JumpToNav: sticky bar with scrollspy active-section highlighting; from sm up the list wraps so every SECTIONS entry is visible (the SECTION_LIST truncation caught in the integration rehearsal, fixed at source), with geometry derived from the measured bar height. Tests pin scrollspy active-section resolution and JumpToNav geometry / render completeness
  • feat(api): attribution object on all v1 supply response envelopes (#338). Every v1 supply success envelope now carries a machine-readable attribution object, pinned by tests alongside the success envelope shapes; the /developers v1 response examples mirror the object. PR A of the partner attribution work (#330 was PR B)
  • fix(intelligence): radar ranks imminent movers over parked items (#331). isActivelyMinting joins the candidate select; etaDays now also requires isActivelyMinting === true, so a stale 7d tail on an item that stopped minting produces no countdown. New per-item status: trending (has an ETA), parked (distance 0, not actively minting), stable; ranking is by imminence (trending by etaDays ascending, then stable by distance ascending, parked last), top 12 taken after ranking. BracketRadar renders "parked at the edge" for parked rows. Route tests cover the parked edition with a stale positive tail, trending sort, and the live-looking tail on a non-minting item resolving to stable
  • feat(admin): server-side people stats + at-a-glance metrics row (#333). Pure computePeopleStats() in lib/services/peopleService.ts aggregates the complete population pre-filter, pre-pagination: status counts, total, new-this-week (inclusive 7-day boundary), active pro, latest payment-grant timestamp, auth mix (discord > walletOnly > email precedence), 14-bucket UTC daily signup series; listPeople returns { people, total, stats }. PeopleAdmin replaces its hand-rolled client-side counters (which undercounted beyond the pagination slice) with a StatColumns panel row: USERS, SIGNUPS 14D (inline polyline sparkline), PRO, AUTH MIX, STATUS. Aggregation unit-tested
  • fix(account): explicit ?tab= deep links win over the onboarding override for embedded-only (walletless) users (#336); test pins onboarding override vs explicit tab deep links
  • content(serials): field note addendum — boxes mint on open; colophon revision and fixed label column (#335)
  • test(server): entitlementPlumbing suite pins Date to FAKE_NOW (1e21911). The two softAuthenticateWithEntitlement tests read the real clock against a fixture whose premium expiry is FAKE_NOW + 10 days (2026-08-31T12:00Z); the suite went red on main and every open PR at that instant. Date (and only Date — timers stay real) is now frozen at FAKE_NOW
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.25.0

v0.24.0

August 29, 2026

  • feat(intelligence): bracket migration radar, premium (#327). New GET /api/intelligence/radar: one scan over game_items for activeMints within 20 of each supply threshold (10 / 50 / 200, three disjoint windows on the activeMints index), activeMints > 0 and the BASE_LIMB_NAMES honesty filter as /api/supply/rankings applies it; ranked by distanceToBracketEdge (lib/supply/bracket.ts, its first production call site), top 12; one grouped 7d mint/burn query over daily_item_mint_counts; netPerDay = (mints7d - burns7d) / 7; etaDays = distance / |netPerDay| ONLY when the sign of the 7d net matches the direction of the nearest edge (net minting toward a ceiling, net burning toward a floor), otherwise null. Distance is to the raw threshold, not threshold plus the indexer announcement hysteresis. Non-premium callers get a 200 stub { items: [], gated: premium }; 60s in-memory cache; private, no-store on every branch. New BracketRadar module renders in the premium branch where Rarest on Chain sat: per row item name, rarity badge, current bracket, active supply, 7d net per day, distance figure (lime toward scarcer, purple toward abundant) as "N from <adjacent bracket>", and either "~N days at current pace" or the exact copy "not trending toward the edge"; frame captions separate the two clocks (distance live to the last indexed block; trend as of the last rollup). RareMoving component and /api/intelligence/rare-moving are removed (same scarcity story, one module too many, and the swap keeps the premium poll budget flat). Tests: radarRoute.test.ts (gated stub, raw-distance ranking, base-limb filter, the ETA honesty rule in both directions plus the flat case), bracketRadar.test.tsx, gate test labels and leak strings
  • feat(intelligence): live supply event log, premium (#328). New GET /api/intelligence/feed (optional ?since=<id>): bracket transitions and drop detections from supply_feed_events, newest first, each event carrying the raw payload plus a summary (describeTransition) and direction (transitionDirection) from lib/supply/feedAlerts. The ?since= mode reuses parseEvents / loadEvents from lib/server/apiV1SupplyRoutes verbatim (forward read, presented newest first); the default tail is an additive loadLatestEvents(limit) in the same module, and the item-name batch join both readers need is extracted into one private toEventDtos mapper, so the app feed and the pro /api/v1/pro/supply/events API cannot drift. The pro route file is untouched and its tests pass unmodified. No server cache (tail is cheap by primary key, a cursor differs per caller); 200 stub { events: [], gated: premium } for non-premium; private, no-store on every branch. New LiveFeed module mounts directly under HeroPulse: transition rows read "<from> -> <to>" in the bracket vocabulary with direction colour and active supply at the crossing, drop rows read "New release detected" with the item count and up to three names then "+N more", a divider opens each new UTC day, up to 30 rows, no motion. The feed is low-frequency by construction (transitions are hysteresis-damped, a growing drop gets one row at first detection), so the empty state says so: "No supply events yet today - transitions are announced only when an item decisively crosses a bracket edge". Tests: feedRoute.test.ts (gated stub, newest-first tail with resolved names, ?since forward paging, 400 on a bad cursor), liveFeed.test.tsx, gate test anchors and leak strings
  • Zero schema changes across both modules: every query runs on existing indexes (game_items activeMints, daily_item_mint_counts [gameItemId, date], supply_feed_events id)
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.24.0

v0.23.2

August 28, 2026

  • security(api): premium-gated supply routes (/api/supply/drops, /rankings, /mint-position) no longer emit shared-cache headers on entitled responses; every branch of the three is now private, no-store, the convention every /api/intelligence/* route already followed (#321). /api/supply/item and /api/supply/batch return an explicit free-field Prisma projection (itemName, rarity, category, functionalTier, supplyBracket, totalMints, totalBurns, activeMints, isNew, isActivelyMinting), typed from the select instead of the raw unselected GameItem row, so time-dimension and indexer-internal columns never leave the server on a public URL (#322). The projected payload is identical for every caller, which is what keeps those two routes legitimately CDN-cacheable. Tests pin both halves: the entitled-branch headers on the gated routes, and the exact key set on item/batch across fresh and cache-hit branches. Follow-on in the same PR: SupplyCard read the two mint-date fields for its First/Last On-Chain row, which is now a single PREMIUM / MINT-DATE HISTORY pointer rendered for every tier (the public route carries no dates for anyone; surfacing them to pass holders needs a gated batch path); components/nft-gallery/SupplyBadge.tsx had zero mount sites and is removed
  • fix(intelligence): the page no longer claims what it cannot do (#323). The Your Collection card in ExploreDeeper (both variants, the hardcoded 941 releases literal, and the intelligenceUrl(address) self-link), the ?address= searchParams read in IntelligencePage, and the disabled you-own FilterChip with its empty handler in ReleaseTimeline are removed; ExploreDeeper is the Compare card alone. HeroPulse drops the Live pill and /api/intelligence/pulse drops lastBlockBehind, which was a constant 0 behind a TODO, along with the indexerCursor query that fed it. The OpenSea-trait scarcity path (app/api/scarcity, lib/hooks/useScarcity, and the ScarcityTraitStats / MarketplaceListing / ScarcityPageData types) had no page consumer and is deleted; getRelativeTime, its one used export, moves to lib/utils/timeAgo.ts. lib/server/intelligenceAccess.ts docstring now states what every route enforces: the lifetime curve and everything else with a time axis is premium, not part of the free supply snapshot
  • fix(intelligence): failures are visible (#324). IntelligencePage reads error and refresh from the shared pulse fetch and renders a page-level Pulse unavailable banner with a Retry (SWR revalidate) instead of silently collapsing to the free layout; the layout branch keeps its shape. StatStrip, PageHeader, IntelligenceFooter, ScarcityComposition and NotableDrops destructure error and render an inline error state (the HeroPulseError / RareMovingError convention) instead of a permanent skeleton or placeholder, gated on error && !data so stale data on a later refresh failure keeps rendering. TopMovers renders No movers in the last N days inside its frame instead of returning null. ItemSearch throws on non-ok responses (a 429/500 body was previously parsed as a result), keeps a separate fetchError, and renders search unavailable versus no items match; a settledQuery guard keeps the empty line off stale queries. Debounce and rate limiting untouched. New tests: moduleErrorStates.test.tsx, components/intelligence/__tests__/ItemSearch.test.tsx, plus cases in the two gate tests
  • fix(intelligence): gated surfaces upsell instead of erroring (#325). The compare and release routes have no meaningful stub and 401 with the same additive gated: premium marker the stubbing routes put on their payload; useIntelligenceFetch now carries it through as a PremiumGatedError and exposes gated: boolean, so ComparePage and ReleaseDetailPage render PremiumUpsell (the ItemDetailPage pattern) for an unentitled viewer and keep ErrorState for genuine failures. API routes unchanged. SupplyCard treats the metadata sentinel None as an absent category and an empty tier as absent, rendering only real values and no line when both are missing; Classified is a real functional tier and still renders
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.23.2

v0.23.1

August 28, 2026

  • Hardened the GUN price endpoint: on an upstream rate-limit or error it now serves the last known good price (marked stale) or falls back to the unkeyed source, instead of returning a server error. app/api/price/gun/route.ts gains a module-level lastGood snapshot per branch (the opensea rarity-floors pattern) and a keyed -> unkeyed -> stale -> 503 + Retry-After ladder for both the markets and the daily-chart branch, so a CoinGecko 429 with no fetch-cache entry can no longer surface as 500/502 (#319). Success-path response shapes unchanged; stale: true appears only on the fallback
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.23.1

v0.23.0

August 28, 2026

  • Walletless OAuth sign-ins (Discord and other social logins with no embedded wallet) now establish a session on any route and are routed to account settings after login. The session bootstrap (useAutoLogin) moved from Navbar, which is not rendered on /, into AuthProviders as a null-rendering SessionBootstrap so it runs on every route including the /?dynamicOauthCode= landing (#317); a scoped post-OAuth redirect then routes the landing to /account?tab=identity once the bootstrap announces the session, gated on a mount-time snapshot of the callback params and a fire-once latch so it is never a global redirect (#318). Internal auth-bootstrap hardening: single owner of the bootstrap, no rendering gate, no redirect at the AuthProviders level
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.23.0

v0.22.3

August 27, 2026

  • fix(auth): the walletless-OAuth session minting shipped in v0.22.0 (#313) could not fire in production. extractIdentifier in app/api/access/validate/route.ts and proxy.ts read the oauth credential as c.oauthProvider / c.oauthAccountId, which is the spelling of the SDK’s client-side user object; the JWKS-verified JWT that both functions actually receive spells verified_credentials snake_case (oauth_provider, oauth_account_id, alongside wallet_name and public_identifier), so the branch returned null and a Discord-first user with no wallet and no email credential got 401 and never received gs_session. Both functions now read either spelling through a shared oauthIdentity() helper, the same hedge lib/auth/dynamicAuth.ts:202 has carried since the social-connections work; the two copies stay identical. validate.openAccess.test.ts gains a snake_case oauth fixture (mints oauth:discord:<id>) and a snake_case wallet-over-oauth precedence case. v0.22.0 was merged without the live Discord-first check its PR body asked for; this release does not claim it works either - the merge gate for this change is that check
  • fix(home): the mount-time wallet validate in app/[locale]/HomeClient.tsx fired twice per page load (paired GRANTED entries 5-40 ms apart in production logs for the same wallet) because its dependency list re-ran the effect on the ref/router identity churn of the first render. A mountValidatedRef latch now lets one mount produce exactly one POST /api/access/validate; the success/banned handling and the /portfolio redirect are unchanged
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.22.3

v0.22.2

August 27, 2026

  • fix(mint-check): the ?q= deep-link setState-in-effect error (react-hooks/set-state-in-effect at MintCheckClient.tsx:356 - the one lint error on main since #311) was fixed by PR #315 (f28b931, merged e202b5c) while this PR was open, and #315 shipped without a version bump or release entry, so this is its release note. Prefill, parse error and loading states are lazy initial state derived from the query param; an async performCheck core is shared by the button and the deep link, with every state update inside a promise reaction; the effect only starts the fetches once per mount behind the autoRan ref. Contract pinned by app/[locale]/mint-check/__tests__/MintCheckClient.deeplink.test.tsx (StrictMode once-per-mount, prefill before any effect, parse error with no fetch, nothing without ?q=, plus the manual-submit preamble). This PR originally carried an equivalent fix and test; both were dropped in favour of #315 on rebase
  • chore(lint): the first trustworthy lint run of the arc surfaced three dead identifiers, all removed - the unused computeBracket import in indexer/src/ingestion/listener.ts, the unused COLLECTIBLE_MAX_MINTS import in lib/supply/__tests__/mintCheck.test.ts (left over from #311), and the unused LISTING_FIELDS const in scripts/dedupe-enriched-items-casing.mjs. tmp/** (gitignored scratch harnesses) added to eslint.config.mjs globalIgnores so a local npm run lint reports tracked code only. eslint now exits 0 with zero errors and zero warnings
  • ci: .github/workflows/vitest.yml gains a Lint step (npm run lint) after Generate Prisma client (app) and before the unit tests. No workflow ran eslint before this, which is how the #311 error shipped; a lint error now blocks merge. Note the lint script itself was never the problem (it is plain eslint, exit 1 on error) - the earlier release gates masked its exit code behind a tail pipe
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.22.2

v0.22.1

August 27, 2026

  • design(serials): editorial pass on public/serials-vs-mints.html (standalone static article, inline CSS, no globals.css). h1 clamp(40px, 9.2vw, 96px) = 2.5x the section head (clamp 26-38px); body stays at --measure 66ch while exhibit panels, tables and the proof card break to the 1120px wrap. Hairline rules between top-level sections; footer colophon split into .colophon key-value rows in the exhibit .kv style. Every pre-existing word of copy verbatim, checked by diffing extracted text at each commit
  • Stamped asides in a shared .stamp grammar (mono eyebrow, 1px lime left rule, recessed surface): FOR THE RECORD (token 549 facts as inline mono chips), RUN THIS CHECK YOURSELF (links /mint-check?q=38684952), the Sack the Ripper "Read that last row again" warning as .stamp.warn (red rule), and the full-width COULD THIS BE CONSOLE OR TESTNET ITEMS? note after the Tacoma chain-order table. The margin-column .note register built mid-arc was removed once nothing used it
  • Thesis sentence restyled in place as a centered two-tone pull quote (serial clause --gs-loss, mint clause --gs-lime). Checklist 01-04 get aria-hidden ghost numerals (Chakra Petch clamp 96-144px, 18%, one digit per row cropped by the card edge with a mask fade, hidden under 700px); the 01-04 row marks dim to --gs-gray-3 beside them via .check:not(.avoid) .mark so the red X keeps its colour. Proof card is one unified list again, hugging the verification code block width with the same 2px --lime-deep left accent
  • Article navigation: sticky CSS-only masthead (GUNZ<span purple>scope</span> wordmark, Field Notes / Mint Check / Open the app, short labels under 480px), footer nav line with the site socials, and the eyebrow kicker linking to /field-notes. Two authorized copy additions: the Mint Check cross-links and the console/testnet margin note (em-dash convention). No JS; A/B comparison scaffolding used during review was removed before merge
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.22.1

v0.22.0

August 27, 2026

  • Walletless OAuth sign-ins (Discord-first, no embedded wallet) now establish a server session and can reach account and developer/API settings. Session validation accepts an oauth credential derived from the verified Dynamic identity; email and wallet sign-in behavior is unchanged
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.22.0

v0.21.4

August 26, 2026

  • feat(mint-check): ranked ceiling raised from 500 to 2,000 lifetime mints for the public Mint Check page only. lib/supply/collectibleLine.ts adds MINT_CHECK_MAX_RANKED_MINTS = 2000 (the measured knee of the rank-count cost curve: 354 ms under 2,000, 4.3 s at 2,500-5,000; covers 1,438 of 4,216 items). getMintPosition gains an optional third parameter maxRankedMints defaulting to COLLECTIBLE_MAX_MINTS, so the premium mint-position route, the portfolio batch SQL and useCommodityItem are unchanged; only getMintCheck passes the new ceiling
  • Two ranked tiers on the page, split at the collectible line by rankedTier() in lib/supply/mintCheckCopy.ts. At or under 500: unchanged (first-mint, straggler and tail verdicts, CLASS Collectible). 501-2,000: exact rank with the verdict "Mint #X of N - at this supply, mint order carries only a limited premium." plus the serial clause, CLASS High supply; first-mint and straggler framing is deliberately not offered above the line. Over 2,000: commodity, as before. Verdict logic moved out of the client into mintCheckVerdict() so both tiers are unit-tested
  • fix(mint-check): commodity verdict rendered "1,367minted" in production; the separator is now an explicit {' '}
  • feat(mint-check): ranked results (both tiers) show one muted line under the chain panel when position > activeMints: "Mint positions count every mint ever made - burns don’t renumber. #{position} of {total} means {total} were minted, even though only {activeMints} survive." Nothing renders otherwise. burnsRenumberNote() in mintCheckCopy.ts, unit-tested
  • Tests: fixture pair 1,367 ranks / 2,001 commoditizes in mintCheck.test.ts; getMintPosition default still commoditizes at 501 while maxRankedMints=2000 ranks the same item; verdict tiers and burns note in mintCheckCopy.test.ts. Supersedes PRs #309 and #310
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.21.4

v0.21.3

August 26, 2026

  • fix(nav): the Intelligence menu items were defined twice - a hardcoded ITEMS const in components/navbar/IntelligenceDropdown.tsx (desktop) and an inline array in the Navbar.tsx mobile panel. 6b72f10 (Mint Check) and 8f1ddf7 (Market) edited only the mobile copy, so desktop rendered three entries (Overview, Supply Lookup, Onchain Explorer) from v0.21.0 through v0.21.2. Both surfaces now render lib/nav/intelligenceItems.ts INTELLIGENCE_ITEMS (href + nav labelKey; consumers call t()). Order is the mobile order: Overview, Supply Lookup, Mint Check, Market, Onchain Explorer
  • IntelligenceDropdown isActive now derives from the same list via isIntelligencePath() instead of a hardcoded three-path expression, so the menu label lights up on /mint-check and /market too. Mobile output unchanged: same order, same labels, per-item active via isIntelligenceItemActive(). ExperimentsDropdown untouched. Unit test on the list order, keys and active matching
  • Market ?item= verified by code, no change: lib/hooks/useMarket.ts seeds selectedItemName from searchParams.get('item'), selectedItem is an exact itemName match against the loaded listings, and market/page.tsx renders ListingDetail when it resolves. So the Mint Check -> /market?item=<name> link opens the item view directly. Caveat: an item with no live listings falls through to the list view rather than an empty detail panel
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.21.3

v0.21.2

August 26, 2026

  • feat(mint-check): /mint-check?q=<tokenId | GunzScan URL | OpenSea URL> prefills the input and runs the check on load (useSearchParams under a Suspense boundary, single-run ref guard). Submit path lifted into runCheck(value); no other page behaviour changed
  • app/[locale]/market/components/ListingDetail.tsx: the Token # value in both the desktop table and the mobile list is now a locale-aware Link to /mint-check?q=<tokenId>. Label text unchanged. Bare tokenId only: MarketListing carries no contract field and buildOpenSeaUrl hardcodes the single Game Item contract, so listings are single-contract by construction
  • Mint Check ranked and commodity results gain one link under the chain panel, "View market listings for {itemName}", to /market?item=<name> - the param useMarket already reads to open ListingDetail
  • components/Navbar.tsx: /market added to the Intelligence dropdown after Mint Check. nav.market already existed in all 8 locale files (es Mercado, fr Marché, tr Pazar), so no message changes. Note: ExperimentsDropdown (Leaderboard + Market) is not mounted anywhere, so /market had no nav or footer link before this
  • Bracket investigation, no change: Mint Check passes GameItem.supplyBracket to bracketLabel(), the same column SupplyCard and /api/lookup read. The indexer sets it from activeMints via bracketForActiveMints (<=10 Scarce, <=50 Limited, <=200 Circulating, >200 Abundant), so a 369-lifetime item with >200 active copies reading Abundant is the classifier’s output, not a composer bug. Follow-up in the same release: the ranked panel’s third cell now reads CLASS / Collectible (driven by COLLECTIBLE_MAX_MINTS from lib/supply/collectibleLine.ts, the same line that made the result ranked) instead of the availability bracket. Availability brackets describe live-supply competition; Mint Check speaks in the article’s collectible/commodity vocabulary at the 500-lifetime line. Bracket data still flows to the commodity panel
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.21.2

v0.21.1

August 26, 2026

  • fix(ui): serial-derived values are labelled Serial, not Mint. The enriched mintNumber / mintNumbers fields are sourced from the metadata "Serial Number" trait (lib/blockchain/avalanche/service.ts), which the serials-vs-mints field note shows is not chain mint order. Relabelled: NFTGalleryControls sort options (Mint # -> Serial #) and search placeholder, NFTDetailTraitsSection label (Mint Number -> Serial Number). Display strings only; mintNumber, mintNumbers, mintDisplay, mintData and groupedRarities keep their names, no enrichment cache schema bump
  • components/nft-detail/MintPositionCard.tsx heading now reads "Mint Position (on-chain)". Chain-derived mint position (lib/supply/mintPosition.ts, /api/supply/mint-position) is unchanged and premium gating is untouched; "Mint" vocabulary is now reserved for it
  • app/[locale]/market/components/ListingDetail.tsx: the listings table column that renders the token ID was headed "Mint #"; it now reads "Token #". Value and logic unchanged
  • Verified, no change needed: a legitimate serial 0 (tokenURI and canonical metadata both return 0) already survives to display as #0, and alphanumeric serials already pass through stripLeadingZeros / isNumericMint unmodified and sort after numeric ones
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.21.1

v0.21.0

August 26, 2026

  • feat(mint-check): public single-token lookup at /mint-check (app/[locale]/mint-check). Paste a raw tokenId, a GunzScan token instance URL or an OpenSea asset URL (lib/supply/parseTokenInput.ts, pure + unit-tested) and see the metadata Serial Number trait next to the on-chain mint position, framed as in /serials-vs-mints.html. Verdict line covers first mint, straggler (>=90 days after the previous mint of the item), tail of the run (>=90% of lifetime mints) and the plain case; the serial clause drops out when the serial is unavailable
  • lib/supply/mintCheck.ts: getMintCheck(tokenId, contractAddress) composer. Exports findMintRow from mintPosition.ts (select widened with toAddress + txHash, no ranking change), then getMintPosition for rank/commodity (same COLLECTIBLE_MAX_MINTS line and honesty flags as the portfolio), GameItem counts + bracketLabel(), burn check on (tokenId, contractAddress, type=burn), and for ranked tokens one query for the previous mint on (blockNumber, logIndex) order to derive gapDaysFromPreviousMint. Result kinds: ranked | commodity | unattributed (mint row, null gameItemId) | not_found (no mint row)
  • GET /api/supply/mint-check: anonymous, no entitlement check, no softAuth import. tokenId validated as digits, contractAddress validated as 0x hex and EIP-55 normalised via ethers.getAddress (supply_events stores checksummed addresses). Per-IP limit 30/min on sha256(x-forwarded-for) via lib/utils/rateLimiter, 429 carries Retry-After: 60. Cache-Control public s-maxage=300/swr=600 for ranked and commodity; 60s for not_found and unattributed so a fresh mint is not pinned. The serial is fetched client-side through the existing /api/metadata proxy with a 4s race, never on this route
  • Footer link (footer.mintCheck key added to all 8 locale files, English) and /mint-check in app/sitemap.ts. Premium mint-position routes and MintPositionCard untouched
  • Tests: parseTokenInput (3 input forms + garbage), getMintCheck against a fake supply_events table (all four kinds, burned, gap on log order, contract scoping), route (400s, checksum normalisation, cache split, per-IP 429 with Retry-After)
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.21.0

v0.20.0

August 26, 2026

  • feat(field-notes): new /field-notes route (app/[locale]/field-notes/page.tsx, server component, revalidate 3600) listing long-form articles from lib/data/fieldNotes.ts. Entries carry slug, title, summary, ISO date, href, tags and readMinutes; static .html hrefs render through a plain anchor rather than the locale-aware Link so they are not prefixed (#304)
  • First entry is the serials-vs-mints explainer shipped in #303 as public/serials-vs-mints.html. It was unlisted there (noindex meta + robots.txt Disallow); both removed now that a public index links to it. AdminPanel link description updated to match
  • Landing page nav (HomeClient.tsx, desktop GlitchLink + mobile menu): Onchain Explorer replaced by Field Notes. /explore remains linked from PublicNav and the Intelligence dropdown; only the landing page entry moved
  • fieldNotes nav key added to all 8 locale files (es/fr/tr translated, rest English per the onchainExplorer precedent). /field-notes added to app/sitemap.ts at priority 0.6
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.20.0

v0.19.8

August 26, 2026

  • fix(account): the shared alert-config number input coerced every keystroke with parseFloat(e.target.value) || 0 (#292), so clearing the field wrote the number 0 and re-rendered it, and the 0 could not be deleted. The same coercion ate the decimal point, since parseFloat("0.") is 0 and the controlled input reset to "0", which put the 0.05 placeholder on gun_price out of reach typing left to right. Pre-existing since f804575; grep of the #289 and #290 diffs for parseFloat returns 0, so neither introduced it
  • A persisted 0 was not inert downstream. floor_drop and snipe_alert read the threshold through ?? 20 and ?? 10, which do not catch 0, so thresholdPct became 0 and dropPct >= 0 matched on every run. gun_price guards with !threshold, which is true for 0, so that subscriber was skipped permanently. A read-only audit of alert_preferences on production found 0 rows with threshold 0, 0 negative and 0 non-numeric, so no user was affected and no backfill is needed
  • The input now holds the raw string while being typed and one module-scoped normalizeConfig coerces at the two persist points, the Save handler and the toggle. An emptied field drops its key rather than storing 0, since absence is what the cron fallbacks are for; a deliberate 0 still survives. Number() rather than parseFloat() so the whole string must be numeric, as parseFloat("12abc") would quietly persist 12 while the selects need non-numeric text to pass through
  • isDirty compares normalizeConfig(localConfig) against savedConfig so a typed "20" still matches a persisted 20; without that the row would read dirty forever and the dimming from #290 would never engage. The alertConfigs[type] !== undefined sentinel is unchanged and nothing seeds state with the display defaults. Also adds min="0" and step="0.01", since no threshold here is negative and the price field wants cents
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.19.8

v0.19.7

August 26, 2026

  • feat(account): the alert-config Save button, one inline JSX node rendered per ALERT_TYPES.map iteration, reported success only through a sonner toast at top-right (#289). The button itself never changed, so the confirmation landed far from the click, and updatePreference returning false surfaced nothing at all. Added a per-type saveStatus map driving idle to saving to saved to revert, plus the toast.error that was missing on failure
  • Keyed by alertDef.type so the three config rows (gun_price, floor_drop, snipe_alert) stay independent. Saved uses --gs-profit rather than --gs-lime, which already means toggle-on on the row directly above and would read ambiguously. Revert timers live in a ref and are cleared on unmount, since the button is conditionally rendered on isEnabled and can unmount mid-timer; an in-flight timer is also cleared at the top of the handler, so a rapid second save restarts the confirmation window instead of being cut short by the first
  • feat(account): the same button was always enabled and always looked active, including on an untouched row (#290). Added per-row isDirty as alertConfigs[type] !== undefined && !isShallowEqualConfig(localConfig, savedConfig), baselined on the persisted pref.config rather than any input placeholder, and dimmed plus disabled the button when they match. A row typed into and then reverted to the saved values re-dims, which is the intended reading
  • No shallowEqual helper exists in lib and the page does not import lodash, so a module-scoped one-level compare was added in-file rather than pulling in an import; config values are primitives, and it checks both key sets so a key present in only one object reads dirty. Branch precedence puts saved above dimmed so a fresh save still reads SAVED. Opacity moved from a trailing disabled:opacity-50 into the individual branches, because disabling the button for a clean row would otherwise have muted the green confirmation added in #289
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.19.7

v0.19.6

August 25, 2026

  • fix(developers): the /developers API access CTA linked to /account?tab=settings (#288), which renders the Settings tab and no API surface. A profile with no ApiAccessRequest row therefore had no reachable route to the request form, so the self-serve access shipped in v0.19.0 had no walkable entry point for the new developers it was built for
  • The form was never gated on the row existing. ApiAccessSection renders its none state (pitch plus form) on activeTab === "api" at account/page.tsx:1338, which is independent of buildAccountTabs, and ?tab=api survives the tab-defaulting effect because that effect returns early on any tab param. /account?tab=api already rendered the working form; only the link target was wrong
  • components/developers/DeveloperCta.tsx: href /account?tab=settings to /account?tab=api. Copy aligned to the destination button in both variants, "Request API Access" to "Get My Free Key" and "Sign In To Request Access" to "Sign In To Get Your Free Key", so the entry point states the same instant-access promise the form actually delivers. The component renders twice on /developers, so one fix covers both
  • app/[locale]/developers/page.tsx: the same stale target in the Getting Started step, the usage and quota pointer and the free-tier howToGet copy, all three of which also described an "Account, Settings, API Access" navigation path that no longer exists. components/account/ApiAccessSection.tsx: a comment still placed the section under the settings tab it was moved out of, which is the likely origin of the stale links
  • Not changed: the apiRequestStatus gate that withholds the API tab chip until a request row exists. Whether API discovery belongs on the account page is a product decision, and accountTabs.ts states the intent that it belongs on /developers. The href fix alone makes the funnel walkable. Residual, deliberately left: on a first visit the strip shows no API chip, so leaving the tab loses the form until the request is submitted
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.19.6

v0.19.5

August 25, 2026

  • fix(auth): gs_session SameSite changed from Strict to Lax (#286). Strict withholds the cookie on any cross-site top-level navigation, so a user arriving from an OAuth provider redirect or an inbound link to a protected page carried no session and proxy.ts bounced them to /. Lax sends it on top-level GET navigation, which is exactly that case, and still withholds it on cross-site POST, PUT, PATCH and DELETE
  • Safe now in a way it was not before: cross-site admin mutation is blocked structurally by the Origin check in adminAuth and the admin proxy (#285) rather than by cookie policy, and the audit preceding that change confirmed every gs_session-authenticated GET is read-only, so the navigation Lax newly permits cannot change state
  • lib/auth/sessionCookie.ts: the set and the clear now read one shared COOKIE_SAME_SITE constant instead of two literals. They have to agree, because a Strict clear against a Lax set can fail to delete the cookie in some browsers, and sharing the constant makes drifting apart impossible rather than merely unlikely. Scoped to gs_session alone (the helper hardcodes its own cookie name); httpOnly, secure, path and maxAge are unchanged, and gs_ref, gs_has_session and the two OTR cookies keep their own settings
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.19.5

v0.19.4

August 25, 2026

  • fix(security): cross-site admin mutations are now refused by an Origin check on cookie-authenticated mutating verbs (#285). Applied at both auth chokepoints, the admin proxy and the session branch of verifyAdminRequest, because the admin routes are reachable two ways and covering one would leave the other open. The protection is now structural rather than a property of the session cookie SameSite setting, which is what previously carried it
  • lib/server/adminAuth.ts: new exported isCrossSiteMutation(), scoped to POST, PUT, PATCH and DELETE on the cookie-authenticated path. Reads are untouched: every session-authenticated GET was audited and is read-only, and they must stay cookie-accessible. The ADMIN_SECRET bearer branch is untouched and short-circuits first, so scripts and server-to-server callers, which send no Origin, keep working. Fails closed rather than open
  • Compares host rather than full origin on purpose: behind a TLS-terminating proxy request.url can report a different scheme than the browser does, and a scheme mismatch would fail closed and lock admins out of their own tools, which is worse than the narrow same-host-different-scheme gap that HSTS already covers
  • Chosen over requiring a token on mutations because it needs no client changes at all. The admin UI has 27 mutating call sites across 8 components and 26 send no auth header, so a token requirement would have been a large edit where any missed call site fails open. Reading a browser-supplied header also cannot reintroduce the v0.19.1 race in which a not-yet-populated token is indistinguishable from a denial. Exercised end to end on both chokepoints against a running server
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.19.4

v0.19.3

August 25, 2026

  • fix(auth): the admin route guards on /strategy, /roadmap and /brand were firing router.replace during the embedded-wallet provisioning window (#284). All three read `if (sdkHasLoaded && !isAdmin) router.replace(...)`, which conflates two states that mean opposite things: the wallet has not arrived yet, and the wallet arrived and is not an admin. sdkHasLoaded true with a non-null user and a null primaryWallet is a real state that resolves seconds later, because an embedded wallet is provisioned asynchronously after social login (useAutoLogin polls 10x500ms for the same lag). A real admin was redirected off their own page with no way back except a reload timed after the wallet landed
  • lib/auth/useAdminGate.ts: new shared hook returning a loading / admin / denied verdict. It resolves on any of three terminal signals: no user at all (anonymous, terminal immediately), a primaryWallet address present (verdict final), or a bounded 5s grace expiring. Waiting indefinitely was not an option because an authenticated session can be terminally wallet-less (email-only, or a Ledger session that restores without the hardware reconnecting) and would otherwise hang on a spinner. 5000ms matches the window useAutoLogin already uses for this lag; the 600ms in WalletRequiredGate is a flash-prevention delay for a different purpose and would expire mid-provisioning. Ordering follows useOtrPlaybackAuth, which checks still-resolving before anonymous
  • The verdict lives in one hook rather than three copies, since three hand-copies of a subtle condition are what produced three copies of the bug. The hook deliberately does not redirect: each page keeps its own router.replace so unmount ordering stays with the component that navigates. The grace timeout is cleared in effect cleanup, which makes a post-navigation fire impossible, and a test asserts zero pending timers after unmount. Render branches are unchanged: the same neutral spinner covers both loading and denied, so nothing new is leaked to a non-admin. the IntersectionObserver effect in brand effect gates on the same verdict. 7 new tests
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.19.3

v0.19.2

August 25, 2026

  • fix(auth): app/[locale]/layout.tsx now treats a verifiable gs_session as a second eager-load trigger alongside gs_has_session (#283). gs_has_session was the only switch that made AuthProviders load the Dynamic SDK, and it is written client-side on onAuthSuccess alone, never re-asserted when the SDK restores a session from localStorage. Once it expired or was cleared, the SDK was never fetched, so primaryWallet never resolved and isAdmin stayed false, stranding a live session in fallback state with no path to recover: the code that would repair the cookie lives inside the SDK the missing cookie prevents loading. gs_has_session is still checked first so the common path short-circuits before any crypto, and anonymous visitors still load zero SDK chunks
  • lib/auth/sessionCookie.ts: new verifySessionCookie(), HS256 signature and expiry only, no ban check, since the layout runs it on every render and enforcement belongs to proxy.ts. The three inline copies of the same jwtVerify call in proxy.ts, lib/server/softAuth.ts and app/api/admin-proxy are deliberately untouched; migrating them onto the helper is a separate cleanup. Verified against a running server: valid gs_session goes eager, while expired, forged and malformed tokens all stay in fallback
  • lib/providers/DynamicProvider.tsx: the gs_session clear moves into the SDK onLogout handler, symmetric with the client-side gs_has_session clear already there. It was previously issued only by the two disconnect buttons, leaving eight other logout paths (validate-denied, OTR sign-out, token expiry, cross-tab broadcast) with a live cookie, which now gates SDK loading. Uses keepalive so a route change cannot cancel it in flight. The explicit calls in Navbar and WalletButton remain as belt-and-braces
  • scripts/migration-smoke/phase4-matrix.mjs: row 8 asserts gs_session absence after logout, read off the browser context because the cookie is httpOnly. Its zero-SDK-chunks invariant now depends on that cookie being cleared, so the test asserts it directly instead of inferring it
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.19.2

v0.19.1

August 25, 2026

  • fix(auth): the Navbar whitelist-validation effect now waits for the Dynamic JWT before it POSTs /api/access/validate. On a cold load the SDK restores user and primaryWallet in a single commit, while RealAuthBridge (a parent) publishes authToken from its own effect, which React runs after the child effect. The effect therefore read an empty token mirror and sent the request with no Authorization header. That route answers a token-less request with 401 { success: false }, which is indistinguishable from a real denial, so the handler called handleLogOut() on every hard refresh. In-app navigation was unaffected because the module-scope mirror in lib/auth/getAuthToken was already warm, which is why the bug presented as refresh-only (9a322a1)
  • components/Navbar.tsx: the effect gates on the reactive authToken from useAuth() and lists it in the dep array, so it re-runs the moment the token lands instead of firing early. This is the same wait useAutoLogin already performed with its bounded poll; the AuthContextValue.authToken field postdates that hook. validatedWalletRef now records the address only after a definitive response, so a token-less pass no longer burns the one-shot and stays retryable, and a second validatingWalletRef preserves the one-request-per-address guarantee. Genuine denials with a valid token still log out, and the network-error path still fails closed
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.19.1

v0.19.0

August 25, 2026

  • feat(portal): free API access is granted at submit. developerPortalService.submitAccessRequest writes status approved, requestedTier DEFAULT_TIER and rateLimitPerMin DEFAULT_RATE_LIMIT on a FIRST submission, with reviewedBy set to the new AUTO_APPROVAL_ACTOR sentinel ("auto-approve") so a machine approval is distinguishable from a person in the admin list and any later audit. The sentinel is capped at 12 characters because the admin row renders reviewedBy through truncateAddress, which middle-truncates anything longer
  • A rejected row that resubmits still returns to pending for human review, deliberately: auto-approving it would let a refused developer bounce straight past whoever refused them. Suspended and pending rows are unreachable from that branch, 409ing as before. Partner and paid are never self-served; raising a tier stays reviewDeveloperRequest on an existing row
  • test(portal): the prisma mock honoured neither reviewedBy nor reviewedAt on create, harmless while nothing set them at creation time and silently fatal once something did. It reads them from data now. The pending-row assertions that used to come free from a fresh submission reach that state the only way left, rejected then resubmitted, and new cases pin the free-tier ceiling, the rejection guard and the suspension guard. 47 tests pass
  • feat(portal): ApiAccessSection.handleSubmit awaits loadKeysAndUsage when the response returns approved. A first submission swaps the panel straight to key management, which would otherwise mount with an empty key list until the next page load. Pitch, button label and toast follow: the button reads Get My Free Key rather than Request Access, because it no longer sends a request anywhere
  • docs(developers): four blocks that promised a review no longer run were rewritten, in the same commit as the behaviour per the DOCS MIRROR THESE SHAPES BY HAND signpost in lib/server/apiV1Supply.ts. Getting Started steps, the what-gets-approved paragraph, the three How-to-get-it cells in the tier table, and the closing CTA. Partner and paid are reframed as a raise on the key you already hold rather than a separate queue, which is what they have always been in the code
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.19.0

v0.18.0

August 22, 2026

  • feat(api): /api/lookup split along the premium line (#262). The snapshot fields (itemName, rarity, category, supplyBracket, activeMints, totalMints, totalBurns) serve every caller from ?q=/?slug=/?list= and keep public s-maxage=60, because those payloads no longer vary by caller. firstMintAt and lastMintAt moved out of the item payloads into detail mode, next to dailyCurve, recentMints and dayOneShare. ?id= resolves the entitlement once (softAuthenticateWithEntitlement + canViewIntelligence(auth, "premium")), strips the shared full-payload cache per caller, and answers Cache-Control: private, no-store with an additive gated: "premium" marker
  • lib/supply/resolveSlug: mint dates dropped from the SELECT, supplyBracket added. The resolver feeds the server-rendered /supply/[slug] payload, which no request gate can inspect, so gating the API alone would have left the dates in the SSR output. The query can no longer serialize what it does not select
  • app/[locale]/supply: the free card keeps its anatomy and gains a supply-bracket chip (bracketLabel). The mint window, the lifetime curve and the recent-mint feed collapse into one PremiumUpsell that names what unlocks. Minted This Week goes with them, since it is lastMintAt arithmetic and the DB isActivelyMinting latch never clears, so it is not an honest free substitute. On a detail card the upsell is the only call to action; the landing footer CTA becomes an auth-aware usePremiumHref link
  • test: 14 tests across the endpoint matrix (anonymous, free and unresolved callers versus premium, header assertions, snapshot modes proven identical across callers, the shared detail cache proven stripped per caller) and the page (a gated card keeps every free number, renders exactly one upsell, and carries zero time-dimension labels)
  • scripts/normalize-enriched-items-casing.mjs tracked: the guarded one-shot that lowercased the 4,626 checksummed enriched_items singletons left after the #261 dedup backfill. It prechecks for collisions and refuses to write while any duplicate group exists
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.18.0

v0.17.0

August 21, 2026

  • refactor(api): shared v1 supply handler core. createSupplyHandler (lib/server/apiV1Handler.ts) owns auth + reject, the usage-capture closure, the try/catch frame, an optional in-memory cache (lib/server/apiV1Cache.ts) and the jsonWithCors plumbing; each route supplies verb, slug, an explicit cachePolicy, parse and load. The five free routes were migrated with a byte-identical response diff against origin/main (#246)
  • feat(api): free/pro namespace split (#247). /api/v1/supply/ (item, batch, rankings, health) now serves the free item shape { itemName, rarity, bracket, activeMints, imageUrl } over a narrower select that never reads the pro columns; /api/v1/pro/supply/ (item, batch, rankings, drops) serves the full nine-key shape. Free rankings accepts activeMints (default), supplyBracket and itemName and answers any other sort with 400; pro rankings accepts all six orderings with totalMints default and fallback. Free /drops is removed (404); drops is pro only
  • feat(api): tier entitlements (lib/server/tierEntitlements.ts). ApiKey.tier stays free text; only "paid" unlocks pro, "partner" deliberately does not, and an unrecognised value fails closed and logs. A non-entitled key on a pro route gets 403 { "error": "This endpoint requires a paid tier" } via the new not_entitled reason, recorded as the new tier_denied usage outcome before params are parsed or anything is queried, so a refusal never leaks even the free shape. Pro responses are Cache-Control: private, no-store. Pro traffic records pro-item / pro-batch / pro-rankings / pro-drops slugs
  • docs(api): /developers rewritten for the split. Free endpoints lead with free-shape samples matching the DTO key for key; pro section follows with the full shape and the upgrade path (swap /supply/ for /pro/supply/); tiers explained (free, partner, paid); 403 row added to the errors table; per-namespace sort behaviour documented; versioning restated as additive from the split baseline. Account API Access pitch and the in-code contract comment updated to match
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.17.0

v0.16.0

August 18, 2026

  • feat(api): public API v1 at /api/v1/supply/ - five keyed endpoints (item, batch, rankings, drops, health). Auth is the X-API-Key header, deliberately not Authorization: the edge proxy verifies any Bearer token as a Dynamic session JWT and would reject an API key before the route ran. Responses are curated DTOs frozen as the v1 contract rather than raw rows, so internal shape changes cannot leak onto the public surface (#227)
  • feat(api): per-key rate limiting, 60 req/min on the free tier. An in-process burst shield sits in front of an atomic per-minute counter, so the ceiling holds across serverless instances instead of per instance; over-limit returns 429 with Retry-After. Per-key daily counters back the portal usage view. Expired minute counters are swept by the existing 15-minute cron rather than a new scheduled job
  • feat(api): supply brackets exposed in the public vocabulary (abundant / circulating / limited / scarce), mapped from the internal slugs at the response boundary and derived from the same labels the UI uses, so the two cannot drift. Scarcity stays distinct from the in-game rarity tier, which ships as its own field
  • feat(api): CORS on every v1 route including OPTIONS preflight, so browser and mobile clients can call the API directly. CDN cache hits bypass the limiter by design for v1: the payload is identical for every key, so no Vary is set
  • feat(portal): self-serve developer portal (#228). Public docs at /developers carry the frozen v1 contract and the terms of use, including the attribution requirement for any surface displaying the data. Signed-in users request access from account settings; approved developers create and revoke up to 3 active keys and read their own usage. Keys are stored hashed with only a short prefix retained for identification, and the full key is returned exactly once at creation
  • feat(portal): admin review queue for approve, reject and suspend, with per-developer tier and rate-limit overrides. Suspension revokes live keys rather than adding a branch to the hot auth path, so verification stays a single indexed lookup. Revoking a key that belongs to another account reports 404 rather than 403, so the endpoint cannot be used to probe for foreign key ids
  • components/ui/VersionBadge.tsx: APP_VERSION bumped to v0.16.0 (#229)

v0.15.3

August 16, 2026

  • hotfix(api): /api/supply/status was 504-ing since the v0.15.x indexer deploy. The unlinked-mint gauge added to the route could not be served by idx_supply_events_unlinked — that index is btree("blockNumber") WHERE ("gameItemId" IS NULL), so a count that also filters type='mint' and contractAddress heap-fetches every row under the predicate (~600k, all burns included) on a 48M-row table. Count removed from the route; the gauge stays where it is actually cheap — logged by the indexer at boot and after every sweep pass (grep "Unlinked mint gauge"). The /supply live proof line reads this endpoint, so it was silently degraded
  • components/ui/VersionBadge.tsx — APP_VERSION bumped to v0.15.3

v0.15.2

August 16, 2026

  • fix(api): split-brain caching removed from the last three supply routes — /api/supply/drops, /rankings, /mint-position served an anonymous stub and the full payload from ONE URL under public s-maxage, so a shared cache could hand signed-in users empty payloads or anonymous callers full data. All three now route through canViewIntelligence(auth, "supply") (public today, policy-flippable without route edits) with a private-cached stub — the same fix #213 applied to /api/supply/item and /batch
  • components/ui/VersionBadge.tsx — APP_VERSION bumped to v0.15.2

v0.15.1

August 16, 2026

  • app/[locale]/intelligence/item/[id] — aggregate-first stat row: with multiple rarity variants the headline shows NAME-level totals under an "All rarities combined" caption; the variant-specific day-one-share cell is swapped for a rarity count so aggregate and per-variant numbers never share a row. Single-variant pages pixel-identical (#226)
  • lib/intelligence/searchDedupe — pure, tested dedupe for lookup surfaces: one row per item name, routed to the dominant variant (largest active supply), showing aggregate supply and an "N rarities" pill instead of a misattributing single rarity badge. ItemLookup opts in via dedupeByName; the compare page deliberately keeps per-variant rows
  • components/ui/VersionBadge.tsx — APP_VERSION bumped to v0.15.1

v0.15.0

August 16, 2026

  • lib/server/intelligenceAccess — per-signal access policy: supply is public, valuation stays gated. Routes consult canViewIntelligence(auth, signal) so a future policy flip needs no route edits (#213)
  • app/api/intelligence/item/[id] — serves supply fields publicly; valuation stripped per caller AFTER the cache read (single cached payload, never served whole to anonymous callers). Cache-Control: private — payload varies by auth on one URL
  • app/api/intelligence/items/search — public, ILIKE-backed with 30 req/min per-IP rate limit (hashed IP), 2-char minimum query, 60s cache. Payload identical for every caller, so public CDN caching is safe
  • app/[locale]/intelligence/item/[id] — intelligence-early blur removed, page is public; per-rarity variants table + name-level aggregate row (GameItem is unique per contract+itemName+rarity); SupplyCard honesty caveat carried onto the page (SUPPLY_TOOLTIP now exported); valuation renders a sign-up gate for anonymous visitors
  • fix(api): split-brain caching removed from supply item and batch routes — soft-auth redaction under a public cache header could poison shared caches in either direction
  • components/ui/VersionBadge.tsx — APP_VERSION bumped to v0.15.0

v0.14.1

August 13, 2026

  • lib/intelligence/rarity.ts — supply-bracket display vocabulary replaced: ultra-rare/rare/uncommon/common DB keys now render as Scarce/Limited/Circulating/Abundant. The old labels reused the quality-tier words, so a 200+-supply item whose game rarity is Uncommon showed a prominent "COMMON" pill — two unrelated taxonomies colliding on one card. DB keys, enum types, and indexer bracket assignment unchanged; labels only
  • Render-site unification — every bracket label now routes through bracketLabel(): app/api/intelligence/scarcity (local BRACKET_LABELS record dropped for shared BRACKET_LABEL/BRACKET_RANGE; response shape unchanged), app/[locale]/scarcity OnChainSupplyTab (4 call sites + bracket column tooltip), components/nft-detail/SupplyCard (label from shared vocabulary, per-card desc/color stay local), ScarcityComposition editorial prose. ItemBadge, RareMoving, TopMovers already used bracketLabel and picked the change up for free
  • app/[locale]/intelligence/item/[id] — header hierarchy fixed: game rarity now renders through RarityBadge (styled pill, dominant), the supply-bracket pill is demoted behind a "supply" caption. Previously the bracket pill sat unlabeled next to the title and read as the item’s rarity
  • components/ui/VersionBadge.tsx — APP_VERSION bumped to v0.14.1

v0.14.0

August 12, 2026

  • app/[locale]/supply/[slug] — shareable per-item URLs: server page resolves the slug case-insensitively via a query-time computed slug (lib/supply/slug.ts slugifyItemName + SQL mirror, no migration), renders the same card the search flow shows, generateMetadata emits per-item title/description/og:image. Unresolvable slug renders the search landing with a notice, HTTP 200. Selecting a result rewrites the URL via history.pushState; popstate handles back/forward through /api/lookup?slug= (#221)
  • app/api/og/supply/[slug] — OG card route cloning the portfolio OG pattern (edge, ImageResponse, brand fonts, corner brackets, 1200x630). Reads supply numbers via @neondatabase/serverless (Prisma unavailable on edge) so shared cards cannot be forged via query params; rarity variants aggregate; s-maxage=3600 + SWR=86400
  • app/api/lookup — additive ?slug= (URL resolution) and ?list=rarest|recent|largest (top-5 zero-query lists) modes. List mode exists because /api/supply/rankings is auth-gated and this page is public; reuses the rankings honesty filters (activeMints > 0, BASE_LIMB_NAMES excluded). ?q=/?id= byte-identical
  • app/[locale]/supply — zero-query entry lists (Rarest On-Chain / Recently Minting / Largest Supply) on the empty landing state; footer coming-soon teaser replaced with a sign-in CTA reusing the navbar auth entry (fair value is live behind auth, not coming soon); live proof line "LAST MINT INDEXED: item // n min ago" from the new additive lastEvent object on /api/supply/status (no wallet in payload, existing fields unchanged, one fetch, no polling)
  • components/ui/VersionBadge.tsx — APP_VERSION bumped to v0.14.0

v0.13.2

August 12, 2026

  • app/[locale]/supply — "Actively Minting" badge replaced with "Minted This Week", derived client-side from lastMintAt (≤7 days). The DB isActivelyMinting flag is a one-way latch (indexer listener sets true on every live mint, nothing clears it), so it read true for items whose last mint was months ago. Tooltip states the badge says nothing about supply caps — only Gunzilla can lock supply
  • components/ui/VersionBadge.tsx — APP_VERSION bumped to v0.13.2

v0.13.1

August 12, 2026

  • app/api/lookup — detail mode (?id=<gameItemId>): daily mint/burn curve from daily_item_mint_counts, last 10 mints from supply_events (ROW_NUMBER over ascending block order = true mint position, anonymizeWallet from lib/intelligence/format), dayOneShare computed from the curve. Same rate limit, separate 60s detail cache, 404 on unknown/zero-mint ids
  • app/[locale]/supply — detail card now renders lifetime activity sparkline (inline SVG, lime mints + coral burn overlay, zero-filled calendar days so a year of silence reads as a flat tail instead of one pixel), recent-mints provenance list (#position, anonymized wallet, time-ago), and day-one share. Detail fetch is progressive enhancement — stat card renders instantly from the search row, curve/mints sections appear when the fetch lands, failures degrade silently
  • components/ui/VersionBadge.tsx — APP_VERSION bumped to v0.13.1

v0.13.0

August 12, 2026

  • app/[locale]/supply — public item supply lookup page: debounced name search, result list, stat-strip detail card (active supply / lifetime mints / burned / mint window), honesty caveat re items earned in-game but not yet minted on-chain. Ships the supply signal standalone — no valuation, provenance, or trends; those stay behind the intelligence gate (#215)
  • app/api/lookup — public fuzzy GameItem search backing the page: supply-only fields, ILIKE via Prisma contains/insensitive, totalMints > 0 filter, 30 req/min per hashed IP (same isRateLimited helper as intelligence search), min query length 2, 60s in-memory cache + public s-maxage=60 CDN headers. New route — no auth changes to the existing gated /api/supply/* or /api/intelligence/* surfaces
  • components/navbar/IntelligenceDropdown.tsx + Navbar.tsx mobile menu — Supply Lookup entry between Overview and Onchain Explorer for authenticated users; dropdown active state includes /supply. supplyLookup nav key added to all 8 locale files (es/fr/tr translated, rest English per onchainExplorer precedent) (#216)
  • app/sitemap.ts — /supply registered (daily, 0.8)
  • components/ui/VersionBadge.tsx — APP_VERSION bumped to v0.13.0

v0.12.2

August 12, 2026

  • components/nft-gallery/NFTGalleryGridCard.tsx — owned quantity renders as a bordered lime chip in the info row (font-bold, color var(--gs-lime), bg/border via color-mix from the same var so brand retunes cannot drift the tint — the hardcoded-rgba first draft was already one retune behind). Chip no longer displaces the rarity name: same-rarity groups show both; mixed-rarity groups show chip only. Artwork stays badge-free per the SupplyBadge/scarcity-dot removal decision
  • components/ui/VersionBadge.tsx — APP_VERSION bumped to v0.12.2

v0.12.1

August 11, 2026

  • next.config.ts — images.remotePatterns: added *.z03.azurefd.net, pathname-constrained to /live/itemsthumbnails/** (shared AFD shard, constraint keeps /_next/image from proxying arbitrary tenants). Gunzilla migrated item thumbnails from cdne-g01-livepc-wu-itemsthumbnails.azureedge.net to Azure Front Door (azureedge.net retirement); metadata.gunzchain.io and GunzScan now emit the new host, which the optimizer 400‑d, so NFTImage fell back to the placeholder on every fresh metadata fetch. Old azureedge entry kept — 7‑day localStorage metadata cache still holds old-host URLs and the host still serves (#212)
  • next.config.ts — i.seadn.io added to remotePatterns and CSP img-src; the existing i2c.seadn.io entry was a typo, silently 400‑ing every imageHires (OpenSea CDN) request through the optimizer. i2c kept until historical payloads are confirmed clean. CSP img-src also carries *.z03.azurefd.net (still Report‑Only)
  • components/ui/NFTImage.tsx — one-way failed boolean replaced with failedSrc tracking (failed = src === failedSrc), so the onError placeholder clears when a new src arrives instead of latching forever
  • components/ui/VersionBadge.tsx — APP_VERSION bumped to v0.12.1

v0.12.0

July 30, 2026

  • lib/offtherecord/tracks.ts — added otr-012 (Scope's Open): audioUrl + lyricsUrl on Vercel Blob, durationSec 156, bpm 99, releasedAt 2026-07-30. Solo CRYPTOHAKI track; library mp3 carries the full OTR ID3 template (USLT lyrics, CC BY-NC 4.0, art preserved)
  • scripts/backfill-daily-mint-counts.ts — aggregation and write collapsed into one server-side INSERT ... SELECT ... ON CONFLICT DO UPDATE. The old shape (TRUNCATE, then ~1M aggregate rows into JS, then chunked createMany) exhausts the V8 heap at current table size and OOM‑d for real during the timestamp repair, leaving the rollup at 999,483 of ~1,045,000 rows after the truncate had already discarded it. No rows transit JS memory now, the destructive truncate is gone (existing rows are corrected in place), and a mid‑statement failure rolls back instead of destroying the rollup. Window semantics and the COALESCE("gameItemId","burnGameItemId") linkage unchanged; id derived from the unique key so re‑runs are stable (#199)
  • supply_events.timestamp / transfer_events.blockTimestamp — historical rows repaired from real block headers after the single‑anchor estimator was found drifting up to +69 days (transfer_events, 9,226,473 rows) and +38 days (supply_events, 45,879,574 rows). Verified 0 future‑dated rows, worst residual 19s on a 30‑row header sample; game_items aggregates and daily_item_mint_counts rebuilt on the corrected data (rollup cross‑checks exactly against source: 46,329,154 mints / 491,536 burns)
  • components/ui/VersionBadge.tsx — APP_VERSION bumped to v0.12.0

v0.11.1

July 28, 2026

  • app/globals.css — OTR glass blur 10px -> 6px (PR #195): through the 82%-opaque panes the two are visually indistinguishable (compared 10/8/6 over live ambient FX) while 6px measures ~6% less total GPU in the heaviest state. Removing the filter from the large panes outright was measured (~25% GPU back) and rejected on the visual gate; the CSS comment documents why
  • app/[locale]/offtherecord/OffTheRecordClient.tsx — swarm and trail streak-fades erase on a 2-frame cadence with the alpha computed from accumulated dt (pow(R, dt1+dt2) = pow(R, dt1)*pow(R, dt2)), so retention and streak length are unchanged at any refresh rate while the full-screen destination-out passes halve. Gas and metaball clears stay per-frame (their canvases fully redraw each frame). Ablation evidence in docs/investigations/otr-gpu-active-cost.md
  • indexer/src/ingestion/normalizer.ts — timestamp estimator re-anchors every 1,000 blocks so long backfills stop drifting from real block time (#197)
  • components/ui/VersionBadge.tsx — APP_VERSION bumped to v0.11.1

v0.11.0

July 28, 2026

  • hooks/useOtrIdentity.ts (new) — shared identity contract for the OTR reveal, consumed by both entry surfaces: state selection (stored gs_otr_handle -> return, absent -> first, frozen on the activation edge), input validation (trim, 24-char cap, non-empty), completion writes (first pins gs_otr_sessions to 1 and stores the handle as typed; return bumps; both write the identical unlock key/value, guarded once per activation), State C reset, and render-time uppercasing via displayHandle. Handles render as JSX text content only, so a stored HTML-looking handle is inert on every echo and greeting
  • components/KonamiOverlay.tsx — home reveal is stateful: first contact types SIGNAL INTERCEPTED / UNAUTHORIZED FREQUENCY // TRIANGULATING... then an inline "> IDENTIFY YOURSELF:" terminal input (blinking block caret, invisible input element, Enter submits, empty submit keeps blinking); a 650ms held beat after submit lets the adversarial -> warm turn land (OPERATOR [HANDLE] VERIFIED // STAND DOWN, YOU’RE ONE OF OURS). Returning operators get FREQUENCY RECOGNIZED / WELCOME BACK, [HANDLE] // SESSION [N] with the quiet "not [HANDLE]?" reset, measurably shorter (~4.1s trigger-to-route vs ~10s first contact). onComplete prop removed: this surface always routes to /offtherecord
  • app/[locale]/offtherecord/OffTheRecordClient.tsx — the locked screen deliberately does NOT mount the cinematic overlay (it is already through the door): sequence completion swaps the mark area for a lean in-place exchange over the same hook ("> IDENTIFY YOURSELF:" -> WELCOME, [HANDLE] -> unlock in place; or WELCOME BACK, [HANDLE] with the reset underneath). The unlock derivation now waits for completeReveal; the admin bypass keeps its silent immediate unlock with no prompt and no session bump
  • lib/offtherecord/unlock.ts — hasOtrHandle() and bumpOtrSessions() alongside the existing handle/session helpers, all try/catch-guarded; unit tests extended (18 cases). offtherecord/page.tsx documents that handle/session keys are post-hydration only and must be embedded as literals in the pre-paint guard if a future change needs them before first paint
  • Reduced motion: both surfaces present statically but still show the handle input on first visit and the greeting on return — the input is function, not decoration
  • components/ui/VersionBadge.tsx — APP_VERSION bumped to v0.11.0

v0.10.1

July 26, 2026

  • app/[locale]/offtherecord/OffTheRecordClient.tsx — silence watchdog converted from a frame count (150 frames, the intended 2.5s only at 60fps; 0.75s at 200Hz) to a wall-clock accumulator against OTR_SILENCE_TIMEOUT_S = 2.5s. Accumulates dt only while the analyser reads all-zero during claimed playback, resets on any audible frame, so track-change loading gaps and short rebuffers can never trip it at any refresh rate
  • vizFailed is no longer a permanent latch: while failed and playback claims to run, the loop keeps sampling the analyser and the first non-zero read re-arms the visualizer in place (EQ remounts, gas/splits resume, no reload). A truly dead analyser (CORS-silent source) only ever reads zeros, so the failed state cannot flap. Verified: 1.5s simulated stall at 200Hz does not trip; a dead analyser trips at ~2.6s; recovery re-arms in ~100ms
  • Frame-vs-time sweep of the same bug class: EQ bar ballistics dt-normalized to the 60Hz reference (attack 0.3/frame -> 1-pow(0.7, dt*60), idle decay 0.94/frame -> pow(0.94, dt*60), peak fall 0.02h/frame -> 0.02h*dt*60) — bars were up to 3.3x snappier on high-refresh displays. Swarm motion integration dt-normalized (steering forces and position step scale by swF; velocities stay px per 60Hz frame) — the murmuration flew 2x too fast at 120Hz, 3.3x at 200Hz. Known non-fix: analyser smoothingTimeConstant applies per read and still varies mildly with refresh rate
  • components/ui/VersionBadge.tsx — APP_VERSION bumped to v0.10.1

v0.10.0

July 24, 2026

  • lib/offtherecord/tracks.ts — added five entries to OTR_TRACKS: otr-007 + otr-008 — Solo & Echo takes of (They Can’t Fuck With Me), otr-009 (Whole Squad Ghost), otr-010 (Lucky To Be Here, ft. lost.aura), otr-011 (No Promises on Teardrop). Each carries audioUrl + lyricsUrl on Vercel Blob, notes, bpm, and real releasedAt dates
  • Synced .lrc files (word-timed via forced alignment) uploaded to Blob; lyricsUrl set so lib/offtherecord/lrc.ts renders the synced-lyrics panel for all four. OTR_TRACKS stays numeric-ordered (…006, 008–011); the 007 main cut folded into otr-008 as the single published version
  • components/ui/VersionBadge.tsx — APP_VERSION bumped to v0.10.0

v0.9.0

July 12, 2026

  • lib/services/accessGateSql.ts — allowedFromGateRow(row) reduced to !row.banned. The whitelist and verified-claim columns are still selected for observability but no longer participate in the decision. Node (hasAccess) and Edge (checkAccessEdge) consume the same predicate so both gates flipped to bans-only atomically
  • app/api/access/validate/route.ts — full rewrite to an open-access gate with proven identity. Requires Authorization: Bearer <Dynamic JWT>; the session identifier is derived from the token’s verified_credentials (wallet credential first, lowercased; else email as "email:<lower>") via extractIdentifier, which MUST stay identical to the proxy’s twin. The request body no longer determines identity — a body identifier that disagrees with the token is logged as IDENTITY MISMATCH and ignored. Flow: 401 without a valid token or extractable identity, 403 { banned: true } for banned identifiers, gs_session cookie for everyone else. Dropped: auto-whitelist via oauth credential, whitelist status branches (permanent/trial/expired), verified-claim inheritance, waitlist join, custom-slug claim
  • app/[locale]/HomeClient.tsx — removed the waitlisted/trialExpired redirect branches and the gateLoading state. View-only wallet submit no longer calls validate (no JWT to send) — the proxy already allows /portfolio?address= without a session, so it just navigates. All authenticated validate calls now bail early when getAuthToken() returns nothing (the call would 401 anyway) and re-fire off the user dep once Dynamic issues the token. Email-only users validate for a session and land on /account instead of the waitlist flow
  • proxy.ts — a valid gs_session cookie failing checkAccessEdge now means exactly one thing (banned), so the failure redirect moved /waitlist → / for both enforcement points
  • Tests — validate.verifiedClaim.test.ts replaced by validate.openAccess.test.ts (13 cases: 401 matrix, ban 403, cookie issuance, token-over-body identity, mismatch logging); accessGateSql + hasAccess suites updated to the bans-only contract. 19/19 green
  • Waitlist sunset — /waitlist is now a server-side redirect('/'); WaitlistClient, useWaitlist, waitlistService, /api/waitlist/status, /api/admin/waitlist, and /api/access/reconcile deleted. whitelistService drops the promoteFromWaitlist re-export and the addToWhitelist waitlist-cleanup side effect; admin whitelist DELETE no longer calls revokeWaitlistPromotion. WaitlistEntry model kept in schema with a deprecation comment; table drop lands in a dedicated migration PR
  • Referral attribution decoupled — recordWalletConnected deleted from referralService (whitelist guard, waitlist membership check, incrementReferralAndCheckPromotion); /api/referral/track accepts clicked | portfolio_loaded only. /r/[slug] keeps click tracking + slug resolution but routes to / instead of /waitlist. Slug CRUD, stats reads, and admin handle tools untouched — no reward path remains
  • Admin + client cleanup — WaitlistTools deleted (was already unwired); peopleService/PeopleAdmin/people route lose the waitlisted status and waitlist merging; useAutoLogin onNotWhitelisted renamed onAccessDenied; validate { banned: true } now surfaces home.errors.accessRevoked (new key, 8 locales) via the home gateError; streaming overlays and Build Games copy moved to open-access phrasing; orphaned joinWaitlist/joinWaitlistDesc/failedToValidate message keys removed
  • components/ui/VersionBadge.tsx — APP_VERSION bumped to v0.9.0

v0.8.26

June 9, 2026

  • lib/services/accessGateSql.ts (new) — single shared access-gate predicate: ACCESS_GATE_SQL runs three EXISTS subqueries (ban_entries, active+unexpired whitelist_entries, portfolio_addresses with status IN (VERIFIED, PRIMARY)) all keyed on $1, and allowedFromGateRow(row) = !banned && (whitelisted || verified_claim). No prisma/neon imports so the Node and Edge runtimes consume it verbatim and cannot diverge. SELF_REPORTED never admits; ban takes precedence
  • lib/services/userService.ts — getProfileByVerifiedClaim(address) resolves the profile holding a VERIFIED/PRIMARY portfolio claim (earliest verifiedAt on the legacy multi-row case, logged), and hasVerifiedClaim(address) is the boolean existence check. Distinct from findClaimByAddress, which has no status filter
  • lib/services/whitelistService.ts — new hasAccess(identifier) runs ACCESS_GATE_SQL via $queryRawUnsafe (parameterised) and returns allowedFromGateRow. isWhitelisted/getWhitelistStatus left unchanged for the waitlist/admin/referral consumers that need true whitelist-table semantics. lib/services/whitelistService.edge.ts — new checkAccessEdge(identifier) runs the same SQL over a fresh per-call neon() client (no cache, so it cannot drift from the live DB or the Node gate)
  • app/api/me/route.ts gate swapped isWhitelisted → hasAccess; proxy.ts swapped both Edge enforcement points (API Bearer + page cookie) checkWhitelistEdge → checkAccessEdge. A wallet that earned a VERIFIED/PRIMARY claim in a prior session now passes all gates; a banned wallet stays blocked even with a verified claim. A parity test pins Node hasAccess and Edge checkAccessEdge to identical decisions across the full 8-row matrix
  • app/api/access/validate/route.ts — after the ban check and the permanent/trial branches, validate now admits a wallet with a verified claim (issues a session, same shape as the permanent branch) before the expired branch and the waitlist join, covering both the expired-trial and never-whitelisted paths. Ban (step 0b) still precedes this; email identifiers never match the claim check
  • lib/services/userService.ts upsertUserProfileWithRecovery — recovery resolution now falls back to getProfileByVerifiedClaim when no Wallet row matches, so a verified wallet recovers to its anchor instead of spawning a ghost (SELF_REPORTED never resolves). The recover branch routes through upsertUserProfile to backfill the login Wallet row, converging the claim and Wallet lookup paths. WI-1 merge-direction behavior is preserved exactly. Closes #125
  • components/ui/VersionBadge.tsx — APP_VERSION bumped to v0.8.26

v0.8.25

May 30, 2026

  • components/nft-detail/NFTDetailPositionCard.tsx — duplicate-serial acquisition rows now sort chronologically (earliest purchaseDate first, undefined sinks to the end) and label each row "Nth copy" via a new ordinalLabel helper instead of "#N" — reads more naturally to non-crypto-native players. New formatDateTime helper renders date + 12-hour time underneath the venue chip so two rows acquired on the same calendar day are visually distinct without needing to surface tokenId or txHash. Row layout reflowed from inline (label / venue / date / View) to two-line stack (label + venue on top, date+time below, View right-aligned at baseline) for better width behavior on narrow viewports. Tooltip copy switched from JSX-expression-mixed text to explicit string literals to avoid the JSX whitespace-collapse rule that was rendering "2distinct tokens" without a space — also reworded "tokens" to "copies" so the language matches the row labels
  • prisma/seed.ts — added ranked-s2 release ({ slug: ranked-s2, name: Ranked Season 2, shortName: Ranked S2, category: ranked }) parallel to the existing ranked-s1, and a single contains match rule { type: contains, pattern: season 02, releaseSlug: ranked-s2, priority: 50 } that catches every rank tier (Legend / Icon / Celebrity / Lead Actor / Star / Cameo / Extra) plus any weapon variants minted with "Season 02" in the name. Future-proofs the mapping without enumerating individual item names. NOTE: takes effect in prod only after running `npx prisma db seed` or adding the entries via Admin → Item Origins
  • components/ui/VersionBadge.tsx — APP_VERSION bumped to v0.8.25

v0.8.24

May 30, 2026

  • components/NFTDetailModal.tsx — added allSameSerial memo (sortedItems.length >= 2 && unique mintNumber set size === 1) to detect grouped tokens whose metadata Serial Number trait collides across copies. Common pattern for Off The Grid profile customization / cosmetic NFTs where every minted token reports the same Serial Number, which made the existing 2-up thumbnail grid render two identical #N badges and confused players who thought they were seeing the same NFT twice. When allSameSerial fires we drop the grid in favor of the single-hero image path and overlay a rarity-tinted ×N quantity chip in the top-right corner (top-2 right-2, primary color text + 18% bg + 60% border, font-mono text-[11px] tabular-nums). Modal also threads walletAddress and duplicateSerialItems={allSameSerial ? sortedItems : undefined} into NFTDetailPositionCard
  • components/nft-detail/NFTDetailPositionCard.tsx — new walletAddress + duplicateSerialItems props. When duplicateSerialItems is set, a useMemo builds duplicateSerialRows by iterating each token and calling getCachedNFT(walletAddress, tokenId) to read acquisitionVenue, purchaseDate, acquisitionTxHash from the nft-detail localStorage cache (already populated by useNFTEnrichmentOrchestrator including secondary-token writes in the grouped-NFT enrichment path), falling back to nft.perTokenAcquisitions for the price/date subset. The Acquisition section header gains a "· N COPIES" suffix and an InfoTooltip explaining "Profile customization items share a single serial number in their on-chain metadata. You own N distinct tokens — each row below is a separate acquisition with its own transaction." The body switches via ternary: instead of the single Source/Acquired/Transaction/Gas rows, it renders one row per token with #N position pill (font-mono w-6) + venue chip (colored same as single-row variant) + date + View tx link. TokenIds intentionally not surfaced — the tx link is the escape hatch for anyone needing on-chain detail. Standard single-row layout preserved for every other case
  • components/ui/VersionBadge.tsx — APP_VERSION bumped to v0.8.24

v0.8.23

May 27, 2026

  • components/nft-detail/SupplyCard.tsx — the inline "?" tooltip next to the "On-Chain Supply" header on item detail rendered as an absolutely-positioned span inside the card body. The parent card uses both overflow-hidden and a clip-path corner cut, so the popup got clipped at the top edge of the card and was effectively invisible on hover. Rewrote SupplyTooltip to portal the popup into document.body with fixed positioning computed from the button’s getBoundingClientRect(); button styling, copy, and clip-path on the popup itself are unchanged. The popup now escapes the parent’s clip rect and renders above the card as intended
  • components/ui/VersionBadge.tsx — APP_VERSION bumped to v0.8.23

v0.8.22

May 27, 2026

  • app/[locale]/intelligence/compare/ComparePage.tsx — surfaced item.rarity (quality tier) via RarityBadge on the compare page so the search dropdown can disambiguate same-named items. The search returns multiple GameItem rows when the same itemName exists at different quality tiers (the table is unique on contractAddress + itemName + rarity), but the UI was only rendering supplyBracket via ItemBadge — which collapses to "Common" for everything 200+ supply. The disambiguating field was in the payload but never displayed, so four legitimately-different rows looked identical. Search row now reorders to RarityBadge → itemName → activeSupply count and drops the supply-bracket pill (the count already conveys supply scarcity, and showing both pills produced two literal "COMMON" labels with two different meanings on the same row — exactly the conflation CLAUDE.md learned rules warn against). PickedChip and ItemHeadCard keep both pills since the supply-bracket signal is more useful once you have committed to an item.
  • components/ui/VersionBadge.tsx — APP_VERSION bumped to v0.8.22

v0.8.21

May 7, 2026

  • components/account/admin/UsersTools.tsx — admin UI for the per-user feature flag system shipped in 412471e. Added a FLAGS column to the registered-users table with a toggleable INTEL pill (intelligence-early). Click flips the flag via PATCH /api/admin-proxy/users with the full next-state featureFlags array; pending state is keyed `${userId}:${flag}` so per-row toggles disable independently while the request is in flight. KNOWN_FEATURE_FLAGS is the in-file vocabulary — extend it to add more pills. Grid template-cols updated from 8 columns to 9 to fit the new 90px FLAGS cell
  • components/ui/VersionBadge.tsx — APP_VERSION bumped to v0.8.21

v0.8.20

May 5, 2026

  • lib/pricing/resolveHistoricalGunPrice.ts — same-day cost-basis stability. The function used to fall through to CoinGeckoService.getGunTokenPrice() on every same-day resolve (since the historical endpoint has no row for today yet), which meant re-enrichment intraday could shift purchasePriceUsd around with each request as GUN moved. Added a tryCache(date) check before the spot fetch — if a previously resolved same-day price exists with confidence !== "estimated", reuse it. First resolve still hits the live spot price, subsequent same-day resolves now return the cached value instead of drifting
  • components/nft-detail/SupplyCard.tsx — relabeled "Supply Intelligence" to "On-Chain Supply" with an inline ? tooltip explaining the data source caveat (tracks on-chain mints from HEX decodes and marketplace activity; in-game-earned-but-not-yet-decoded items are not included; actual total supply may be higher). Bracket descriptions, "in circulation" copy, "First/Last Minted" labels, and the serial-number line all updated to "on-chain" framing for consistency. SupplyTooltip is a local component with hover + click toggle, clipped-corner styling, fixed 208px width
  • app/[locale]/account/page.tsx — dropped overflow-hidden on the Language section card so the locale dropdown can render outside the card boundary instead of being clipped
  • components/ui/VersionBadge.tsx — APP_VERSION bumped to v0.8.20

v0.8.19

May 5, 2026

  • components/charts/AcquisitionTimeline.tsx — provenance trail geometry rewrite. Replaced the visx LinePath with curveBasis through 3 points (start, midpoint, end) with a hand-rolled SVG path: M start L (cx + dx*0.3, cy) C (cx + dx*0.5, cy), (cx + dx*0.5, originY), (cx + dx*0.7, originY) L origin. Control points sit at 50% X on each end of the curve segment, forcing horizontal tangents at both joins so the straight segments meet the cubic Bezier without visible kinks. Removed @visx/curve and LinePath imports
  • components/ui/VersionBadge.tsx — APP_VERSION bumped to v0.8.19

v0.8.17

Apr 23, 2026

  • /intelligence route scaffold — server component (metadata) + client component (logic) under app/[locale]/intelligence/. IntelligencePage.tsx reads ?address= from useSearchParams for the Your Collection overlay, renders PageHeader + HeroPulse + StatStrip + ActivityChart and SectionPlaceholder stubs for RareMoving / TopMovers / ScarcityComposition / ExploreDeeper / NotableDrops / ReleaseTimeline / IntelligenceFooter. Placeholders resolve in Commits 7‑9 of the intelligence buildout
  • /scarcity retired — middleware.ts emits a 301 redirect from /scarcity and any /{locale}/scarcity to the equivalent /intelligence path, preserving the full query string (?address=, sort, tab, etc). ?tab=opensea redirects to /market instead and strips the tab param. /scarcity removed from PROTECTED_PAGES so the redirect runs before the auth check. app/[locale]/scarcity/page.tsx + layout.tsx deleted; utils.ts and components/* retained because subsequent intelligence components re‑import them. app/[locale]/market/page.tsx "Scarcity Data" link retargeted to /intelligence
  • 5 intelligence API routes under app/api/intelligence/: /pulse (15s cache — mintsToday from live SupplyEvent, 7d/30d/all‑time from DailyItemMintCount rollup, itemsTracked / totalEvents / totalReleases aggregates, todayVs30dAvgPct delta), /activity (60s cache, range=30d|7d|all, aggregates daily mint+burn series from rollup, derives release annotations from DropEvent where newItemCount ≥ 50 capped at 10), /rare-moving (60s, ultra‑rare/rare/uncommon items with 7d activity, contextSnippet joined against DropEvent.itemNames), /movers (60s, top‑N items by 7d mint volume within a supply bracket, 7‑day sparkline array per item), /scarcity (60s, bracket composition + 7‑bucket granular distribution using the indexer’s denormalized activeMints column). All routes use prisma.$queryRaw for aggregations Prisma’s builder can’t express cleanly. None gate on auth — global metrics only, public per design intent
  • lib/intelligence/ helpers — format.ts (formatMints with <1M exact + ≥1M condensed, formatSupply always‑exact, formatRelativeDate, formatDurationSeconds, formatPercent + formatSignedPercent, anonymizeWallet first4+8×*+last4, formatShortDate, formatIsoDate), urls.ts (itemUrl/releaseUrl/compareUrl/intelligenceUrl canonical builders), rarity.ts (SupplyBracket vocabulary with bracketColor/bracketLabel helpers, ultra‑rare=coral, rare=purple, uncommon=lime, common=gray‑2), shape-detect.ts (CurvePoint/CurvePattern/ShapeResult types exported + detectShape/narrateShape throwing stubs; full implementation lands in the item detail page commit)
  • components/intelligence/ primitives — ItemBadge (clipped‑corner rarity pill, color from lib/intelligence/rarity), ScarcityBar (horizontal stacked bar proportional to item counts across brackets, supports optional labels below), Sparkline (inline SVG polyline with all‑zero and single‑point edge cases handled)
  • app/[locale]/intelligence/components/PageHeader.tsx — "N releases · N items · N events" bar pulled from /api/intelligence/pulse. HeroPulse.tsx renders the 48px mintsToday number with a delta pill against 30d avg, a lime live‑indicator dot, and a "Live / updated Ns ago" meta line that ticks client‑side via setInterval each second. StatStrip.tsx shows 7d / 30d / all‑time burns / items tracked in four columns with vertical dividers, collapses to 2×2 on mobile. All three share the pulse fetch via SWR dedup (single network request)
  • app/[locale]/intelligence/components/hooks/useIntelligenceFetch.ts — thin SWR wrapper with a default 60s refreshInterval, revalidateOnFocus off, dedupingInterval clamped to min(refreshInterval, 15s); returns { data, error, isLoading, refresh, updatedAt } with the response body’s updatedAt hoisted for the "last updated" meta lines
  • app/[locale]/intelligence/components/ActivityChart.tsx — visx‑based dual‑axis daily mint/burn chart (200px tall, full container width via @visx/responsive ParentSize). Left axis lime line = mints, right axis dashed coral = burns. Burns render only when the user toggles the overlay checkbox. Range pills (30d / 7d / all‑time) refetch /api/intelligence/activity?range=… with SWR keyed on the URL. Peak annotation labels the max mint day. DropEvent annotations render as lime‑ringed dots at the chart baseline with title tooltips; clicking routes to /intelligence/release/[id]. Hover state shows a vertical guide line, filled data point marker, and a foreignObject tooltip in the top corner. Reuses chartTheme from components/charts/theme.ts
  • app/globals.css — added --gs-coral (#FF6B4A) token plus --gs-coral-dim and --gs-coral-glow, wired through @theme inline as --color-gs-coral for Tailwind utilities. Used for ultra‑rare bracket, burns overlay, ITEM B in compare mode (Commit 12), and destructive accents
  • app/api/intelligence/pulse/route.ts — added totalEvents (all‑time rollup mints + all‑time rollup burns) and totalReleases (prisma.dropEvent.count()) to the response so PageHeader, HeroPulse, and StatStrip all source from the same endpoint without additional fetches. 7 parallel queries via Promise.all; warm cache response under 50ms
  • app/[locale]/intelligence/components/SectionPlaceholder.tsx — shared stub component that renders a clipped‑corner box with "[SectionName]" text at a configurable height. Commit 7‑9 components replace individual placeholders incrementally; seams are intentional so partial progress is reviewable

v0.8.16

Apr 20, 2026

  • indexer/src/aggregation/drops.ts — full rewrite. Replaces the stateful sliding‑window detector (recentNewItems[] + knownItemNames Set + Date.now()‑vs‑block‑timestamp pruneWindow) with a stateless DB‑backed query against game_items.firstMintAt. evaluateDrop early‑exits when isNewItem === false; otherwise runs detectClusterAtBlockTime which range‑scans game_items WHERE firstMintAt BETWEEN (anchor − 30min) AND anchor AND totalMints > 0. Threshold remains 3. Idempotency via DropEvent overlap query (where isOngoing AND detectedAt in window AND itemNames hasSome [...]). closeExpiredDrops exported for periodic invocation — not yet wired to a cron route
  • indexer/prisma/schema.prisma — added @@index([firstMintAt]) on GameItem; coexists with rescue‑branch @@index([tokenId]) on SupplyEvent. Matching index added to authoritative prisma/schema.prisma. Migration prisma/migrations/20260419000000_add_first_mint_at_index/migration.sql ships the CREATE INDEX statement
  • indexer/scripts/backfill-drop-events.ts — new. Reconstructs drop_events from historical game_items.firstMintAt by calling evaluateDrop for each first‑mint in ascending block‑time order, then calls closeExpiredDrops with the most‑recent block timestamp as anchor. --dry-run default; --apply with optional --i-understand-this-wipes-existing-drops for safe re‑runs. Production apply created 941 DropEvents from 3344 first‑mints; spans 2025‑03‑14 genesis through 2026‑03‑23. Row count is higher than the session‑collapsed estimate because the detector’s ongoing‑drop overlap query filters by detectedAt within the trailing 30‑minute window, so multi‑hour sustained drops (genesis catalog mint, 2025‑12‑23 Hexmas) fragment into multiple consecutive rows as the original drop’s detectedAt rolls out of the window. Per‑day biggest examples: Hexmas day biggest=79 items, Feral Beast biggest=25, Le BoBo biggest=42
  • indexer/src/ingestion/listener.ts — wired evaluateDrop into resolveAndLinkEvent’s mint branch. Captures the post‑increment db.gameItem.update return value; on totalMints === 1 (true first mint), if firstMintAt is null, runs an inline update to set firstMintAt + firstMintBlock to the current event’s timestamp/block before invoking evaluateDrop. Wrapped in try/catch so detector failures are non‑fatal to ingestion. Phase B rebuildAggregates would eventually re‑derive firstMintAt from MIN(supply_events.timestamp), but the detector needs the value at evaluation time
  • rescue commit 7daa303 — captured ~2,000 lines of Phase A/B indexer architecture that had been authored directly on the VPS and never pushed. Includes batched normalizer.ts (createMany 2000‑event batches with estimated‑timestamp extrapolation, no inline metadata or drop calls), resolve-pass.ts (GunzScan‑paginated metadata pass with adaptive rate‑limit handling and aggregate rebuild), gapfill.ts/gapfill-early.ts/resolve-early.ts/backfill-unlinked.ts operational utilities, and the Phase A → background listener → Phase B choreography in index.ts. Captured as a single rescue commit; reconciled with the drop detector via PR #30

v0.8.15

Apr 19, 2026

  • indexer/scripts/merge‑game‑item‑duplicate.ts — new parameterized migration: takes --canonical and --stale id flags; validates same contractAddress, rarity, and TRIM(itemName); reassigns every referencing SupplyEvent (gameItemId and burnGameItemId) onto canonical; sums totalMints/totalBurns/activeMints; merges firstMintAt/lastMintAt ranges and firstMintBlock/lastMintBlock ranges; ORs isActivelyMinting; ANDs isNew; recomputes supplyBracket from merged activeMints (thresholds inlined from CONFIG.BRACKETS: 10/50/200); preserves canonical’s other fields with stale fallback on null; deletes the stale row; single Prisma interactive transaction with 60s timeout; idempotent (re‑run with already‑merged ids exits 0)
  • Resolved Kestrel Templar Epic collision from v0.8.14 skip list — merged stale cmmv8twr (' Kestrel Templar', 2527 mints) into canonical cmmv905v ('Kestrel Templar', 1197 mints); 2527 SupplyEvents reassigned; canonical row now activeMints=3724, supplyBracket=common, firstMintAt=2025‑03‑14, lastMintAt=2026‑04‑06, isActivelyMinting=true, category='Sniper Rifle' (canonical normalized form won over stale 'SniperRifle'); 0 byte‑exact and 0 trim‑equivalent collisions remain in game_items

v0.8.14

Apr 19, 2026

  • Indexer itemName trim — classifyOTGItem in indexer/src/resolution/classifier.ts now applies .trim() to metadata.name before returning; on‑chain metadata occasionally shipped names with leading or trailing whitespace which the indexer passed through unmodified; all downstream writes (upsertMint, upsertBurn, drop detector, secondary cache) get the normalized value automatically
  • scripts/backfill‑trim‑item‑names.ts — one‑time migration normalized 57 of 58 whitespace game_items rows and 5 of 5 whitespace item_origin_items rows; raw SQL because the indexer Prisma schema does not declare ItemOriginItem; batches of 50 for the Prisma 7 transaction timeout cap; idempotent with --dry‑run default; 1 collision ( Kestrel Templar Epic vs Kestrel Templar Epic) logged and skipped for manual merge
  • BASE_LIMB_NAMES PHOSPHOR FURY correction — dropped trailing space on 'PHOSPHOR FURY' entry in lib/intelligence/baseItems.ts so the case‑sensitive Set lookup in isBaseLimb() continues to match the 4 post‑backfill rarity rows (166K combined active mints); without this change the Tranche B2 /scarcity base‑limb filter would have regressed the moment the backfill landed

v0.8.13

Apr 19, 2026

  • Scarcity rankings zero‑guard — OnChainSupplyTab filters out activeMints === 0 rows from the rankings table to match the zero‑guard already shipped on SupplyBadge and SupplyCard (commits 1320ddd, 187ff27); API route now also supports excludeZero query param (default true) so data.total and data.brackets reflect the filter
  • Base‑limb exclusion on /scarcity — /api/supply/rankings accepts excludeBase param (default true); imports BASE_LIMB_NAMES from lib/intelligence/baseItems (Phase 0 allowlist) and filters via Prisma where: { itemName: { notIn: [...BASE_LIMB_NAMES] } } on items, total, and groupBy; UI adds a "Show base limbs" toggle near the bracket‑filter chip with offset‑reset useEffect; default view drops 94 base‑limb rows (93 common, 1 rare) and ultra‑rare bracket falls from 10 inflated‑by‑coverage‑gaps to 3 genuine
  • Rarity vs Bracket disambiguation — both desktop column headers now render an InfoTooltip (components/ui/InfoTooltip) icon with copy clarifying that Rarity is in‑game cosmetic quality (Common/Uncommon/Rare/Epic, unrelated to mint count) and Bracket is mint‑count scarcity (Ultra‑Rare ≤ 10, Rare 11‑50, Uncommon 51‑200, Common 200+); mobile card view unchanged since it has no column headers

v0.8.12

Apr 17, 2026

  • item_origin_items casing fix — three write sites (applyMatchRules, itemOriginService.createItem, prisma/seed.ts) were lowercasing itemName on write while game_items stores canonical on‑chain casing; case‑sensitive JOINs between the two tables returned 0 rows; fix preserves canonical casing on every write path and switches itemOriginService.deleteItem to a case‑insensitive findFirst + delete‑by‑id so removals still work across casings
  • ItemOriginsContext.buildIndex — Map keys now lowercase on insert so getItemOrigin's existing .toLowerCase() read path keeps working regardless of DB casing; without this change the backfill would have caused every release chip and origin label to go blank against v0.8.11's build
  • scripts/backfill‑item‑origin‑casing.ts — one‑time data migration, idempotent, --dry‑run default; applies updates in Prisma batches of 50 since Prisma 7 BatchTransactionOptions has no timeout; production run aligned 594 of 621 rows to canonical casing (27 orphans with no GameItem match remain lowercase pending curation); case‑sensitive JOIN gi.itemName = ioi.itemName now resolves 594 names instead of 0

v0.8.11

Apr 17, 2026

  • Middleware cron bypass — added pathname.startsWith('/api/cron/') short‑circuit in middleware.ts API block; cron routes now reach their own verifyCronAuth() guard instead of being rejected with 401 by the Dynamic JWT verifier; matches existing pattern for /api/admin‑proxy/*, /api/admin/*, and /api/access/validate
  • Match rule application pipeline — new lib/intelligence/applyMatchRules.ts walks GameItem rows without a matching ItemOriginItem, evaluates ItemOriginMatchRule entries in priority order (prefix then contains), creates an ItemOriginItem per first‑match; idempotent via @@unique([itemName, quality])
  • scripts/apply‑match‑rules.ts — CLI wrapper with --dry‑run flag; loads .env.local for standalone runs; first production run mapped 172 distinct names across 19 of 30 seeded rules (halloween 72, hexmas 48, retro 14, legacy 7, black‑friday 5, enforcer 4, others)
  • Daily cron schedule — /api/cron/apply‑match‑rules route wraps applyMatchRules() with verifyCronAuth; vercel.json schedule "0 3 * * *" keeps ItemOriginItem rows in sync as new GameItem rows land
  • lib/intelligence/baseItems.ts — 25‑name curated allowlist of OTG base prosthetic limbs (18 arms + 7 legs); verified against GameItem table with exact DB casings preserved (Starter Arm, PainKilla, 'PHOSPHOR FURY ' with trailing space); BASE_LIMB_SET + isBaseLimb() helper for Phase 1 scarcity filtering
  • drop_events investigation — docs/DROP_EVENTS_INVESTIGATION.md documents why the indexer's DropEvent table is empty despite active mint activity; root cause: pruneWindow() in indexer/src/aggregation/drops.ts compares block timestamps against Date.now() instead of the latest entry timestamp, so every push is immediately pruned during backfill and catch‑up; two 5‑item qualifying clusters (2026‑02‑10 and 2026‑03‑23) were missed
  • Intelligence page coverage audit — docs/INTELLIGENCE_COVERAGE_FINDINGS.md quantifies pre‑Phase‑0 gap: 15.1% item coverage, 9.3% supply coverage; post‑Phase‑0: 94 base limbs tagged (2.8M supply separated), 760 non‑base items mapped (21.6%), 2761 orphans remaining

v0.8.10

Apr 8, 2026

  • Smart post‑login routing — HomeClient defers the social‑login redirect until UserProfileContext loads; returning Discord users with linked game wallets now route to /intelligence?address=<gameWallet> instead of being dumped on /account; non‑social wallet logins keep the existing direct‑to‑/intelligence path
  • gs_just_logged_in sessionStorage flag — set by HomeClient on both fresh‑wallet and already‑connected paths before pushing; consumed exactly once by the /account post‑login guard so intentional Profile‑nav clicks never trigger an auto‑redirect
  • /account post‑login guard — new useEffect in AccountContent reads the flag, finds the first non‑embedded portfolio address, and router.replace()s to /intelligence when present; postLoginRedirectRef ensures it fires at most once per page load
  • Focused onboarding view — new showFocusedOnboarding = isEmbeddedOnly && !hasGameWalletForTabs branch in /account; hides the full tab bar (identity/wallets/sharing/settings) and renders a centered welcome heading + GameWalletOnboarding card; welcomeName falls back through socialConnections.username → displayName → "player"
  • Auto‑revealing tabs — once the focused user pastes a game wallet, addPortfolioAddress + refreshProfile flips hasGameWalletForTabs, showFocusedOnboarding becomes false, and the full tabbed account UI appears on next render with no manual navigation
  • GameWalletOnboarding secondary actions — added two escape hatches under the paste‑address input: "Connect Wallet" calls useDynamicModals().setShowLinkNewWalletModal(true) for users who already have MetaMask, "Browse the Explorer" links to /explore for users who want to look around first

v0.8.8

Apr 5, 2026

  • Discord social login — Dynamic Labs OAuth integration; users sign in with Discord and receive an auto‑provisioned embedded wallet; social‑login users route to /account instead of /intelligence
  • Social connections API — /api/social/link, /api/social/unlink, /api/social/resolve, /api/social/connections routes for linking Discord (and future OAuth providers) to user profiles; SocialConnection Prisma model with unique provider+providerId constraint
  • Auto‑whitelist for social logins — /api/access/validate detects Bearer token with OAuth credentials and auto‑whitelists the embedded wallet address; eliminates manual whitelist step for Discord users
  • Embedded wallet retry — useAutoLogin retries up to 5s (10 × 500ms) while Dynamic Labs provisions the embedded wallet after OAuth completion; prevents race condition where wallet is null during provisioning
  • Social login routing — HomeClient detects OAuth credentials and routes social‑login users to /account (no game wallet yet) instead of /intelligence; skips email‑only validation flow for social users
  • SocialConnections component — account page shows linked social accounts with connect/disconnect UI; useSocialResolver hook for resolving wallet addresses to social identities
  • Game wallet onboarding banner — prompts social‑login users to link their GunzChain game wallet for full portfolio access
  • Privacy policy update — added Data Protection Contact section (privacy@gunzscope.xyz); linked contact email in Your Rights section

v0.8.7

Mar 29, 2026

  • i18n framework — next‑intl integrated with App Router; locale‑aware routing via app/[locale]/ structure; middleware composed with existing auth middleware; next/link and next/navigation swapped to locale‑aware wrappers across 24+ files
  • French translations — fr.json with full coverage: Navbar, PublicNav, StaticPageNav, Footer, homepage (hero, features, social proof, dashboard preview, error states); LanguageSwitcher component in navbar with flag icons
  • Homepage stats bar overhaul — replaced GUN Price with On‑Chain Events count and Platforms Unified metric; platform icons replaced with official Simple Icons brand SVGs (Avalanche, Solana, OpenSea); top/bottom bar stat positions swapped
  • Mint position query — NFT detail modal shows "Mint #X of Y" via GunzScan token instance API; position derived from mint event ordering
  • Per‑item normalized enrichment cache — write‑through cache layer keyed by contract+tokenId; enriched data persists independently of gallery‑level cache; reduces redundant re‑enrichment on revisits
  • Admin auth unification — 7 admin API routes (whitelist, waitlist, users, shares, revenue, referrals, item‑origins) consolidated to shared admin auth pattern; removed hardcoded admin wallet from shares route
  • Snapshot field fix — enrichment snapshot now captures purchasePriceUsd, purchasePriceUsdEstimated, transferDate, gunPriceAtTransfer fields that were previously dropped during snapshot serialization
  • Visual polish — film grain noise overlay texture; supply count badges gated behind authentication; rich text heart icon fix for i18n paired tags
  • Per‑item cache reads — loadEnrichedItems() fetches individual EnrichedItem rows from server; seedLocalCacheFromEnrichedItems() pre‑populates localStorage from per‑item rows instead of monolithic blob; orchestrator reads per‑item first, falls back to blob
  • Supply card rarity fix — SupplyCard now reads rarity from activeItem (token‑level) instead of group‑level nft.traits which never updated on token switch in mixed‑rarity groups
  • Modal classification fix — NFTDetailModal now uses getSpecificItemType() instead of raw OpenSea Class trait; Meatport items correctly show "Combat DJ" class and "Meatport" origin badge via new event release + prefix match rule

v0.8.6

Mar 24, 2026

  • GUNZscopeAchievements.sol — soulbound ERC‑721 contract (UUPS upgradeable) deployed to Avalanche Fuji testnet; proxy at 0x779d1bc9769944821c135D460944C31D134bc201
  • Cross‑contract verification — claim() reads PortfolioAttestationV2 view functions (getAttestationCount, getLatestAttestation, getAttestation, hasRegisteredHandle) via IPortfolioAttestation interface; 4 verification types: TIME, VALUE, ACTIVITY, COLLECTION
  • Soulbound enforcement — _update() override blocks all transfers except mint/burn; ERC721URIStorageUpgradeable for per‑token metadata URIs
  • 8 launch achievements registered: Pioneer (1+ attestation), Handle Holder (gsHandle), Rising Star (10K GUN), Whale (100K GUN), Collector (10+ items), Hoarder (50+ items), Diamond Hands (90d/3 att), Veteran (180d/6 att)
  • claimBatch(uint256[]) — multi‑achievement claim in single tx; isEligible() view function for gas‑free pre‑check
  • deploy‑achievements.ts — Ledger‑based deployment script using custom LedgerSigner (same pattern as upgrade‑mainnet‑ledger.ts); 10 Ledger confirmations (2 deploy + 8 achievement registrations)
  • 35 Hardhat tests covering all 4 achievement types, soulbound transfer blocking, batch claims, double‑claim prevention, edge cases
  • Participation economy design doc — 4‑layer architecture: identity (deployed) → SBT milestones (testnet) → dust/participation score (planned) → transferable token (future); inspired by Midnight night/dust model
  • Roadmap pages updated — /build‑games reputation card moved from "next" to "live"; Phase 1 items include SBT contract; /roadmap item 04 reflects actual implementation

v0.8.5

Mar 21, 2026

  • Gaming Asset Intelligence Indexer v2 — standalone Node.js process on DigitalOcean VPS (PM2‑managed); two‑phase architecture: Phase A bulk‑scrapes ERC‑721 Transfer events via eth_getLogs, Phase B resolves metadata via GunzScan pagination
  • Phase A: deferred metadata normalizer — createMany with skipDuplicates, estimated timestamps from reference block + 2s block time, adaptive chunk splitting for dense ranges (>20K events); 27.69M events captured across 16.5M blocks in ~5 hours
  • Phase B: GunzScan instance pagination — fetches /api/v2/tokens/{contract}/instances (50/page, 600ms delay), classifies via classifyOTGItem, batch‑links SupplyEvents to GameItems using WHERE tokenId IN (...) bulk updates; resumable cursor in indexer_cursors table
  • Parallel architecture — WebSocket listener (ethers.js WebSocketProvider) starts before Phase B; real‑time mint/burn capture runs concurrently with background metadata resolution; listener auto‑backfills gaps on reconnect
  • Self‑healing Phase B — runPhaseB() wraps resolvePass() in retry loop (100 max, exponential backoff 1‑5 min); GunzScan 429s trigger 30s backoff with 5 retries per page; process never crashes, just waits and resumes
  • Periodic aggregate rebuilds — every 200 pages (~10K tokens), rebuildAggregates() recomputes totalMints/totalBurns/activeMints/supplyBracket for all GameItems from linked SupplyEvent counts
  • Gap‑fill script (gapfill.ts) — standalone PM2 process re‑scanning blocks 1.47M–8.77M to recover events lost during Neon Postgres OOM period; createMany + skipDuplicates for idempotent fills
  • Neon Postgres scaling — autoscaling 1‑2 CU (was 0.25 CU); eliminated Postgres OOM errors on bulk inserts; DB_BATCH_SIZE tuned from 100→‑2000 rows per INSERT
  • Database index — idx_supply_event_token_id on (tokenId, contractAddress) for fast Phase B updateMany lookups across 27M+ rows
  • VPS infrastructure — 1GB swap added as OOM safety net; Node.js heap capped at 1024MB; two concurrent PM2 processes (gs‑indexer + gs‑gapfill) stable at ~400MB combined
  • ValueHeader layout — 7d change badge moved inline with market value (same row); new position % line below cost basis showing distance from cost basis with directional arrow and profit/loss coloring
  • Homepage stats bar — replaced hardcoded/low‑impact metrics (NFTs Tracked: 70, Blocks Scanned: 16M+, Tracking: 24/7) with live indexer data; hero bar now shows On‑Chain Events (28M+), Items Indexed (3,500+), Blocks Indexed (16.7M+) with count‑up animations; /api/stats/site extended with indexerCursor + gameItem.count() via parallel Promise.all; social proof section pulls live event count with static fallback

v0.8.4

Mar 22, 2026

  • calcPortfolio cost basis fix — was using currentGunPrice for cost basis calculation; now uses purchasePriceUsd (historical price at acquisition) with currentGunPrice × costGun as fallback only when no historical data exists
  • NFT summary cards rework — NFTHoldingsCard Face A: count + venue breakdown (decoded/traded/transferred/received); Face B: profit/underwater count, cost vs market value, liquidity reality check (listed/with sales/no data). GunSpentCard Face A: estimated market value + pricing source breakdown; Face B: ROI % with cost in vs value now
  • Transfer reclassification fix — removed mutually exclusive marketplacePriceGun guard from isGenuineTransfer (required undefined while resolvedVenue required defined); transferred items now resolve to sender’s original venue, purchase price, and date instead of being classified as purchases
  • transferDate field — new field on NFT type and cache; computed from transfer event timestamp; propagated through enrichment pipeline and cache write (purchasePriceUsdEstimated + transferDate added to modal pipeline cache merge)
  • Provenance trail on AcquisitionTimeline — ghost dot (r=3, 30% opacity) at original purchase date, dashed gray connector, main cyan dot at transferDate; dateExtent and lockPoints updated to include transferDate; tooltip shows "Bought Mar 5 → Transferred Mar 12"
  • GunBalanceCard layout — Face A: balance + USD value; Face B: current GUN price + LiquidityIndicator with position‑impact messaging; plain‑language depth explanations for non‑traders
  • PortfolioSummaryBar — passes market value (displayTotal) as totalValue to ValueHeader so cost basis subtitle shows correct % above/below
  • Enrichment venue resolution — when tracing transfer back to sender’s original purchase, store sender’s venue (e.g., opensea) instead of transfer
  • Sparkline color fix — use scalar cost basis for sparkline profit/loss coloring when per‑point historical data unavailable
  • Modal pipeline cache merge — cache write now preserves orchestrator fields (marketExitGun, comparableSalesMedian, etc.) instead of overwriting with pipeline‑only subset
  • Cache schema v29 — force transferDate re‑enrichment for all cached items
  • Attestation explorer — WALLETS column showing registered wallet count per attested user
  • Portfolio layout — removed wallet search bar from authenticated portfolio view; search remains on view‑only/explore flow

v0.8.3

Mar 21, 2026

  • Sparkline loading strategy — server snapshots are now primary sparkline source; localStorage demoted to offline fallback; skeleton shimmer during 800ms server fetch; GUN price sparkline as timeout fallback; one‑time localStorage cleanup of stale bootstrap data
  • Modal cache‑first read — reset effect pre‑populates itemPurchaseData and resolvedAcquisitions from localStorage cache synchronously, eliminating empty→cached data flash on modal open; resolvedAcquisitionsRef updated atomically
  • Enrichment retry — retry delay 2s→5s, max 2 retries (3 total passes), per‑attempt diagnostic logging; currentRetryNfts re‑filtered between attempts so only unresolved NFTs retry
  • Cache housekeeping — useHandleResolver 5‑min TTL + 500 max size; useSupplyData Map cleanup on >200 entries; priceHistory server Map 1000‑entry guard; waitlist gs_waitlist_id cleanup in useAutoLogin
  • GUN price sparkline tooltip — 4 decimal places for sub‑dollar values, "GUN" prefix in GUN price mode; redundant GUN% second line hidden when primary line IS the GUN price
  • Auth gating — AuthGate component (blur/replace/inline variants) with "VIEW ONLY · CONNECT WALLET TO UNLOCK" CTA; gates cost basis, P&L, GUN spent, insights, charts, acquisition details, position card, supply badges, liquidity indicator
  • Server‑side redaction — lib/server/redactCostBasis.ts strips 20+ cost/acquisition/P&L fields from API responses; lib/server/softAuth.ts provides non‑rejecting Bearer auth check; applied to /api/portfolio/snapshots, /api/portfolio/[wallet]/pnl, and all /api/supply/* endpoints
  • Portfolio snapshots — GET endpoint redacts totalCostBasisUsd, totalGunSpent, enrichmentPct for non‑owner requests; PnL endpoint short‑circuits with { redacted: true } before expensive RPC work
  • Supply endpoint gating — /api/supply/batch, /api/supply/item, /api/supply/rankings, /api/supply/drops return empty results for unauthenticated requests

v0.8.2

Mar 17, 2026

  • DexScreener liquidity integration — new data source for GUN token trading depth, volume, and sentiment; lib/api/dexscreener.ts fetches + aggregates across all GUN pairs with 60s in‑memory cache and stale fallback
  • /api/dex/gun‑liquidity route — GET endpoint returning GunLiquidityData or { available: false }; Cache‑Control: s‑maxage=60, stale‑while‑revalidate=30; always 200 (graceful degradation)
  • useGunLiquidity hook — SWR‑based with 60s polling and dedupingInterval; returns { data, loading, error }
  • LiquidityIndicator component — compact pill matching 7d price change badge styling (clipHex(4), color‑mix borders, font‑mono font‑semibold); classifies depth (deep/moderate/thin/minimal), activity, and sentiment
  • Player‑friendly tooltip — two‑part hover: plain‑language explanation of what liquidity means + data breakdown (volume, buy/sell ratio, pair count, primary market, market cap); no crypto jargon
  • GunBalanceCard integration — LiquidityIndicator rendered below USD value on both card faces (balance + price views)
  • Architecture updates — build‑games diagram Layer 1 expanded to 4‑column grid with Liquidity/DexScreener card; data source counts 5 → 6; README ASCII art + endpoint count 74 → 75; homepage pricing sources 4 → 5

v0.8.1

Mar 14, 2026

  • Cross‑wallet favorite labels — PinnedFavoritesRow now shows source wallet label (from portfolioAddresses) with title attr for full address; falls back to truncateAddress(), then "other wallet" for unresolved stubs
  • Favorite metadata now stores walletAddress — FavoritePinActions passes walletAddress into FavoriteButton metadata; usePinnedFavorites stub path reads fav.metadata.walletAddress instead of hardcoding "unknown"
  • walletLabels prop — PortfolioClient builds Map<string, string> from portfolioAddresses; threaded through PinnedFavoritesRow for label‑over‑address display
  • NFTGalleryGridCardProps.walletAddress — new optional prop threaded from NFTGallery → NFTGalleryGridCard → FavoritePinActions for metadata capture
  • Matrix decode entrance animation — CSS keyframes: matrix‑scanline (lime sweep), matrix‑card‑decode (clip‑path + scaleY + blur + brightness), matrix‑rain‑col (vertical lines), matrix‑label‑decode (letter‑spacing + blur flicker), matrix‑container‑enter (inward glow), matrix‑divider‑decode (scaleX sweep)
  • DecodeText component — character‑by‑character scramble effect using requestAnimationFrame interval; resolves left‑to‑right with randomized probability; 18‑frame cycle (~300ms)
  • MatrixRain component — generates N random vertical gradient lines with staggered delays/durations; positioned absolutely within matrix‑container
  • Animation gating — hasAnimated ref in PinnedFavoritesRow prevents replay on re‑renders; prefers‑reduced‑motion media query disables all matrix animations
  • Admin panel: Fees & Revenue tab — new AdminTab "fees" with FeesRevenueTab component; 3 sections: Revenue Streams (lime gradient, dynamic stats from /api/admin/revenue), User‑Facing Costs (purple gradient, static table), Operator Costs (warning gradient, static table with cache notes)
  • /api/admin/revenue route — GET endpoint, Bearer $ADMIN_SECRET auth; returns attestationCount, handleChangeCount, avaxCollected (placeholder values, TODO: wire to on‑chain events)
  • WalletAddressInput default border — border‑transparent → border‑white/[0.12]; focus: border‑[var(‑‑gs‑lime)]/40
  • useUserProfile event handler fix — gs:profile‑updated handler wrapped in queueMicrotask() to prevent "setState during render" warning when FavoriteButton dispatch triggers setProfile in WalletDropdown’s useUserProfile instance
  • UsersTools.tsx placeholder fix — \u2026 in JSX string attr rendered literally; wrapped in {""} expression for proper unicode interpretation
  • NFTGalleryPagination debug info — gated behind ?debug=1 URL param instead of NODE_ENV === "development"
  • usePinnedFavorites resolvedCacheRef — caches previously resolved NFT+wallet pairs so walletMap re‑fetches don’t downgrade full cards to stubs

v0.7.5

Mar 10, 2026

  • NFTDetailPositionCard text readability pass — row labels 9px/40% → 10px/50%, section headers 10px → 11px, GUN values 14px → 15px, USD conversions 50% → 60% opacity, arrows 30% → 40%, italic subtexts switched to text-white/50 + tracking-wide
  • Purple accent visibility — VIA SALES label, waterfall dropdown buttons, Reference Estimate header, warning icon all bumped to full opacity (were /60–/80)
  • Low-confidence waterfall dropdown: labels 20% → 30%, values 12px/40% → 13px/50%; full-confidence: labels 25% → 35%, values same bump
  • NFT description subtitle in modal: added tracking-wide letter spacing
  • Image lightbox — click NFT image in modal to open fixed z-[200] overlay; scroll-to-zoom (0.5×–5×, 0.15 step), click-to-toggle (1×/2×); bg-black/95 solid (no backdrop-blur to avoid GPU load); zoom % indicator at bottom; plain <img> tag to avoid Next.js Image fill constraints
  • Modal image size bump — 180×180/220×220 → 220×220/280×280 (mobile/desktop); container changed from aspect-square+max-w to explicit dimensions; parent switched to flex justify-center for proper centering
  • High-res image pipeline — imageHires field added to NFT type, CachedMetadataData, and avalanche.ts cache read path; /api/opensea/orders extracts image_url from maker_asset_bundle; threaded through getNFTListings → enrichment orchestrator → metadata cache (7d TTL); lightbox uses imageHires || image
  • Acquisition timeline tooltip — glass effect: background rgba(22,22,22,0.6), backdrop-blur 6px, border 12% white opacity

v0.7.4

Mar 10, 2026

  • /build-games landing page overhaul — reordered sections: "The Market" (13M players, 450K DAU, zero existing trackers) now leads as section 01 before Architecture; judges see the "why" before the "how"
  • GitHub repo link added to hero CTAs and bottom CTA section — links to Gunzilla-NYC/gunzscope for judge access to source code
  • Attestation explorer link — "View on Snowtrace" button in On-Chain Proof section links directly to the attestation contract on AVAX C-Chain
  • Market section copy refined — "OTG and GUNZscope Phase I is a sequenced strategic entry point, not the ceiling" replaces generic framing
  • Footer — CRYPTOHAKI text now links to /cryptohaki profile page

v0.7.3

Mar 8, 2026

  • CSS design token system — 12-step neutral scale (--gs-n-0 through --gs-n-11), 14 semantic aliases (--gs-bg, --gs-surface, --gs-card, --gs-text-*, --gs-border, --gs-status-*), 3 transition tokens (--gs-ease, --gs-dur-fast, --gs-dur-norm); enables single-point theme switching
  • Variable-driven corner-cut utilities — .cut-sm (4px), .cut-md (8px), .cut-lg (12px) classes backed by --gs-cut-* custom properties; replaces hardcoded clip-path polygons
  • Accessibility: scroll-behavior forced to auto in prefers-reduced-motion media query — smooth scrolling was still active for users with reduced motion enabled (WCAG 2.1 SC 2.3.3)
  • Compositing layer merge — grid-bg + scanlines collapsed into single .page-bg element across 8 page files; eliminates one full-viewport GPU texture per page (~2-4 MB VRAM savings)
  • IntersectionObserver auto-disconnect — observers in home page social proof, brand page, and build-games page now call unobserve() after element reveal; ~29 persistent observer callbacks eliminated post-animation
  • rAF-gated scroll handlers — Navbar, PublicNav, and ScrollToTopButton scroll listeners wrapped in requestAnimationFrame ticking gate with passive: true; caps setState calls at 1/frame (8× reduction on 120Hz displays)
  • content-visibility: auto on 3 below-fold home page sections (Features, Social Proof, Dashboard Preview) — browser skips layout+paint for offscreen content; ~200-400 DOM nodes deferred on initial load

v0.7.2

Mar 7, 2026

  • Wallet modal UX overhaul — "Connect Whitelisted Wallet" → "Get Started"; two lanes now labeled "View Only" (paste address) and "Full Access" (Dynamic connect) with badge indicators; description copy rewritten for clarity
  • Middleware view‑only bypass — /portfolio with ?address= query param now skips gs_session cookie validation; applies to both missing‑cookie and expired‑cookie paths in middleware.ts
  • connectionMode fix — PortfolioClient.tsx compares activeWalletData.address against primaryWallet.address (case‑insensitive); returns "view‑only" when viewing a different address than the connected wallet; fixes View Only badge, attestation button gating, and ShareDropdown signing controls
  • useIsViewOnly() hook — new selector in PortfolioContext.tsx; reads connectionMode from context; consumed by WalletIdentity.tsx (badge) and ShareDropdown.tsx (attestation gating)
  • Portfolio initializing hard timeout — 15s setTimeout in isPortfolioInitializing effect; fires regardless of gunPrice state; prevents permanent "Calculating…" when CoinGecko times out; existing 10s enrichment timeout preserved as inner guard
  • Error boundaries — app/global‑error.tsx (root‑level, wraps <html>/<body>) and app/portfolio/error.tsx (route‑level with brand styling); both show error digest + retry button
  • Body scroll lock — useEffect in app/page.tsx sets document.body.style.overflow="hidden" when showWalletModal is true; cleanup restores on unmount
  • NFTGalleryPagination "All N NFTs loaded" message gated behind process.env.NODE_ENV === "development"
  • E2E test suite — 57 Playwright specs across 5 files (wallet‑modal, view‑only‑flow, hard‑refresh, network‑edge‑cases, data‑integrity‑responsive); desktop‑chrome + mobile‑chrome (Pixel 7) projects; runs against BASE_URL || gunzscope.xyz
  • Tweet thread automation — compose‑tweet.mjs gains ‑‑version flag for targeting specific update entries; post‑tweet.mjs auto‑loads .env.local credentials; tweet.mjs interactive CLI with preview/edit/post flow

v0.7.1

Mar 5, 2026

  • Server‑side whitelist enforcement — middleware.ts verifies Bearer JWT (API routes) and gs_session cookie (page routes) against whitelist_entries via checkWhitelistEdge() on every request; 7 page routes + 26 API routes in matcher
  • Session cookie (gs_session) — lib/auth/sessionCookie.ts creates HS256‑signed JWT via jose SignJWT; set by /api/access/validate on 3 success paths (permanent, trial, promoted); cleared by /api/auth/logout POST; 7‑day maxAge, httpOnly + Secure + SameSite=Strict
  • Soft delete for whitelist — WhitelistEntry.isActive column; removeFromWhitelist() sets isActive=false instead of DELETE; banService.ts fixed to use update() not delete(); addToWhitelist() upserts with isActive=true for re‑activation
  • whitelistService.edge.ts — Edge‑compatible whitelist check via @neondatabase/serverless HTTP queries; predicate: isActive=true AND (expiresAt IS NULL OR expiresAt > NOW())
  • useAutoLogin whitelist pre‑check — calls /api/access/validate before /api/me; non‑whitelisted wallets get handleLogOut() immediately, preventing ghost profile creation
  • promoteFromWaitlist() fix — upsert update clause now sets isActive: true (survives prior admin soft‑delete)
  • KonamiOverlay trial‑expired UX — parses trialExpired from /api/access/konami 403 response; shows "Trial already used" instead of generic error
  • Navbar + WalletButton disconnect — fire‑and‑forget fetch("/api/auth/logout") before handleLogOut()
  • Ghost profile cleanup — 4 zero‑engagement UserProfile rows deleted; Digital Panoply whitelisted via direct DB upsert

v0.7.0

Mar 5, 2026

  • Portfolio Pins — FavoriteButton wired onto NFTGalleryGridCard image overlay (bottom‑right, opacity‑0 group‑hover:opacity‑100); PinButton component reads useUserProfile().favorites, PATCH /api/favorites/[id] toggles pinned; useNFTGalleryFilters partitions by pinnedRefIds Set, floats pinned to top preserving sort order within each group
  • isOwnPortfolio prop threaded NFTGalleryProps → NFTGalleryInner → NFTGalleryGridCard; computed in PortfolioClient via connectedWallets.includes(activeWalletData.address.toLowerCase())
  • Wishlist model — FavoriteItem extended with externalContract, externalTokenId, externalChain (String?), lastKnownValue (Float?), lastValueAt (DateTime?); type union includes "wishlist"
  • GET /api/favorites — listFavorites() splits by type !== "wishlist" vs type === "wishlist", returns { favorites, wishlist }
  • GET /api/favorites/refresh‑wishlist — updates lastValueAt for all wishlist items
  • AccountPanel Wishlist tab — TabId union extended; wishlistItems/favoriteItems computed from profile.favorites.filter(); star icon, lastKnownValue GUN display, lastValueAt date
  • Admin Users tab — listUsers() in userService with Prisma OR search (displayName, email, wallet address); GET /api/admin/users with verifyAdmin + isWhitelisted cross‑reference; UsersTools component with debounced search, 7‑column grid, whitelist dot indicator
  • Feature Requests GlitchLink in desktop navbar — gated behind hasWallet, positioned after ExperimentsDropdown
  • useUserProfile — togglePin() action with optimistic update; FavoriteItem interface gains pinned: boolean + wishlist fields; AddFavoriteInput gains optional externalContract/externalTokenId/externalChain
  • addFavorite() return type includes pinned: boolean; upsert create/update now persists wishlist fields
  • Display name fallback — UsersTools, ShareLeaderboard, admin shares page now show truncateAddress(primaryWallet) instead of raw email or "Anonymous"
  • shareService getShareLeaderboard() includes primaryWallet via wallets relation (orderBy isPrimary desc, take 1)
  • Duplicate cryptohaki UserProfile cleaned via raw SQL script (scripts/cleanup‑duplicate‑user.ts) with correct FK column mapping per table

v0.5.4

Mar 4, 2026

  • PortfolioSparkline component — new chart in ValueHeader showing % change from cost basis over time; dual‑clip split‑gradient fill (profit above zero, loss below), zero baseline dashed line, pulsing endpoint dot, edge‑aware hover tooltip as absolute div sibling
  • PortfolioSparkline colors — profit fill #A6F700 (brand lime), loss fill #B44AFF (soft orchid / ‑‑gs‑rarity‑epic); endpoint glyph + drop‑shadow glow match; tooltip % text uses same palette
  • ValueHeader 7d badge loss state — switched from var(‑‑gs‑loss) red to #B44AFF orchid (bg/border/text)
  • PnLScatterPlot refinements — floor cluster annotation (horizontal dashed line for dots < 5 GUN), simplified legend (4 items), zone label opacity 0.18 + fontSize 11, break‑even label rotation via atan2, break‑even dot treatment (white stroke within 5%)
  • PnLScatterPlot + AcquisitionTimeline margins normalized to { top: 16, right: 24, bottom: 32, left: 40 } in embedded mode
  • AcquisitionTimeline — removed vertical grid lines; lollipop stems converted to gradient opacity (5 linearGradient defs per venue, stemGradient() helper)
  • AcquisitionTimeline stem gradient fix — gradientUnits="userSpaceOnUse" with explicit pixel y1/y2 coordinates; default objectBoundingBox caused zero‑width line degenerate bounding box
  • AcquisitionTimeline horizontal crosshair — dashed line from hovered dot to Y‑axis on proximity lock
  • AcquisitionTimeline tooltip — added acquisition date (MMM DD, YYYY) and current USD value fields
  • AcquisitionTimeline stem bottom stopOpacity reduced from 0.25 to 0.10

v0.5.3

Mar 4, 2026

  • 40‑item performance audit across 5 phases — parallelization, caching, architecture, DRY, decomposition
  • Parallelized 6 critical paths: fetchGunPricesForDates (5‑concurrent batches), /api/leaderboard (Promise.all DB+price), floor‑drop/portfolio‑digest/whale‑tracker crons, avalanche.ts RPC calls
  • Cache‑Control headers on 6 API routes: /api/price/gun, /api/portfolio/[wallet]/pnl, /api/nft/pnl/[tokenId], /api/marketplace/purchases/*
  • Deferred Vercel Analytics + SpeedInsights after hydration via AnalyticsDeferred component
  • motion/react added to optimizePackageImports — tree‑shakes Framer Motion bundle
  • useGunPrice migrated to SWR — built‑in dedup, stale‑while‑revalidate, background refresh
  • Shared server‑side GUN price cache (lib/server/gunPrice.ts) — single in‑memory cache across all API routes
  • /api/scarcity — in‑memory cache + parallel mint‑count lookups
  • React.cache() on share/referral server pages for same‑request dedup
  • PortfolioContext — 14 prop‑drilled values lifted into context provider, eliminates drilling cascade
  • GalleryFilterContext — filter state moved from prop threading to dedicated context
  • WalletIdentity — removed redundant boolean guards
  • Deduplicated timeAgo, validateSlugLocally, slug constants into lib/utils/
  • OpenSea cache‑control helpers consolidated to app/api/opensea/cacheHelpers.ts
  • memo() on NFTGalleryControls + chart sub‑components (AcquisitionTimeline, PnLScatterPlot)
  • animate‑spin SVGs wrapped in <span> for GPU compositing
  • .map().filter() → single‑pass .reduce() in hot paths
  • Leaderboard DB‑level DISTINCT instead of JS dedup
  • Cron logAlert via after() for non‑blocking response
  • AdminPanel.tsx decomposed: 1,342 → 155 lines + 8 sub‑components in components/account/admin/
  • useNFTAcquisitionPipeline: types + candidates extracted to lib/hooks/acquisition/ (1,747 → 1,360 lines)
  • brand/page.tsx: 5 static sections extracted to app/brand/sections/ (1,682 → 195 lines)
  • PortfolioClient: PortfolioSummaryBar + Footer converted to dynamic() imports (5 total lazy‑loaded components)
  • Passive scroll listeners + startTransition for scroll‑driven state
  • 10s timeout on all CoinGecko fetches
  • /api/price/history restored to 24h revalidate cache

v0.5.2

Mar 2, 2026

  • /build‑games landing page — hero, stats bar (live from /api/stats/site), BuildVelocityChart, multi‑chain architecture diagram, 9‑card feature grid, on‑chain proof section (live attestation count), valuation waterfall, dashboard preview, roadmap CTA; layout matches homepage patterns (max‑w‑7xl, section‑number, section‑line)
  • /build‑games/roadmap — public fork of /strategy with admin gate removed; DocBadge, BuildVelocityChart, 6 phases, ScopeBar, vision banner; minimal nav with Logo + back link
  • PublicNav component — reusable nav for public pages (no Dynamic Labs dependency); Logo, VersionBadge, GlitchLink items, Login CTA, mobile hamburger, scroll backdrop blur; matches Navbar layout exactly
  • /explore now uses PublicNav instead of Navbar — anonymous visitors see Home + Onchain ID links instead of empty bar
  • /explore/attestation/[cid] viewer page — renders attestation metadata as branded UI instead of raw JSON; summary cards (wallet, value, items, block), merkle root with copy, holdings table (first 100 + expand), Autonomys DSN badge; getMetadataLink() extracts CID and links to viewer
  • Strategy page — added BuildVelocityChart section above vision banner
  • Homepage — Onchain ID link now uses GlitchLink (bracket + scramble effect on hover)
  • KonamiOverlay bug fixes — input bg‑black/60 → bg‑black/85 (bottom transparency fix); border‑1 → border‑2 with higher opacity (0.15 empty, 0.50 valid); input text‑white/90 → text‑[var(‑‑gs‑white)] + tracking‑wider + caret‑lime; focus‑within state on empty input
  • globals.css — input::selection override: rgba(166,247,0,0.3) instead of solid lime; fixes green flash on paste
  • DynamicProvider — .modal‑card bg rgba(22,22,22,0.5) → rgba(22,22,22,0.88); auth modal readability fix

v0.5.1

Mar 2, 2026

  • Nav restructure — Leaderboard, Scarcity, Market grouped under “Experiments” dropdown; new ExperimentsDropdown component with GlitchLink‑style trigger (useGlitchScramble, brackets, chevron), close‑on‑outside/Escape/route‑change
  • “Explore” renamed to “Onchain ID” across navbar (desktop GlitchLink + mobile menu) and home page
  • Explore page — Autonomys DSN changed from external link to static label; Avalanche link color → AVAX red (#E84142); Autonomys label color → brand blue (#4A7AFF); per‑row metadata links blue
  • Autonomys attestation count fix — isAutonomysURI() helper matches both direct gateway URLs (gateway.autonomys.xyz) and proxy URLs (/api/attestation/metadata/{cid}); tooltip now shows correct count
  • Admin notification badge on WalletDropdown — fetches /api/feature‑requests, counts status=open; red pill badge on trigger + inline badge next to Feature Requests nav item; admin‑only, refreshes on route change

v0.5.0

Mar 2, 2026

  • /api/market/listings — parallelized getActiveListingsDetailed + getCollectionSaleEvents via Promise.all; previously sequential
  • Module‑level collection sales cache in OpenSeaService.getCollectionSaleEvents — keyed by slug::afterDateMs, 30‑min TTL; findUsableSalesCache() cross‑key reuse (unfiltered 200‑entry cache serves 50‑entry unfiltered requests)
  • Module‑level GunzScan name cache in resolveTokenMetadata — keyed by contract:tokenId, 24h TTL, 500‑entry max; batch fetch only uncached tokens; logs hit/miss ratio
  • New lib/api/marketCache.ts — MarketReferencePrice / MarketReferencePriceCache interfaces; module‑level singleton with 5‑min TTL; populated by /api/market/listings, consumed by /api/market/reference‑prices
  • New /api/market/reference‑prices route — lightweight GET returning per‑item‑name floor prices from shared cache; no OpenSea calls; s‑maxage=300, stale‑while‑revalidate=60; returns empty if market data not cached
  • MarketReferencePriceData type added to lib/types.ts
  • applyValuationTables() — new optional 5th param marketReference?: MarketReferencePriceData; fills currentLowestListing from market bulk data only when per‑NFT enrichment hasn’t provided one (nft.currentLowestListing === undefined)
  • PortfolioClient.tsx — 4th parallel fetch for /api/market/reference‑prices alongside rarity/comparable/floor; passed to applyValuationTables()
  • NFTDetailPositionCard — expandable waterfall tier list in both Reference Estimate and Market Reality sections; builds waterfallTiers from currentLowestListing/comparableSalesMedian/rarityFloor/floorPrice; best tier marked, others behind toggle

v0.4.9

Mar 2, 2026

  • /api/attestation/status — now returns contractAvaxBalance (contract’s AVAX balance from collected fees) and attestFee (current fee in AVAX); batched via Promise.all with existing totalAttestations query
  • ATTESTATION_ABI extended with owner() view and withdraw() — new exports: withdrawFees(signer) calls contract.withdraw() and waits for receipt; getContractOwner(provider) reads on‑chain owner address
  • OnChainTools admin component — AVAX Earned + Current Fee rows in Live Status; Withdraw button with 5‑state flow (idle/switching/signing/confirming/success); chain switch via ensureAvalancheChain, signer from Dynamic’s primaryWallet.connector.getWalletClient(); tx hash links to Snowtrace on success
  • Admin panel auth fix — production showed empty whitelist/waitlist because NEXT_PUBLIC_ADMIN_SECRET was not set in Vercel; fetch callbacks use data.entries ?? [] which treats 401 as empty data silently

v0.4.8

Mar 1, 2026

  • PortfolioAttestation.sol v3 — attest() now takes explicit `address wallet` as first parameter instead of using msg.sender; enables delegation (wallet A pays, wallet B gets the attestation); require(wallet != address(0)) guard
  • UUPS proxy upgraded on C‑Chain: implementation 0x80A6C9661Fb0fEd1cCEBf568bCb709D548B98358; proxy address unchanged (0xEBE8FD7d40724Eb84d9C888ce88840577Cc79c16)
  • Frontend ABI + submitAttestation() updated to pass wallet param; usePortfolioAttestation hook passes tracked walletAddress (in‑game wallet) as attestation target
  • Attestation flow reordered: chain switch → get signer → fetch blockNumber from signer’s provider (fixes RPC mismatch causing estimateGas failures)
  • Contract ownership transferred to Ledger wallet (0x8ABF…488C); hardhat‑ledger plugin + upgrade script configured for future UUPS upgrades
  • Implementation contract verified on Snowtrace via Routescan API
  • Test suite expanded to 29 tests: delegation (alice pays for bob), multi‑payer, zero‑address rejection

v0.4.7

Mar 1, 2026

  • PortfolioAttestation.sol v2 — configurable attestFee (0.01 AVAX default), owner/withdraw/transferOwnership/setFee; attest() now payable with require(msg.value >= attestFee); totalFeesCollected counter; FeeUpdated + OwnerTransferred events
  • Contract redeployed to Avalanche C‑Chain: 0xf8f5aa3D940009987F02AD92e44A5434Bab748bf
  • Frontend contract layer — ABI updated with payable attest + attestFee() view; submitAttestation() reads fee from contract and sends as tx value; exported getAttestFee() for UI display
  • Metadata URI now routes through gunzscope.xyz/api/attestation/metadata/{cid} instead of gateway.autonomys.xyz — /api/attestation/metadata/[cid] proxy route with 24h immutable cache
  • Test suite expanded to 26 tests: fee enforcement, overpayment, owner withdraw with gas accounting, ownership transfer, fee update, zero‑address guard

v0.4.6

Mar 1, 2026

  • GUN Δ hollow dots on Cost vs Value scatter chart — NFTs without market valuation (no listing, comparable sale, rarity floor, or collection floor) now appear as hollow circles positioned via synthetic floor derived from GUN token appreciation: syntheticFloor = cost × (gunPrice / historicalGunUsd)
  • Hollow dots use profit/loss green/red coloring (not amber) with dashed stems to distinguish from filled market‑valued dots; fixed 4px radius, no quantity scaling
  • Grouped GUN Δ NFTs (×2, ×3) expanded into individual hollow dots with deterministic ±2% cost jitter to prevent visual stacking
  • Legend restructured with MARKET / GUN Δ sections separated by pipe dividers; each section shows filled vs hollow profit/loss indicators
  • portfolioInsights.ts — generateInsights now includes GUN Δ items in total unrealized P&L; added getGunDeltaPnlUsd() helper for Track A currency appreciation; new gun_delta insight type with isNeutral amber styling
  • Insight quantity counting uses nft.quantity ?? 1 instead of array length — grouped ×2 NFTs count as 2 individual items in "below cost basis" and totalTracked denominators
  • InsightsPanel.tsx — gun_delta icon (swap arrows SVG) with amber accent when isNeutral
  • portfolioInsights test suite expanded to 12 tests; added quantity>1 coverage

v0.4.5

Mar 1, 2026

  • Autonomys Auto Drive integration — attestation metadata uploaded to Autonomys DSN via @autonomys/auto‑drive SDK; /api/attestation/upload route accepts JSON payload, returns CID + gateway URL; usePortfolioAttestation uploads before on‑chain tx, falls back to inline data: URI if upload fails
  • On‑Chain Explorer — public /explore page queries PortfolioAttested events from C‑Chain contract; /api/attestation/events route with chunked queryFilter (49k block range per call, public RPC 50k limit) + 5‑min server‑side cache + stale fallback on error; useExplorer hook
  • Explorer UI — stats banner (attestations/wallets/GUN attested), desktop table + mobile cards, Snowtrace tx/address links, Autonomys gateway metadata links, loading skeletons, empty state with portfolio CTA
  • Navbar + sitemap + home page Explorer links — public page, no auth gate; added to isInApp check, desktop GlitchLink, mobile menu, sitemap at hourly/0.7
  • Exported ATTESTATION_ABI and getContractAddress() from lib/attestation/contract.ts for event query reuse

v0.4.4

Mar 1, 2026

  • Seaport v1.6 ABI fix — extractCostFromOrderFulfilled now tries v1.5 ABI first, falls back to v1.6 (which adds address recipient to OrderFulfilled non‑indexed data); previous v1.5‑only decode silently failed on GunzChain’s Seaport, causing tx.value fallback to assign total batch cost to every item
  • NFT detail cache schema v24→v25 — invalidates all stale entries with incorrect batch‑purchase prices
  • On‑chain portfolio attestation — usePortfolioAttestation hook builds Merkle tree of NFT holdings, submits root+totalValue to PortfolioAttestation contract on Avalanche C‑Chain; ensureAvalancheChain() handles wallet_switchEthereumChain / wallet_addEthereumChain
  • ShareDropdown attestation UI — "Attest On‑Chain" button with 7‑state flow (idle/building/switching‑chain/signing/confirming/success/error); existing attestation indicator; Snowtrace tx link on success
  • WalletIdentity passes Dynamic primaryWallet connector + isOwnWallet to ShareDropdown for attestation signing
  • Attestation status API — GET /api/attestation/status reads deployer GUN balance (GunzChain) + AVAX balance (C‑Chain) + totalAttestations from contract
  • AdminPanel OnChainTools — added C‑Chain AVAX balance display
  • Hardhat config — added avalanche mainnet network (chainId 43114) + Routescan etherscan verification; deploy:avalanche npm script

v0.4.3

Feb 28, 2026

  • Konami trial access — Konami code now grants 72‑hour trial whitelist instead of permanent access; WhitelistEntry gains expiresAt DateTime? field (null = permanent, non‑null = trial)
  • getWhitelistStatus() discriminated union — returns { status: permanent | trial | expired | none, expiresAt? }; replaces boolean isWhitelisted() in validate and waitlist/status endpoints
  • Konami API rewrite — checks existing whitelist status before granting; permanent → no‑op, active trial → return existing info, expired → 403 "trial already used", none → creates 72h trial entry + waitlist entry with threshold 1
  • joinWaitlistForTrial() — creates or updates waitlist entry with promotionThreshold: 1 (vs default 3); idempotent downgrade if already on waitlist
  • Tiered referral thresholds — DEFAULT_PROMOTION_THRESHOLD=3, TRIAL_PROMOTION_THRESHOLD=1, EXPIRED_TRIAL_PROMOTION_THRESHOLD=2
  • bumpExpiredTrialThreshold() — lazy idempotent upgrade 1→2 on expired trial detection; called from validate and waitlist/status endpoints
  • promoteFromWaitlist() upsert pattern — converts expired trial to permanent whitelist (sets expiresAt: null) when referral threshold met
  • Validate API trial‑aware responses — { trial: true, expiresAt } for active trials; { trialExpired: true, waitlisted: true } with threshold bump for expired
  • Home page trialExpired redirect — expired trial users redirect to /waitlist?address=xxx&trialExpired=true; active trial stores expiresAt in localStorage
  • KonamiOverlay messaging — "TRIAL ACCESS GRANTED" with "72 hours of full access activated" and "Refer 1 friend to keep it forever"
  • WaitlistClient trialExpired state — reads ?trialExpired=true URL param; shows "Trial Ended" header with dynamic referral count based on per‑entry promotionThreshold

v0.4.2

Feb 28, 2026

  • Ban/reset system — BanEntry model in Prisma; banService with isBanned/banAddress/unbanAddress/resetAddress/listBans; ban guards on /api/access/validate, /api/access/konami, /api/access/reconcile, /api/waitlist/status, and joinWaitlist(); banned users get 403 with { banned: true }
  • Admin PATCH /api/admin/whitelist — { address, action: "ban"|"unban"|"reset", reason? }; ban removes from whitelist + waitlist + blocks re‑enrollment; reset clears whitelist + waitlist without ban (user can rejoin); GET ?view=banned returns paginated ban list
  • AdminPanel ban/reset UI — Ban/Reset buttons on whitelist entries, Ban button on waitlist entries, dedicated Banned Users section with unban capability
  • WaitlistClient banned state — useWaitlist hook returns isBanned; banned users see "ACCESS REVOKED" page with support contact instead of join form
  • ReferralRedirect banned handling — email‑only validate returning { banned: true } shows revoked message instead of redirect
  • KonamiOverlay wallet flow fix — submitting state was never reset after successful wallet submission, permanently disabling the Confirm button in the handle phase; added setSubmitting(false) + initial handle availability check via /api/referral/check‑slug
  • Home page validation refactor — separated wallet and email validation into independent useEffect blocks; wasConnectedOnMount now tracks wallet only (email users always validate); emailValidatingRef prevents duplicate email validation calls
  • Attestation status API — GET /api/attestation/status route stub
  • Hardhat config — updated GunzChain testnet RPC URL; added Fuji (Avalanche C‑Chain testnet) network

v0.4.1

Feb 28, 2026

  • Hero redesign — split title into super‑label ("YOUR OTG" at text‑2xl‑3xl with 0.15em tracking) + dominant scramble headline (text‑6xl‑[104px]); removed "Arsenal" line; purple glow text‑shadow on OTG (40px blur, 0.3 opacity)
  • Scramble words updated — Intelligence, Lore, Legacy, Edge (was Intelligence, Dominance, Advantage, Edge)
  • Hero subtitle rewrite — "The tactical intelligence layer for Off The Grid / Start your legacy, analyze the market, dominate the meta."
  • CTA section hierarchy — "Early access — whitelist only" label above Connect Wallet button; Konami hint below at white/25 opacity; removed redundant "Connect here" link
  • KonamiOverlay v2 — "Ready Player Zero" title with rotating subtitles (3 variants, module‑level counter); custom input accepting EVM/Solana/email with type badge; onSubmit returns Promise<boolean>; confirmed state with "CLEARANCE CONFIRMED" card; auto‑proceeds to Dynamic auth after 2.5s via onProceed callback

v0.4.0

Feb 27, 2026

  • Konami code easter egg — useKonamiCode hook listens for ↑↑↓↓←→←→BA on home page; 2s inactivity timeout resets progress; triggers KonamiOverlay
  • KonamiOverlay tactical scan — full‑screen overlay with hex matrix rain (120ms refresh), horizontal scan line (2.5s linear sweep), corner bracket framing, 5‑line sequential reveal with motion/react stagger
  • Konami whitelist input — after scan animation completes, WalletAddressInput slides in with clip‑path ENTER button; validates via detectChain; ESC or backdrop click to dismiss
  • POST /api/access/konami — accepts { address, email }; attempts promoteFromWaitlist first, falls back to addToWhitelist with "Konami code" label; reconciles email+wallet if both provided

v0.3.9

Feb 27, 2026

  • Email waitlist gate — /api/access/validate accepts { email } alongside { address }; emails stored as email:user@example.com identifier in waitlist/referrer tables; deriveAutoSlug handles email: prefix (username before @)
  • Email‑to‑wallet reconciliation — new POST /api/access/reconcile endpoint; promoted email user connects wallet → whitelists wallet address with reconciled:{email} note; WaitlistClient detects wallet connection and auto‑reconciles
  • WaitlistClient email mode — identifier resolution from wallet OR ?email= search param; "Signed in as" email notice; promotion celebration shows Connect Wallet CTA instead of auto‑redirect for email users
  • useWaitlist type parameter — hook accepts type: "wallet" | "email"; fetches /api/waitlist/status with ?address= or ?email= accordingly
  • Email gate in app/page.tsx — email‑only Dynamic users (no wallet) now go through /api/access/validate instead of bypassing straight to /portfolio; non‑whitelisted emails redirect to /waitlist?email=
  • GlitchText container stability — glitch hover effect no longer resizes the login button; fixed by preserving container dimensions during letter scramble animation
  • SEO: robots.txt — blocks /api/, /admin/, /brand, /roadmap, /strategy; references sitemap.xml
  • SEO: dynamic sitemap — app/sitemap.ts with 13 public pages, priority‑weighted (1.0 home → 0.1 legal)
  • SEO: root layout metadata — Viewport export (themeColor #0A0A0A), metadataBase, title.template "%s | GUNZscope", default OG/Twitter cards, JSON‑LD WebApplication schema
  • SEO: page metadata — added Metadata exports to changelog, updates, credits, privacy, terms, cookies; created layout.tsx wrappers for client‑component pages (market, insanity); noindex on brand/roadmap/strategy
  • SEO: canonical URL on /portfolio layout to prevent ?address= duplicates
  • SEO: alt text on 4 NFT images in scarcity and market pages (was empty string)
  • Admin‑gated /strategy page — 6‑phase strategic roadmap; vertical timeline with phase nodes, pill‑tag items, market scope expansion bar
  • Admin panel Links tab — card grid linking to all admin/internal pages with colored dot indicators
  • Brand page Working Links section — Build Games landing preview link; Waitlist Flow section 06 with gate/state/promotion test panels

v0.3.8

Feb 26, 2026

  • Waitlist redirect fix — paste‑address flow (non‑Dynamic‑SDK users) now passes address as query param to /waitlist; WaitlistClient reads from primaryWallet?.address || searchParams.get(‘address’); previously redirected back to / because no Dynamic wallet was connected
  • Waitlist page Suspense boundary — wrapped WaitlistClient in <Suspense> in app/waitlist/page.tsx; required by Next.js for useSearchParams() during static prerender
  • Waitlist status API auth removed — GET /api/waitlist/status no longer requires Dynamic JWT; position and referral count are non‑sensitive; paste‑address users have no auth token
  • useWaitlist hook auth optional — getAuthToken() now called inside try block; token sent as Authorization header only when available; missing token no longer causes early return that left isLoading stuck at true
  • Reusable WalletAddressInput component — components/ui/WalletAddressInput.tsx; unified chain detection (GunzChain/Solana badge, validation border, hint text) across 5 input locations: home page, admin panel, account page, insanity mode, portfolio search
  • WalletAddressInput validateChain prop — when false, disables red border for non‑matching input; used by Handle Tools where slugs are valid input alongside wallet addresses
  • Admin panel column layout standardization — WhitelistTools, WaitlistTools, ShareLeaderboard all use same toolbar‑at‑top pattern with shrink‑0 pb‑3 mb‑3 border‑b divider; address + action button on same row
  • Stale .next cache diagnosis — dev server was serving compiled chunks with old error text "This address isn’t on the early access list yet." from cached .next/dev/ files while source had been updated

v0.3.7

Feb 23, 2026

  • Referral register GET handler — added try/catch around getReferrerByWallet() DB call; was the only API route handler without error wrapping, causing unhandled throws to return HTML 500 instead of JSON
  • useReferral client hook — added regRes.ok guard before .json() parse; non‑2xx responses now log status + body via console.warn("[Referral]") and show status code in error message instead of generic "Failed to load referral data"
  • Admin‑gated /roadmap page — app/roadmap/page.tsx; converted gunzscope‑blockchain‑architecture.html to React/Tailwind with brand CSS vars, clip‑path corners, proper typography; isAdminWallet() gate with redirect

v0.3.6

Feb 22, 2026

  • Updates page accordion refactor — extracted UPDATES data + UpdateEntry interface to lib/data/updates.ts; page.tsx slimmed from 449 → 48 lines, imports data and delegates to new UpdateTimeline client component
  • UpdateTimeline component — components/updates/UpdateTimeline.tsx; ‘use client’ accordion with useState‑based open set, CSS height transition (200ms ease‑in‑out), chevron rotation; tag:’current’ entry auto‑expanded, others collapsed, multiple can be open simultaneously
  • Push‑to‑main workflow updated — CLAUDE.md and docs/notes/push‑to‑main‑workflow.md now reference lib/data/updates.ts instead of app/updates/page.tsx for user‑facing update entries

v0.3.5

Feb 22, 2026

  • Card/modal market data unification — NFTDetailPositionCard Track B now uses same waterfall as deriveCardData(): marketExitGun → comparableSalesMedian → rarityFloor → currentLowestListing; previously modal only checked marketExitGun + computeMarketInputs (which excluded rarityFloor)
  • Tier‑confidence gating — new trackBIsSalesBased boolean on NFTCardData; gallery cards and list rows only show Track B MARKET line for sales‑based tiers (1‑4: EXACT, VIA SALES, VIA SKIN, VIA WEAPON); statistical proxies (tiers 5‑6: RARITY, FLOOR, LISTED, SIMILAR) suppressed from card display
  • Modal low‑confidence treatment — tiers 5‑6 render "Reference Estimate" card instead of "Market Reality": dimmer styling (opacity‑80), no VS COST row, no MARKET P&L percentage, warning text explaining it’s a proxy not sales data
  • exitTierLabel fallback chain — modal’s Track B exitTierLabel now falls back through VIA SALES → RARITY → LISTED when marketExitTierLabel is null, matching card behavior

v0.3.4

Feb 22, 2026

  • Dual‑track P&L card redesign — NFTDetailPositionCard restructured into two distinct cards: Track A ("Your Deal", lime border) shows GUN token appreciation since purchase; Track B ("Market Reality", purple border) shows market‑based P&L from comparable sales waterfall
  • Cost Basis merged into Track A — standalone COST BASIS section removed; cost basis row now lives inside the Track A card as the first data row, followed by TODAY’S VALUE row, providing a natural reading flow from "what you paid" → "what it’s worth" → "your P&L"
  • P&L hero treatment — both track cards use a border‑top divider to separate data rows from the P&L value, displayed in font‑display 20px bold with profit/loss coloring; italic subtext below explains the number in plain English
  • Label‑left / value‑right row pattern — all data rows use flex justify‑between with shrink‑0 labels (mono 9px uppercase gray) and right‑aligned values (display 14px semibold white tabular‑nums); arrow separator (→) between GUN and USD amounts
  • Track B confidence line — shows data quality indicator (green dot + tier label like "VIA SALES" + sample count), plus ABOVE/BELOW FLOOR pill when market exit differs from collection floor
  • Card styling standardized — bg‑[var(‑‑gs‑dark‑3)] with subtle border, 3px colored left border (lime for Track A, purple for Track B), p‑5 padding, rounded‑lg corners
  • Removed Observed Market Range section — NFTDetailObservedMarketRange component no longer rendered in modal; getPositionOnRange helper removed
  • Admin gate on /brand page — /brand now restricted to admin wallets using existing isAdminWallet() utility; non‑admin users redirected to /

v0.3.3

Feb 22, 2026

  • Tiered valuation waterfall (Track B) — 6‑tier Market Exit estimate per NFT: EXACT (same tokenId), VIA SALES (same baseName), VIA SKIN (same skinDesign), VIA WEAPON (same weapon), SIMILAR (deferred), FLOOR (collection floor)
  • Time‑weighted median — comparable sales weighted by recency (7d = 1.0, 7‑30d = 0.75, 30‑90d = 0.50, 90+ = 0.25); weighted‑median walk instead of simple median
  • Item name parser — new parseItemName() extracts skinDesign and weapon from "X for the Y" naming pattern, enabling Tier 3 and Tier 4 waterfall groupings
  • Pure valuation service — lib/portfolio/valuationService.ts walks waterfall with minimum‑sale‑count gates (1 for EXACT, 2 for all others), returns estimatedGun + tier + tierLabel
  • Waterfall data in comparable‑sales API — /api/opensea/comparable‑sales now returns waterfall (byTokenId, byName, bySkin, byWeapon) alongside existing items; backward‑compatible optional field
  • applyValuationTables enhanced — calls getMarketExitValuation() per NFT, writes marketExitGun, marketExitTier, marketExitTierLabel to NFT objects
  • Track B on gallery cards — grid cards (medium+) and list rows show "~592 GUN · VIA SALES" below existing P&L and ValuationLabel
  • Track B in modal QuickStats — 4th column "Market Exit" shows estimated GUN, USD conversion, tier label, and P&L vs cost basis; grid adapts 3→ 4 columns when data available
  • Scarcity tracking prep — useNFTEnrichmentOrchestrator now tracks max observed mint number per baseName in scarcityMapRef for future Tier 5 matching
  • NFT type extended — 3 new fields: marketExitGun, marketExitTier (1‑6), marketExitTierLabel

v0.3.2

Feb 21, 2026

  • Full‑pagination enrichment — enrichment now defers until all NFT pages are loaded (was firing per‑50‑item page, causing concurrent enrichment races and backward progress jumps on wallets with 50+ NFTs)
  • Generation‑guarded enrichment — startEnrichment increments a generation counter; all state updates (setProgress, setEnrichedNFTs, updateCallback, setIsEnriching) check gen === generationRef.current before writing, preventing stale enrichments from wallet switches
  • Enrichment diagnostic summary — console.info after completion logs total/cached/fresh/failed counts, date/costGUN/costUSD/listing resolution percentages, free transfer count, and venue breakdown
  • Incremental refresh — handleRefresh uses new invalidateListingPrices() instead of clearWalletCache(); only clears listingFetchedAt/currentLowestListing/currentHighestListing on each cached entry, preserving all acquisition data
  • Removed groupNFTsByMetadata import from PortfolioClient — handleLoadMoreNFTs now passes mergedNFTs (already grouped) directly to startEnrichment instead of re‑grouping per page
  • cumulativeBaseRef reset — new startEnrichment calls reset cumulativeBaseRef to 0 alongside generation bump, preventing stale cumulative offsets

v0.3.1

Feb 21, 2026

  • Server‑side GUN price history cache — new GunPriceHistory Prisma model stores confirmed historical GUN/USD rates in Neon PostgreSQL; shared across all users so the first person to resolve a date’s price populates it for everyone
  • Waterfall tier 2: server cache — resolveHistoricalGunPrice now checks the shared server table between localStorage and CoinGecko (3s timeout), with write‑through to localStorage on hit and fire‑and‑forget write‑back on CoinGecko/DefiLlama success
  • GET /api/gun‑price/history — public endpoint with CDN caching (1h fresh, 24h stale‑while‑revalidate); 404s cached for 5 min to avoid hammering DB for missing dates
  • POST /api/gun‑price/history — validated write endpoint with ATH guard, confidence‑based upgrade logic (won’t overwrite daily with estimated), rejects estimated prices from shared table
  • "Synced X ago" indicator — ValueHeader shows when the portfolio was last loaded from server cache, with staleness coloring (>24h = brighter gray)
  • Manual refresh button — spinning refresh icon next to the synced timestamp clears localStorage cache and re‑triggers full wallet fetch + enrichment
  • Refresh disabled during enrichment — button grays out and spins while NFT enrichment is active to prevent redundant requests

v0.3.0

Feb 21, 2026

  • Modal P&L reorganization — separated market valuation from GUN appreciation into two distinct P&L stories: market-based (via listings/sales/floor) when available, xGUN fallback otherwise
  • Unified P&L computation — QuickStats UNREALIZED and YOUR POSITION P&L now always agree (both use market-first, xGUN fallback)
  • Valuation method labels — QuickStats and YOUR POSITION show specific source: VIA LISTING, VIA SALES, VIA FLOOR, or GUN Δ
  • GUN Based Performance sub-section — when market data drives headline P&L, a separate section shows pure GUN token appreciation with explanatory narrative
  • CoinGecko fetch guard — modal acquisition pipeline skips redundant historical price fetches when enrichment data already provides confirmed USD values
  • Valuation method badges on gallery cards — 6-tier taxonomy (LISTED, SALES, RARITY, FLOOR, COST, UNLISTED) shown on NFT cards
  • Hidden redundant GUN @ line — cost basis section no longer shows "GUN @ $X.XXXX at time of purchase" when GUN Based Performance section is visible

v0.2.9

Feb 21, 2026

  • xGUN P&L formula — PnL now purely reflects GUN/USD price appreciation: Y = historicalGunPrice, Z = currentGunPrice, P&L = costGun × (Z‑Y). Removed market‑data waterfall (listing/comparable‑sales/rarity‑floor) from cards, modal, sort, and portfolio summary
  • Removed pnlSource labels — "vs listing" / "vs sales" / "vs floor" badges no longer appear on NFT cards since PnL is now single‑source
  • OpenSea event_timestamp fix — 4 parse sites were treating Unix seconds as milliseconds, producing dates in January 1970 and triggering $0.0776 fallback prices
  • Stale closure overwrite fix — async modal loadItemDetails captured resolvedAcquisitions at effect start time; cache‑rendered data was overwritten ~1s later. Fixed via resolvedAcquisitionsRef pattern in both NFTDetailModal and useNFTAcquisitionPipeline
  • Transfer chain tracing fix — buildCandidateFromHoldingRaw now uses senderAcquiredAtIso, senderVenue, and senderTxHash when using sender cost data, instead of the transfer date/venue
  • Transaction fee extraction — txFeeGun and senderTxFeeGun computed from receipt.gasUsed × receipt.gasPrice in avalanche.ts; propagated through ResolvedAcquisition, selectBestAcquisition, and all candidate builders
  • Gas fees display — YOUR POSITION section in NFTDetailModal shows purchase and transfer gas fees when available
  • Server cache hydration sanitization — PortfolioClient strips legacy purchasePriceUsd values from server‑cached NFTs when purchasePriceUsdEstimated !== false
  • CoinGecko ATH sanity check — /api/price/history rejects prices above $0.12 (GUN ATH ~$0.115) and logs a warning
  • Stale Next.js server cache bypass — /api/price/history temporarily switched to cache: 'no‑store' to purge incorrect CoinGecko historical data (revert to revalidate: 86400 after confirmation)
  • Enrichment trust guard — modal no longer overwrites confirmed purchasePriceUsd (purchasePriceUsdEstimated === false) with its own recomputation
  • Cache schema v24 — full client‑side re‑enrichment forced after CoinGecko data correction and server cache purge
  • MetaMask fallback — main page wallet connect falls back to MetaMask deep link when Dynamic SDK fails to trigger wallet
  • wGUN acquisition support — enrichment orchestrator handles wGUN‑based purchases for cost extraction
  • useNftPnL hook rewrite — portfolio summary P&L now uses xGUN formula instead of floor‑based calculation
  • PnL sort rewrite — useNFTGalleryFilters pnl‑desc sort uses xGUN unrealized USD gain with currentGunPrice threading

v0.2.8

Feb 19, 2026

  • Historical price CORS fix — new /api/price/history server‑side proxy routes CoinGecko historical price requests through the server, fixing silent CORS failure that left purchasePriceUsd undefined on all client‑side lookups
  • 14‑day sparkline — /api/price/gun now fetches 14d market_chart alongside 7d sparkline; PriceData type extended with sparkline14d; bootstrap and performance hooks prefer 14d data
  • Portfolio history backwards extension — bootstrapPortfolioHistory can now prepend synthetic points from the sparkline when the sparkline reaches further back than stored history, with 30‑min buffer gap and ~24‑point sampling
  • On‑chain cost extraction for transfers — acquisition pipeline now captures costGunFromChain for cross‑wallet transfers with wGUN payment, calculates USD from historical GUN price
  • Universal GUN→USD fallback — any item with finalPurchasePriceGun > 0 and no USD value gets historical price lookup as a last‑resort conversion
  • Transfer cost basis propagation — NFTDetailModal costBasisGun now falls through to traced original purchase price for TRANSFER acquisitions instead of always returning null
  • Enrichment cache invalidation — marketplace purchases (opensea, in_game_marketplace) with missing price are treated as incomplete and retried on next enrichment cycle
  • USD‑first acquisition card — NFTDetailAcquisitionCard shows $USD as primary line with GUN as secondary when USD is available, for both decode cost and purchase price sections
  • Chart tooltip dynamic positioning — BackdropChart tooltip now renders above the point when point is in lower half of chart, below when in upper half, with hoverY null‑check guard
  • ValueHeader pointer‑events — elements with [title] attribute now receive pointer‑events for native tooltip hover
  • Item origins expansion — new categories early_access, reward, content_pack; Pioneer Set, Player Zero Set, Prankster Set, Anarchist Set reclassified; Going Ape Shit, Hump for Dominance added to Aperil Fools
  • Empty wallet state — portfolios with 0 GUN and 0 NFTs show centered "Nothing Detected" message with inline search bar + Leaderboard/Market CTAs instead of empty $0.00 dashboard
  • CLAUDE.md — documented Production Whitelist API endpoints and admin workflow

v0.2.7

Feb 18, 2026

  • Item origin registry scaled to 35+ releases — added Enforcer BP, Pink Fury BP, Mr Fuckles BP, Hopper Pilot BP, Mad Biker BP, Neotokyo event, Trick Treat or Die event expansion, plus dozens of individual items
  • AIRDROP label — any NFT with a known origin and sub‑1‑GUN acquisition cost now displays "AIRDROP" instead of "0 GUN"; unknown‑origin items keep their raw price
  • Contains match rules — Don DeLulu CP and Mrs Crackhead Santa CP items now use fuzzy‑contains matching to catch blockchain name variations that exact‑match missed
  • ItemRelease description field — releases can now carry rich event descriptions (lore, mechanics, reward details); Trick, Treat or Die is the first with full event metadata
  • Trick, Treat or Die event badge — Halloween items now show the actual event name instead of generic "Halloween"
  • Loading messages refreshed — three new OTG‑themed quips in the portfolio loading rotation
  • Welcome popup streamlined — merged feedback paragraphs, removed redundant bug‑report CTA; single "Got it, let me explore" button

v0.2.6

Feb 18, 2026

  • Search bar validation — invalid addresses (e.g. trailing special characters) now show inline chain detection badge and hint text instead of silently failing; "Go" button properly gates on address validity
  • Wallet actions relocated — Watch and Portfolio buttons moved from search dropdown to the wallet identity bar, where users have context after viewing a wallet’s data
  • Item origin database — curated lookup table mapping NFT items to their release origin (Battle Pass, Content Pack, Event) with 26 releases catalogued
  • Navbar dropdown contrast — darker background and stronger shadow on wallet dropdown so it no longer blends into the page behind it

v0.2.5

Feb 18, 2026

  • Hook extraction — five custom hooks pulled from PortfolioClient (loading messages, chart milestone gating, portfolio snapshots, wallet search actions, multi‑wallet gallery), reducing component from 1,203 to 1,012 lines
  • Lazy‑load below‑fold — ChartInsightsRow and NFTGallery dynamically imported with skeleton placeholders for faster initial paint
  • Accessibility — skip‑to‑content link, aria‑live on metrics grid, ARIA tablist with roving tabIndex and arrow‑key navigation on chart tabs
  • Wallet switch race fix — request ID ref pattern on handleWalletSubmit discards stale responses when rapidly switching wallets
  • Gallery card stagger — first 24 NFT cards fade‑in with translateY stagger animation (30ms grid, 20ms list); cards beyond 24 render instantly

v0.2.4

Feb 17, 2026

  • Cost basis sparkline — dashed white line on the portfolio value chart shows historical cost basis alongside market value, visually revealing unrealized P&L gap
  • Star‑appear animation — chart dots fade in like stars appearing in a night sky as NFTs enrich, growing from tiny pinpoints to full size over ~10 seconds
  • Random dot stagger — new dots appear in randomized order across the chart (Fisher‑Yates shuffle) instead of chronologically, for a more organic night‑sky feel
  • "Under Active Dev" label — moved from chart tab headers into the Insights section for a cleaner chart UI

v0.2.3

Feb 17, 2026

  • Share image redesign — tactical HUD aesthetic OG image with dot‑grid background, corner brackets, gradient accent line, metric cards for GUN balance / NFTs / cost basis
  • Download portfolio image — new button in share dropdown fetches the OG image and saves it as PNG
  • Cost basis in share links — GUN spent on NFTs is now stored in share snapshots and displayed on the OG card
  • Chart zoom fix — zooming no longer pushes dots off‑screen; scales use base width so data positions stay stable
  • Zoom to cursor — Shift+scroll zooms toward the mouse pointer; zoom buttons auto‑scroll to the densest data cluster
  • Gallery performance — React.memo on card components + memoized card data prevents unnecessary re‑renders
  • Search debounce — 200ms debounce on gallery search input eliminates lag from keystroke‑driven re‑filtering
  • content‑visibility: auto on NFT cards — browser skips painting offscreen cards, reducing compositor work on large galleries
  • Enrichment speed — batch delay reduced from 800ms to 200ms, priority window expanded from 12 to 18 above‑fold NFTs

v0.2.2

Feb 17, 2026

  • Chart crossfade — Timeline and Cost‑vs‑Value charts stay mounted simultaneously, opacity crossfade via motion/react eliminates DOM rebuild flicker
  • Aligned chart dimensions — embedded Timeline now matches Scatter plot margins and height so content doesn’t shift during crossfade
  • Multi‑wallet NFT total fix — portfolio wallets that load after the primary wallet now correctly update the NFT count
  • NFT count includes duplicates — gallery item count sums quantities instead of unique token count
  • Holdings card cleanup — removed per‑category GUN amounts, combined Bought+Minted on one row, consistent vertical spacing
  • Navbar wallet dropdown — converted from slide‑out panel to positioned dropdown with spring animation, click‑outside and ESC‑key close
  • Navbar layout fix — three‑group flex prevents wallet address width changes from shifting navigation links
  • Transition demo — interactive comparison of four chart transition styles added to brand page

v0.2.1

Feb 17, 2026 · 6:30 AM EST

  • Spring‑physics animations site‑wide — all panels, modals, drawers, and accordions now use motion/react with consistent spring config
  • Custom green arrow cursor — replaces default pointer everywhere, zero‑lag tracking via direct mousemove transform
  • Drop‑panel close fix — resolved race condition where trigger button click re‑opened panel immediately after click‑outside closed it
  • Active‑state indicators — wallet‑switcher and share trigger icons stay green while their panels are open
  • Navbar layout stability — three‑group flex prevents wallet address from shifting navigation links
  • BreakdownDrawer accordion — smooth height expand/collapse replaces instant show/hide
  • UnlockBanner trust section — spring‑animated height reveal instead of instant toggle
  • WeaponLabDrawer exit animation — drawer now slides out instead of vanishing on close
  • ConnectPromptModal entrance — scale + fade spring animation replaces no‑animation mount

v0.2.0

Feb 16, 2026 · 5:48 AM EST

  • wGUN cost extraction fix — OpenSea offer fills now resolve acquisition cost from ERC‑20 receipt logs
  • Offer fill detection — NFTs acquired via pre‑signed OpenSea offers show "OpenSea (Offer)" as source
  • Hardcoded wGUN contract address as constant — no longer depends on env var for cost extraction

v0.1.9

Feb 16, 2026 · 3:21 AM EST

  • Bundle diet — removed 15 unused dependencies (210 packages), including Nivo, GSAP, tsparticles, force‑graph, reaviz
  • Replaced axios with native fetch across all API services (−30 KB)
  • Replaced framer‑motion with a single CSS keyframe for page transitions (−45 KB)
  • Code‑split 13 heavy components via next/dynamic — charts, modals, debug panels load on demand
  • Deferred PostHog initialization to after hydration for faster first paint
  • Added optimizePackageImports for visx, ethers, posthog‑js — tree‑shakes unused exports
  • AVIF image format enabled site‑wide (20‑50% smaller than WebP on supported browsers)
  • Converted barrel‑file imports to direct imports in key components for better tree‑shaking

v0.1.8

Feb 16, 2026 · 1:10 AM EST

  • Market page — search all active OpenSea listings, drill into individual items with buy links
  • Scarcity upgrades — quality badges, Best Deal sort, price range filter, cross‑links to Market
  • OpenSea listing coverage tripled — fetches up to 3,000 listings (was 1,000)
  • PnL scatter plot redesign — gradient stems, lighter grid, boosted zone labels, legend row, bordered data strip
  • Sqrt‑aware Y‑axis ticks — labels evenly spaced in visual space instead of bunching at the bottom
  • Chart height increase — both charts get more vertical breathing room
  • Fixed zoom height jump — switching between charts no longer causes jarring layout shift
  • Quality metadata pipeline — GunzScan rarity trait extracted and propagated end‑to‑end

v0.1.7

Feb 15, 2026 · 8:23 PM EST

  • Acquisition Timeline — log‑scale Y‑axis with curated tick marks for better dot distribution
  • Data‑driven dot entrance animation — new dots materialize as enrichment discovers them
  • Portfolio sparkline stability — snapshots only record after enrichment completes, eliminating jagged reloads
  • Backdrop sparkline clipping fix — increased top margin and enforced minimum container height
  • Hydration fix — loading text no longer mismatches between server and client renders
  • CSS shorthand/longhand conflict resolved in chart metadata card and insights border
  • Chart zoom no longer inflates container height — fixed‑height chart area with hidden scrollbar
  • ShareDropdown cleanup — removed dead code, simplified conditional logic
  • SimpleMetrics and usePortfolioSummaryData reduced by ~700 lines of dead code
  • PnLScatterPlot simplified — removed unused tooltip state and redundant computations

v0.1.6

Feb 14, 2026 · 8:57 PM EST

  • Social sharing — Share on X, Discord, or copy link with rich OG preview cards showing portfolio value, P&L, and NFT count
  • Valuation waterfall upgrade — per‑item listing > comparable sales median > rarity‑tier floor > cost basis
  • Insights panel expansion — unrealized P&L, most valuable, biggest loss (5 insight types total)
  • Acquisition Timeline chart — interactive visx timeline of NFT purchases by venue and date
  • P&L Scatter Plot promoted to main portfolio view (was insanity‑only)
  • All charts now use full valuation waterfall (listing > comparable > rarity > floor)
  • Chart visual overhaul — sqrt scales, glow effects, gradient zones, smarter axis formatting

v0.1.5

Feb 14, 2026 · 4:05 PM EST

  • NFT valuation waterfall: per‑item listings, rarity‑tier floors, comparable sales medians
  • Dual‑value display — cost basis vs market value side by side
  • Per‑item P&L with visx interactive charts
  • Feature request system with community voting, bug reports, and screenshot attachments
  • Collapsible request cards with lightbox image viewer
  • UXR welcome popup for new testers with onboarding guidance
  • Crosshair cursor performance: removed backdrop‑blur from overlays, cached DOM walks, targeted cursor rules
  • Display name support for wallet profiles
  • Portfolio history bootstrap with sparkline seeding
  • Hybrid portfolio: aggregated summary + per‑wallet gallery with SWITCH
  • Read‑only portfolio access via ?address= param — browse any wallet without logging in
  • Migrate from SQLite to Neon PostgreSQL — full read/write in production

v0.1.4

Feb 13, 2026 · 4:56 AM EST

  • Scramble‑decode loading text matching home hero animation
  • 10pm Easter egg — because someone had to
  • NFT Holdings sparkline toggle on first wallet search
  • Server‑side RPC proxy for reliable production wallet loading
  • View transitions with framer‑motion page animations
  • Wallet address help panel for new users
  • Auto‑populate credits from completed feature requests

v0.1.3

Feb 12, 2026 · 4:35 PM EST

  • NFT sparkline toggle with historical hover counts
  • Dynamic Labs SDK upgrade (4.59.1 → 4.61.2)
  • Crosshair cursor performance fix
  • UX polish: onboarding flow, nav, login gate, multi‑admin
  • Grouped NFT visual overhaul: dynamic rarity accents, mergeIntoGroups
  • Decode cost extraction fix for relayer‑submitted transactions

v0.1.2

Feb 11, 2026 · 10:37 PM EST

  • GunzScan API migration with infinite scroll
  • Ambient backdrop sparkline with smooth curves and overlay toggles
  • Auto‑load portfolio on wallet connect
  • Component decomposition: Navbar, PortfolioSummaryBar, scarcity, feature‑requests
  • Wallet dropdown enhancements + identity bar refactor
  • SEO metadata for all pages
  • Standardized API response types

v0.1.1

Feb 10, 2026 · 11:47 PM EST

  • Confidence indicator overhaul with enrichment reliability fixes
  • Insanity Mode toggle + clip‑corner card design
  • Sticky accent lines and container transparency polish
  • Email auth flow + adaptive onboarding
  • Scarcity page UX improvements
  • Disconnect UX and network switch visibility fixes

v0.1.0

Feb 9, 2026 · 7:20 PM EST

  • Public feature request and management system
  • Dynamic wallet onboarding with styled connect flow
  • Redesigned footer with social links
  • Leaderboard page with access gate and active wallet display
  • Nav glitch effect + gallery refactor

v0.0.3

Feb 5–8, 2026

  • NFTDetailModal decomposition (3,163 → 1,069 lines via 4 extracted hooks)
  • Portfolio context + hooks architecture refactor
  • useWalletDataFetcher, useNFTEnrichmentOrchestrator, useWalletAggregation hooks
  • WaffleChart composition visualization with stagger animation
  • Marketplace price enrichment pipeline
  • Portfolio three‑section layout with Simple/Detailed toggle

v0.0.2

Jan 31 – Feb 1, 2026

  • NFT P&L pipeline with historical prices, rarity floors, and comparable sales
  • Interactive rarity filter pills in NFT gallery
  • YOUR POSITION section in NFT detail modal
  • Floor price enrichment + metadata caching
  • Security vulnerability fixes (31 → 9)
  • Functional tier support from raw metadata
  • Native GUN balance fetch fix

v0.0.1

Genesis

Jan 19–22, 2026

  • Initial release — GUNZscope is born
  • Multi‑chain portfolio tracker for Off The Grid
  • NFT Armory/Lab feature with weapon compatibility
  • Acquisition truth layer using RPC‑only fingerprints
  • Progressive accounts implementation
  • OpenSea + in‑game marketplace data integration

Built for the Off The Grid community · Not affiliated with GUNZILLA Games

© 2026 GUNZscope
·Terms·Privacy·Credits·API·Mint Check··

Built with ♥ by CRYPTOHAKI for the Gunzilla community.