Skip to content

PackX402 beta: core data model, fairness engine, x402 payments, CardTrader adapter, auth, docs - #1

Draft
The-Daly wants to merge 21 commits into
mainfrom
beta/initial-packx402-build
Draft

PackX402 beta: core data model, fairness engine, x402 payments, CardTrader adapter, auth, docs#1
The-Daly wants to merge 21 commits into
mainfrom
beta/initial-packx402-build

Conversation

@The-Daly

@The-Daly The-Daly commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Summary

Initial beta scaffold for PackX402 — a provably fair, supplier-backed trading-card pack
platform powered by Algorand x402. This PR lays the full data model and the core
safety-critical business logic (fairness selection, payment settlement, purchase limits,
loyalty, eligibility, wallet-signature auth), plus a handful of real UI pages, and is
honest in PROJECT_STATUS.md about what's tested vs. scaffolded vs. not started.

No Docker/PostgreSQL and no live third-party credentials (CardTrader, GoPlausible) were
available in the build environment.
Everything verifiable without those — strict
TypeScript against the full Drizzle schema, next build, and 77 Vitest tests — passes.
See PROJECT_STATUS.md for the exact ✅/🟡/⬜ breakdown; do not trust marketing language
elsewhere in the repo over that file.

What's implemented and tested

  • Full 59-table Drizzle/Postgres schema covering every entity in the product spec
  • Pack tier config (Spark–Genesis) with server-side network/value gating
  • Deterministic provably-fair engine (src/server/fairness/engine.ts) with a fixed,
    hand-computed test vector published in docs/FAIRNESS_PROTOCOL.md
  • AES-256-GCM field encryption, tamper-tested
  • Responsible-purchasing limit evaluation (self-exclusion, cool-off, pause, daily/weekly/
    monthly limits, immediate-decrease/delayed-increase rule)
  • Supplier-listing eligibility rules (spec section 38)
  • CardTrader mock SupplierAdapter — idempotent purchases, cart-safety abort on
    unexpected non-empty cart
  • Loyalty-level calculation with a Silver beta reward cap
  • ISO week-key logic for the one-free-pack-per-week rule
  • Age-gate/eligibility policy evaluation
  • Real per-chain wallet-signature verification for Algorand (algosdk), Solana
    (tweetnacl), and EVM (viem) — each verified against an actual generated keypair signing
    and verifying a real message, with negative tests
  • Landing page, pack marketplace, pack detail, provably-fair verifier (with a working
    /api/fairness/verify endpoint), odds library + JSON download
  • Global security headers (CSP/HSTS/etc.), CSRF double-submit cookie, Redis rate-limit
    helper
  • npm run build succeeds; tsc --noEmit is clean

Wallet integrations

Signature verification logic is implemented and tested for all three chains. The actual
connect UI (@txnlab/use-wallet-react for Algorand, @phantom/react-sdk for Solana/EVM)
is not built yet.

x402 integration status

/api/x402/algorand/v1/packs/open returns HTTP 402 with PaymentRequirements and, given
a valid X-PAYMENT header, verifies/settles and runs the fairness pipeline end to end
(src/server/packs/offer-service.ts). Solana/EVM adapters exist with the same shape.
Mock mode is the default and only tested path. Live mode does direct chain
verification (algod / web3.js / viem) rather than calling the GoPlausible facilitator's
HTTP API, since no facilitator credentials were available to verify that contract — see
docs/DECISIONS.md.

Supplier-integration status

CardTrader SupplierAdapter implemented for both mock (tested) and live (unverified, no
API token available) modes, per the documented v2 API shape with
via_cardtrader_zero=false. The serialized one-purchase-at-a-time worker described in
spec section 39 is not implemented — offers only enqueue a supplier_purchases row
today; nothing consumes the queue yet. Top follow-up item.

Social and affiliate features

Data model only. No application code, routes, or UI. See docs/SOCIAL_MODERATION.md and
docs/AFFILIATE_PROGRAM.md.

Weekly free-pack and loyalty systems

Pure calculation logic implemented and tested (loyalty level + beta cap, ISO week key,
free-pack-grant DB uniqueness). No claim API route or recalculation job exists yet. See
docs/LOYALTY_AND_FREE_PACKS.md.

Security controls

No custodial wallets/private-key storage anywhere in the schema. Integer USDC base units
everywhere (no float money math). Math.random never used for card selection. High-value
tiers and MainNet are gated server-side by env flags checked at both boot
(src/server/env.ts) and per-request (createPackOffer). Session tokens are opaque and
stored only as a sha256 hash. See SECURITY.md and docs/THREAT_MODEL.md.

Tests performed

  • 77 Vitest unit/integration tests, all passing (npm run test)
  • Strict tsc --noEmit, clean
  • npm run build (Next.js 16 + Turbopack), clean
  • npm run lint (ESLint), clean
  • npx prettier --check ., clean
  • Not performed: any test against a live PostgreSQL database, live Redis, or live
    third-party credential (none were available in the build environment) — flagged
    explicitly everywhere it matters rather than claimed as done. Playwright e2e tests are
    not written yet.

Known limitations

See PROJECT_STATUS.md for the full list. Headline items: no /api/auth/* route
handlers or auth UI yet (the underlying logic is tested), no opening-theater/purchase-
confirmation UI, no supplier-purchase worker, no admin dashboard, no social/affiliate/
referral application code, migrations generated but not yet applied to a live database in
this environment.

Credentials or external configuration still required

  • CardTrader API token (for CARDTRADER_MODE=live)
  • GoPlausible/x402 facilitator credentials (for X402_FACILITATOR_MODE=live) — or accept
    the direct-chain-verification approach already implemented as the long-term design
  • A provisioned Postgres + Redis for any environment beyond local Docker Compose
  • SESSION_SECRET / FIELD_ENCRYPTION_KEY generated fresh per environment

Legal-review items

See docs/LEGAL_REVIEW_REQUIRED.md in full. Headline items: consumer-protection review
before enabling high-value packs or MainNet, IP/trademark counsel sign-off on card-name
usage and pack artwork, responsible-purchasing/gambling-adjacent regulation review,
data-protection review of docs/PRIVACY_DATA_MAP.md (including the currently-unencrypted
eligibility_records.dateOfBirth field flagged there).

Exact deployment steps

See docs/DEPLOYMENT.md. Local: npm install && docker compose up -d && npm run db:migrate && npm run db:seed && npm run dev.

Project rename note

Renamed from "Pack402" to "PackX402" per the repository owner's direction partway through
this build — reflected in the package name, branding, docs, and this repo's name/URL.


🤖 Generated with Claude Code

Session update (2026-08-01)

Since the original PR description was written, this branch gained:

  • /api/auth/* route handlers wired up on top of the already-tested
    auth-service.ts: signup, login (request/verify), logout, wallet nonce/verify,
    session listing, session revoke-all, verify-email. Auth UI (forms) still not built.
  • PackArt visual component system (src/components/pack-art/): PackArt,
    PackArtSkeleton, CardBack, CardOverlaySlot, OpeningStage, FourPackStage,
    ResultEffect, PackCarousel — wired into the landing, marketplace, and pack-detail
    pages. PackCarousel is an infinite circular strip with drag/swipe + flick-velocity
    scrolling, no dot pagination.
  • Real generated pack artwork installed for 10 of 14 tiers at public/packs/*.png
    (Higgsfield-generated, dark-glass pack + X-crest motif, per-tier metallic accent).
    Remaining 4 locked tiers (Crown/Vault/Grail/Genesis) are still CSS placeholders.
    A follow-up regeneration pass is planned to push the art style further toward a
    toon/cel-shaded look
    — the current set leans photoreal-luxury; an earlier
    toon-style exploration was well-received and will become the new template.
  • Tier-lock threshold changed from $25 to $250 (src/server/config/pack-tiers.ts):
    everything above $250 (Crown/Vault/Grail/Genesis) is locked pending supplier-
    fulfillment bankroll; Prism–Mythic availability extended accordingly. Docs updated to
    match (README, docs/LEGAL_REVIEW_REQUIRED.md, .env.example).
  • Fixed a real dev-mode bug: src/proxy.ts's CSP was blocking eval(), which broke
    Next.js HMR entirely — now only relaxed in development, unchanged (strict) in
    production.
  • src/server/db/seed.ts rewritten to generically build a pool (commons + one chase
    card) for all 14 tiers from an expanded fixture ladder in
    src/server/suppliers/cardtrader/fixtures.ts (20 fixtures, ~$0.30–$9,500) — every tier
    now seeds correctly, not just Spark/Gold as before.

Verified this session: npm run typecheck, npm run lint, npm run test (77/77
passing) all clean. npm run build was not re-run against this exact commit in this
session (was previously verified clean earlier in the branch's history); recommend a
human re-run before merge given the size of the diff.

Still not started: opening-theater/purchase-confirmation UI, supplier-purchase worker,
admin dashboard, social/affiliate application code, live-DB verification (no
Docker/Postgres in this build environment). See PROJECT_STATUS.md for the current
full ✅/🟡/⬜ breakdown.

The-Daly and others added 21 commits August 1, 2026 19:34
…2 payments, CardTrader adapter, auth, docs, CI

- Full 59-table Drizzle/Postgres schema covering every spec entity
- Deterministic provably-fair selection engine with a published, hand-verified test vector
- x402 Algorand/Solana/EVM payment adapters (mock-default; live paths via direct chain verification)
- CardTrader SupplierAdapter (mock + live), idempotent purchases, cart-safety checks
- Auth primitives: hashed sessions, SIWE-style wallet messages, real per-chain signature
  verification (Algorand/Solana/EVM) each tested against a generated keypair
- Server-side age-gate/eligibility policy, responsible-purchasing limits, loyalty calc
- Landing, marketplace, pack-detail, provably-fair verifier, and odds-library pages
- Global security headers, CSRF double-submit cookie, Redis rate-limit helper
- 77 passing Vitest tests, clean strict typecheck, clean production build
- Full required documentation set, GitHub Actions CI, issue/PR templates, CODEOWNERS
- Renamed project from Pack402 to PackX402 per user request

See PROJECT_STATUS.md for the authoritative implemented/unverified/not-started split.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wires the tested auth-service (signup/login/wallet-verify/session logic)
into /api/auth/* route handlers, introduces the PackArt component system
(PackArt, PackCarousel, OpeningStage, FourPackStage, ResultEffect, CardBack)
wired into the landing/marketplace/pack-detail pages, installs real
Higgsfield-generated art for 10 unlocked tiers, relaxes CSP eval() only in
dev to unbreak Next.js HMR, and moves the tier-lock threshold from $25 to
$250 to match current supplier-fulfillment bankroll.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Regenerates all 10 unlocked tier packs in a cartoon/Pokemon-style (cel-shaded,
starburst crest, "PackX402" wordmark, no gold, transparent cutouts for 3D
interactions). Adds a real opening-theater flow at /packs/[tierKey]/open:
browse via a 3D-perspective carousel, drag-to-rip the pack open (RipToOpen),
watch the card-reveal wheel spin down to the winning card (CardRevealWheel),
with an occasional low-probability cosmetic coin-flip flourish (CoinFlip) that
never changes the real outcome or its odds. Wires the real x402 endpoint
end-to-end; since no wallet-connect UI exists yet, the real payment
requirement is shown honestly rather than faking a purchase.

Also adds src/server/card-images/resolver.ts (the documented
supplier-photo -> PSA -> CardTrader -> public-catalog -> fallback priority
chain, with a domain-allowlist/SSRF guard, tested), replaces the flat 1.15x
procurement price cap with a tapering per-tier max-obtainable-value
multiplier (Spark $0.50 -> $25 cap down to Genesis's 2x), and surfaces
per-card reference values plus a max-obtainable-value summary on the
pack-detail page.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Removes passwordless-email signup/login entirely (routes and auth-service
functions) per product decision: Google sign-in (Auth.js/NextAuth v5, mounted
at /api/oauth to avoid colliding with the existing wallet session routes) and
direct wallet signature are now the only two ways into a PackX402 account.
Adds an oauthIdentities table keyed by Google's stable subject id, and closes
a real pre-existing gap: createPackOffer() previously never checked
eligibility at all for any auth method; it now rejects any offer for a user
with no passing age/location eligibility record, with a new
POST /api/auth/oauth/complete-eligibility endpoint to satisfy it.

CardImageResolver's public-catalog provider now makes real live calls to the
Pokemon TCG API and YGOPRODeck (both free/keyless) instead of always
returning null, resolving actual card images by name for both games -
confirmed working from this environment. Tests updated to mock fetch for
determinism.

Fixes the drag-to-rip gesture: switches from a vertical drag to a horizontal
drag-across-the-top gesture with a visible handle, adds a synthesized tear
sound (Web Audio API, no asset file) and a sparkle burst along the seam, and
corrects the tear seam position from a 50/50 split down the middle to a
small strip near the top like a real foil pack. Restructures the open-pack
page so the rip/reveal stage sits above the carousel and only appears after
a pack is explicitly selected (tap), not just scrolled past. Adds a
no-database /dev/rip-preview page for demoing the animation without Postgres.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CardRevealWheel now cycles through actual possible pulls from the pack's
pool during the spin (via a new resolveCardImages batch resolver and
/api/packs/[tierKey]/spin-preview endpoint) instead of blank card backs -
confirmed live against the real Pokemon TCG API. Required adding the image
hosts to next.config.ts's remotePatterns (next/image refused to load them
otherwise - a real bug caught while verifying in-browser).

Replaces the open-pack page's infinite carousel with PackShelf: uniform-size
packs in a horizontally scrollable row, sorted ascending by price left to
right, each with its own pay button. Adds PaymentMethodPanel showing wallet
(the one real, functional method), plus Apple Pay and PayPal rendered
realistically but disabled - neither has a real merchant credential
configured, so they're documented as not-yet-live rather than faked as
working, matching this repo's existing pattern for CardTrader/x402 live mode.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two real bugs from user testing: the drag surface was a thin handle strip
pinned to the top instead of the whole pack, and the torn-off top piece used
a straight inset() clip-path — since the pack's "PackX402" wordmark sits
below the actual foil seam, cutting a clean horizontal line left the full
wordmark visible on both the flying-away piece and the stationary pack
underneath, reading as a duplicated top.

Replaces the straight cut with a shared zigzag polygon clip-path so both
pieces interlock along a genuinely torn-looking boundary with no overlap,
and moves the drag handlers onto the entire pack area (the handle graphic
at the top is now a decorative hint only, not the sole interactive target).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reverses the earlier "cosmetic only" coin-flip decision per explicit user
request: a fixed 4% chance, evaluated server-side on every completed pack
opening from the SAME committed fairness seed as the primary pull (never
client-side Math.random), of awarding a second real card from the same pool.
Both the hit/miss determination and the bonus card pick are independently
verifiable exactly like the primary pull, via domain-separated derivations
(deriveBonusFlipHit/selectBonusPoolEntry in src/server/fairness/engine.ts).

Schema: rips.packOfferId/paymentId are no longer individually unique - a
"primary" and a "bonus_flip" rip can now both exist against the same paid
offer (nothing is paid twice), enforced instead by a (packOfferId, kind)
composite unique index. CoinFlip.tsx now animates a server-determined result
instead of generating its own random flip. The x402 open endpoint returns
the real bonusFlip outcome; OpenPackClient and the no-DB dev preview both
wired to show the second card on a hit.

Flags the real EV/odds impact in docs/LEGAL_REVIEW_REQUIRED.md and documents
the mechanic in docs/FAIRNESS_PROTOCOL.md - not yet reflected in the
published odds table, which still needs updating before this ships publicly.

Also replaces the open-pack page's carousel with PackShelf (uniform-size
packs, ascending price left to right, per-pack pay button) built as
structural inspiration from a reference app's UI patterns (not its branding),
per user direction not to copy it directly - explicitly excludes that
reference's real-money cash-out mechanic, which conflicts with PackX402's
documented "physical cards only, not an investment platform" positioning.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Applies a fixed odds table (Common 25% / Uncommon 24.8% / Rare 16.1% /
Epic 16.1% / Legendary 14% / Grail 4%, summing to exactly 100.00%) uniformly
across all 14 pack tiers, with each rarity's price band scaled
proportionally to that tier's own price - e.g. a $5 pack's Grail band is
$9-$100 (20x), matching the user-specified example exactly (verified by
test). The max-obtainable-value cap now derives directly from the Grail
band's upper bound, superseding the earlier separate tapering-multiplier
schedule. Extracted into src/server/packs/rarity-bands.ts as pure, testable
logic rather than buried in the seed script; db:seed rewritten to pick one
representative fixture per rarity band from the mock CardTrader ladder.

Also adds GET /api/packs/openings, a user-scoped pack-opening history
endpoint (only the authenticated user's own rips, via packOffers.userId) -
deliberately separate from /api/fairness/verify, which stays
public-by-ripId on purpose since that's what makes a proof independently
verifiable by any third party, not just the pack's owner.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fixes the header's "Log in"/"Get started" links, which pointed to /login
and /signup - pages that never existed (a pre-existing gap, not something
this session introduced) - by wiring in the real Google sign-in button and
a working logout button. Auth state is checked client-side via
/api/auth/session after mount rather than in the root layout via cookies():
the latter was tried first and forced every single page in the app to
render dynamically instead of statically, a real regression caught by
comparing build output before/after.

Adds /collection, a simple page listing the signed-in user's own pack
openings (card, set, tier, value, date, a link into the fairness verifier),
consuming the user-scoped GET /api/packs/openings endpoint added earlier.
Also teaches the fairness verifier form to read a ?ripId= query param and
auto-verify, so the "Verify" link from /collection actually works instead
of just prefilling the page.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Closes the gap flagged in docs/LEGAL_REVIEW_REQUIRED.md: the 4% bonus-flip
chance changes every tier's effective EV/odds but wasn't shown anywhere a
buyer would see it before purchasing. Adds a one-line disclosure ("Bonus
flip: 4% chance of a second card on every opening") next to the existing
max-obtainable-value figure. Updates the legal-review note and
PROJECT_STATUS.md to reflect that this is now disclosed, while making clear
disclosure isn't the same as legal clearance - counsel still needs to weigh
in on whether a disclosed "extra" reward changes the gambling-regulation
analysis.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Installs @txnlab/use-wallet-react + @txnlab/use-wallet's full real peer
dependency set (@perawallet/connect, @blockshake/defly-connect,
@walletconnect/modal + sign-client, @agoralabs-sh/avm-web-provider,
lute-connect) - use-wallet bundles dynamic imports for every connector it
supports regardless of which ones WalletManager is actually configured
with, so the build fails without them installed even though only Pera is
used (WalletManager is constructed with wallets: [WalletId.PERA] only).

Adds WalletManagerProvider (TestNet-only, wraps the app), ConnectPeraButton
(connect/disconnect, shortened address display), and PayWithWalletButton -
the real second half of the x402 flow: builds an algosdk ASA-transfer
transaction from the server's own PaymentRequirements (never a
client-invented amount/recipient), signs it via the connected wallet's
transactionSigner, submits to algod, waits for confirmation, and re-POSTs
the open endpoint with a real X-PAYMENT header matching
algorand-adapter.ts's decodePaymentHeader contract. Wired into
PaymentMethodPanel/OpenPackClient, replacing the "wallet-connect UI doesn't
exist yet" placeholder.

Confirmed in-browser: the connect button renders and initiates a real Pera
handshake with no console/bundler errors. Not verified: an actual
connect-sign-submit-settle click-through, since no funded TestNet Pera
wallet was available in this environment - same "implemented against the
documented contract, unverified live" status as the rest of this repo's
live payment paths.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extracts the carousel-select -> rip -> spin -> reveal -> bonus-flip demo
into a shared InteractivePackDemo component and places it front-and-center
on the homepage alongside a plain-language walkthrough of the commit-reveal
fairness algorithm, so visitors can try the mechanic before connecting a
wallet or paying. Same component now backs both the landing page and the
existing /dev/rip-preview page.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Replaces the ~20-card hand-authored mock CardTrader inventory with 110
  real cards (90 Pokemon + 20 Yu-Gi-Oh) pulled live from api.pokemontcg.io
  and db.ygoprodeck.com, each carrying its real current market price
  (2026-08-02 snapshot, $0.08-$1,300). CardTrader itself stays mock mode.
- Adds a real Higgsfield torn-pack render for 4 tiers (Spark, Scout,
  Obsidian, Mythic), shown briefly during the rip gesture's "tearing"
  phase before the reveal wheel spins. PackArt's new `torn` prop falls
  back to the closed-pack art for tiers without one.
- Switches the rip gesture's clip-path (and the torn art itself) from a
  jagged zigzag tear to a single clean straight seam line, per feedback.
- Confirms the bonus-flip coin animation never rolls its own odds (it
  only animates the server's real 4% result) - only the demo intentionally
  uses a higher rate, clearly commented as such.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Coin-flip UI now only renders when the bonus flip actually hits (4%
  server-determined chance) - it previously rendered on every spin and
  merely animated to a "miss" outcome the other 96% of the time.
- The torn-pack art phase now has a guaranteed minimum visible duration
  (900ms real flow, 1100ms demo) instead of being purely fetch-latency-
  dependent, so it doesn't flash past unnoticed on a fast connection, and
  now also appears on the deferred-payment settle path which previously
  skipped straight to the reveal wheel without showing it at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- New /eligibility page + EligibilityForm collect DOB/country/18+
  acknowledgment via the existing complete-eligibility endpoint (now
  CSRF-protected, matching its sibling mutating routes). OpenPackClient
  links here automatically on an "eligibility_required" rejection.
- offer-service.ts's getRollingSpend() replaces the "passes zeros" stub
  with a real query summing settled payments over rolling 24h/7d/30d
  windows, feeding the existing (already-tested) limit-decision logic.
- New supplier-purchase queue worker (src/server/suppliers/purchase-
  worker.ts + run-worker.ts, `npm run worker:supplier-purchases`): a real
  long-running process, not a request handler, that claims queued rows
  (race-safe via a conditional UPDATE), runs validate/add-to-cart/confirm/
  purchase against the CardTrader adapter, and records the outcome.
  Documented, not silently solved: no cross-process lock for multiple
  workers on one supplier account, no substitution-search-on-unavailable
  (no configured price tolerance exists to drive it), and it fails clearly
  when a user has no shipping address on file (no UI for that yet).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New /shipping-addresses page + CRUD API (GET/POST /api/shipping-addresses,
DELETE and set-default on /api/shipping-addresses/:id) so a user can add
the address their pulled cards ship to - the exact gap the new
supplier-purchase worker fails clearly on (no_shipping_address_on_file).
Every free-text field is encrypted at rest via the existing encryptField/
decryptField helpers; first address added is auto-default; deleting the
default promotes the next-oldest one. All mutating routes are
CSRF-protected. Linked to from /collection. Also factors the country/US-
state lists shared with EligibilityForm into src/shared/countries.ts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New /account page (linked from the header once signed in): read-only
connected-wallets list (new GET /api/auth/wallet/list, scoped to the
session's own userId), active sessions with a real "sign out everywhere"
control wired to the existing sessions API, and links to the other
account-scoped pages (openings, shipping addresses, eligibility,
responsible purchasing). Linking an *additional* wallet to an
already-authenticated account isn't wired up yet - documented as a
follow-up rather than half-built.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Full redesign of all 10 unlocked tiers to one standardized template:
  a glowing P+X vault-arc emblem in a corner-bracket frame, tier name in
  a bottom pill badge, no "PackX402" wordmark on the pack face at all.
  Only material/color and a tier-specific background motif differ
  (lightning, sunburst, radar lines, art-deco fan, diamond facets,
  holographic rays, frost facets, cracked glass, cosmic nebula). Iterated
  through several rounds of real feedback - corner checkmarks removed,
  wordmark font/position issues resolved by dropping the wordmark
  entirely rather than continuing to fight image-gen text rendering.
- Real video-driven rip animation for Spark: RipToOpenVideo.tsx maps the
  user's drag position directly to video.currentTime on an actual
  Higgsfield-generated tear-open video (kling3_0 image-to-video
  interpolation between a closed still and a straight-line-torn still),
  replacing the CSS clip-path illusion for this tier. Wired into both the
  real opening theater and the no-DB demo via a shared tier->asset map
  (rip-video-map.ts) so it's a per-tier rollout, not all-or-nothing - the
  other 9 tiers still use the existing CSS rip until they get the same
  video treatment.
- Verified live: dragging scrubs the video frame-by-frame, releasing past
  the commit threshold plays it through, then the reveal wheel spins.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds Starter, Scout, Bronze, Silver, Gold, and Prism to the tiers with a
real Higgsfield-generated tear-open video (kling3_0 interpolation between
closed/torn stills), joining Spark. Wired via the existing
RIP_VIDEO_BY_TIER map - no code changes needed beyond the new entries.

Starter and Bronze needed a corrected still-generation prompt (the tear
initially landed partway down the pack instead of as a thin strip near
the top zigzag seam, per user-caught feedback) before their videos were
generated. Platinum, Obsidian, and Mythic still need the same treatment -
generation stopped when the Higgsfield workspace ran out of credits.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
TESTNET_CEILING and MAINNET_CEILING now cover just Spark and Starter -
every other tier (Scout through Genesis) is locked server-side via
isTierPurchasableOn()/offer-service.ts, not just hidden in the UI. This
narrows the beta catalog to the two tiers that already have the real
video-driven rip-open treatment, rather than shipping a mix of
video-tier and CSS-clip-path-tier packs while Higgsfield credits are
depleted. The landing-page/dev-preview demo now only features these two
tiers as well, matching real availability.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ty fix

- Fixture ladder (fixtures.ts) grows from 110 to 200 real cards: 180
  Pokemon sampled from 799 real candidates across 9 real sets (previously
  just 3), biased toward the cheap end so Spark/Starter's rarity bands
  have genuine in-band matches instead of leaning on fallback selection.
- pickFixtureForBand (rarity-bands.ts) now takes a hard
  absoluteMaxUsdcBaseUnits cap, enforced before both the in-band search
  and the closest-match fallback. Previously the fallback had no ceiling
  of its own - a cheap tier with no in-band candidate could theoretically
  land an arbitrarily expensive fixture. db:seed now passes each tier's
  own procurementPriceCapUsdcBaseUnits as that cap.
- Verified by simulating the actual pool selection against the live
  fixture ladder: Spark's ($0.50) max obtainable card is $5.47, Starter's
  ($1) is $10.90 - both comfortably under their $10/$20 caps, with every
  rarity band landing a real in-band fixture rather than a fallback pick.
- Two new regression tests in rarity-bands.test.ts lock in the cap
  behavior (105/105 tests passing). Pool-version label bumped to
  2026-08-02.3.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant