CCWork
CC
Email me

Work

SwishX

A marketplace for sealed packs of graded sports cards, built through Lazer Technologies in 2026.

Role
Sole architect and builder
When
Feb–Jun 2026
Client
SwishX, via Lazer Technologies
Stack
TypeScript, Next.js 16, React 19, NestJS 11, Drizzle, Postgres, and 11 more
Status
Live ↗
swishx.co
SwishX marketplace home page: the "Your X-Factor Awaits" hero with graded cards, above the Browse Packs row of sealed packs
The consumer home page on swishx.co.

Context

A buyer funds a wallet, opens a pack in a WebGL reveal, and gets a real graded card in their vault. From there they can keep it, sell it back, or have the slab shipped to them.

I built it through Lazer Technologies between February and June 2026. Under the one product there's a custodial wallet, an inventory that has to hold up when a new drop sells fast, a shipping pipeline and a fairly heavy animation, and every purchase touches all of them.

Each card exists once and the balances are customer money, so most of the design work went into making sure a balance couldn't drift and a card couldn't be sold to two people.

What I built

I built the Next.js consumer app, the React admin, the NestJS API on Cloud Run, the Firebase functions, and the GCP infrastructure under them.

  • A double-entry ledger in Postgres. Every economic action is a posting, and balances are a projection updated in the same transaction, so reading a balance is one indexed lookup.
  • The purchase path. One database transaction validates the pack, draws the card, claims the slot, debits the wallet hold and writes the order, and an atomic Redis Lua script in front of it enforces per-user limits. When two buyers race for the last card, one gets it and the other sees "sold out".
  • A pack-odds engine that seeds each pull from the purchase and stamps the engine version on it, so any past pull can be replayed exactly.
  • Catalog ingestion that scrapes graded cards and reads each slab label with GPT-4o Vision.
  • The pack-opening animation, in Pixi.js, Three.js and GSAP. It checks the device and network before it picks particle counts, blur passes and resolution, so the same reveal runs on a small phone and a desktop. No React state update fires inside an animation frame.
  • Shipping through Shippo. Buying a label takes a compare-and-set lock and writes a record before it calls the carrier, so a label is never paid for twice or lost.
  • Deploys from GitHub to GCP through Workload Identity Federation, with no long-lived keys. Backend-only changes deploy on their own path, which took a backend deploy from about an hour to 15 to 20 minutes.

Two buyers, one card

Press Race to send two buyers for the last card.

Checkout

Buyer A

Order confirmed

Cards left

−1

Row unlocked

Buyer B

Order confirmed, but the card is gone

  1. A Reads the stockSELECT stock → 1Cards left: 1
  2. B Reads the stockSELECT stock → 1Cards left: 1
  3. A Sees one left, takes payment1 > 0 ✓ · chargeCards left: 1
  4. B Also sees one left, takes payment1 > 0 ✓ · chargeCards left: 1
  5. A Order confirmedUPDATE stock = stock − 1 → 0Cards left: 0
  6. B Order confirmed, but the card is goneUPDATE stock = stock − 1 → −1Cards left: −1

Oversold: two paid orders for one card, stock −1.

A generic demo of the race a marketplace checkout has to handle, not client code.

Open the demo on its own page

Architecture

SwishX architecture. One API sits in front of Firestore and Postgres, and events and pipelines run on their own workers.
  1. The buyer signs in with Privy. The API verifies that token and issues an HttpOnly session cookie.
  2. Browser traffic goes through a same-origin proxy to the NestJS API on Cloud Run.
  3. Product state (the catalog, vaults and users) lives in Firestore. All money lives in Postgres, in the ledger.
  4. A purchase touches both stores. A transactional outbox and reconciliation sweepers keep them in step.
  5. Stripe webhooks are verified, deduplicated in Redis and published to Pub/Sub. A dedicated worker drains them, with a dead-letter collection behind it.
  6. Catalog ingestion reads each slab label with GPT-4o Vision and queues low-confidence reads for a person.
  7. Fulfillment runs through Shippo, with its own worker and a tracking state machine that only moves forward.
SwishX "Rare Air" basketball pack: a red foil wrapper with a player silhouette wearing number 23
A sealed pack, the thing the reveal tears open. Art: SwishX.

Decisions

The ledger and the product data live in different databases, and there's no two-phase commit between them. A purchase writes to a transactional outbox, and sweepers that claim rows with FOR UPDATE SKIP LOCKED post each money movement to the ledger exactly once. We didn't need a saga framework for it.

Ledger rules

The arithmetic rules are CHECK constraints, and the postings table has a BEFORE UPDATE OR DELETE trigger that rejects the change outright. A new endpoint can't skip a rule the database enforces. When something is wrong, the fix is a new reversing entry, and a partial unique index stops the same entry from being reversed twice.

Webhooks

Stripe events pass through Redis deduplication, a Postgres inbox and handlers that are idempotent per effect, with the platform's dead-letter queue behind them. None of it runs inside a user's request. Replaying a webhook doesn't charge or credit anyone twice.

A PSA-graded Freddie Freeman Topps Opening Day rookie card in its clear plastic slab
What lands in the vault after a pull. Image: SwishX.

What went wrong

GPT-4o Vision was confidently wrong about slab labels whenever there was glare or an odd angle. I started handling its output like any other untrusted input. Every read is checked against a schema, a malformed answer gets one repair attempt, and anything under the confidence gate goes to a person, because a wrong grade would misprice the card.

We had a double-processing bug on Stripe webhooks, caused by CPU throttling. Moving webhooks onto their own always-on worker fixed it.

The odds engine started with a 32-bit seed. When I ran the birthday-paradox numbers for the purchase volumes we were planning for, about 0.29 collisions were expected at 50,000 purchases, which was too many. I moved it to a 64-bit seed:

seed = SHA-256(purchaseId : packId : timestamp : nonce)
rng  = SplitMix64(seed)
pull = a weighted tier, then a uniform card within that tier

At 200,000 purchases the expected count is about zero. Each purchase carries the engine version, so pulls made on the old seed still replay without a data migration.

The last-card demo above shows the same race.

Screenshots

  • swishx.co
    SwishX home page further down: pack prices, a two-step "How it works" (purchase packs, then collect or sell back) and top weekly pulls
    How it works, on the SwishX home page, Aug 2026.
  • SwishX recent pulls feed: graded cards pulled minutes ago, each with its pack tier; usernames blurred
    Recent pulls feed on swishx.co, Aug 2026. Usernames blurred.
  • SwishX Gold pack: a gold foil wrapper with the SwishX wordmark
    Pack art for the Gold tier. Art: SwishX.

My part

About 98% of the commits are mine, and the rest came from other contributors on the engagement. The screenshots show swishx.co as it was live in 2026. The graded cards and pack art in them belong to their owners and are shown with SwishX's permission.

Numbers

FigureWhat it measures
~200Klines of TypeScript across three apps, the functions service and shared packages
122test files (Vitest, Playwright end-to-end, Firebase security-rules tests)
145flow documents in the structured manual-QA framework
27 / 627 atomic RBAC permissions composed into 6 built-in roles
~1 h → ~15–20 minbackend deploy time after path-scoped deploys
≈0.29 → ≈0expected seed collisions after the move from a 32-bit to a 64-bit seed (at 50K and 200K purchases)
50–100/spurchases on a hot drop with zero oversell, in load tests (not production traffic)
Questions