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 ↗
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.
Buyer A
Order confirmed
Cards left
−1
Row unlocked
Buyer B
Order confirmed, but the card is gone
- A Reads the stock
SELECT stock → 1Cards left: 1 - B Reads the stock
SELECT stock → 1Cards left: 1 - A Sees one left, takes payment
1 > 0 ✓ · chargeCards left: 1 - B Also sees one left, takes payment
1 > 0 ✓ · chargeCards left: 1 - A Order confirmed
UPDATE stock = stock − 1 → 0Cards left: 0 - B Order confirmed, but the card is gone
UPDATE 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.
Architecture
- The buyer signs in with Privy. The API verifies that token and issues an HttpOnly session cookie.
- Browser traffic goes through a same-origin proxy to the NestJS API on Cloud Run.
- Product state (the catalog, vaults and users) lives in Firestore. All money lives in Postgres, in the ledger.
- A purchase touches both stores. A transactional outbox and reconciliation sweepers keep them in step.
- Stripe webhooks are verified, deduplicated in Redis and published to Pub/Sub. A dedicated worker drains them, with a dead-letter collection behind it.
- Catalog ingestion reads each slab label with GPT-4o Vision and queues low-confidence reads for a person.
- Fulfillment runs through Shippo, with its own worker and a tracking state machine that only moves forward.

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.

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 tierAt 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
How it works, on the SwishX home page, Aug 2026. 
Recent pulls feed on swishx.co, Aug 2026. Usernames blurred. 
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
| Figure | What it measures |
|---|---|
| ~200K | lines of TypeScript across three apps, the functions service and shared packages |
| 122 | test files (Vitest, Playwright end-to-end, Firebase security-rules tests) |
| 145 | flow documents in the structured manual-QA framework |
| 27 / 6 | 27 atomic RBAC permissions composed into 6 built-in roles |
| ~1 h → ~15–20 min | backend deploy time after path-scoped deploys |
| ≈0.29 → ≈0 | expected seed collisions after the move from a 32-bit to a 64-bit seed (at 50K and 200K purchases) |
| 50–100/s | purchases on a hot drop with zero oversell, in load tests (not production traffic) |
Links
Stack: TypeScript, Next.js 16, React 19, NestJS 11, Drizzle, Postgres, Redis, Firestore, Pub/Sub, Cloud Run, Stripe Connect, Privy, Shippo, GPT-4o Vision, Pixi.js, Three.js, GSAP
- See it liveswishx.co (the live product) ↗

