
Building a TCG Inventory Engine: Next.js + Airtable + Live TCGPlayer Pricing
February 8, 2025
Building a TCG Inventory Engine
A normal e-commerce platform — Shopify, BigCommerce, even WooCommerce — is built around stable SKUs at stable prices. Trading card products break that assumption immediately. Market prices on TCGPlayer move daily, sometimes hourly. Bulk pricing for D2B customers depends on the live market number plus a per-SKU discount. And the catalog itself is constantly turning over as sets release and rotate.
So I built my own.
The stack
The whole thing fits in a couple hundred lines of Next.js plus Airtable as the source of truth.
- Airtable is the database. Each row is a SKU with name, quantity, condition, category, a TCGPlayer URL, a bulk-pricing percentage, and a coming-soon flag with expected stock count and arrival date.
- Next.js (Vercel) serves the public storefront and a single internal API route at
/api/inventory. - TCGPlayer scraping runs server-side inside that API route. For each SKU with a TCGPlayer URL, I extract the product ID and hit two endpoints: TCGPlayer's
mpapiprice endpoint, and as a fallback, the__NEXT_DATA__blob embedded in the product page HTML. - Image resolution is a chain: TCGPlayer's product image CDN → the Pokémon TCG API set logo → set artwork → a deterministic gradient placeholder.
Why scrape instead of use the official API
TCGPlayer has an API, but it's gated by their partner program. Scraping the public market price is more flexible and lets me fetch on demand without rate-limit headaches — as long as I do it from rotating residential proxies (more on that in another post).
The pricing logic
Every product has a BULK_PERCENTAGE field. The math is straightforward: market price × bulk percentage = bulk unit price. Total value = bulk unit price × quantity. The storefront shows market price publicly and the bulk price is what we quote internally to D2B customers.
const bulkUnitPrice = marketPrice * (bulkPercentage / 100);
const lineTotal = bulkUnitPrice * quantity;
That's the entire pricing engine. It's simple because it has to be: D2B buyers want predictable formulas, not opaque algorithms.
Coming-soon products
Preorders and unreleased product needed special treatment. Each SKU has a COMING_SOON flag, an expected stock count, and an arrival date. The storefront shows a separate "Coming Soon" section with a countdown to the release date. This is what the wholesale preorder funnel on cravinos.dev hooks into — the same data drives both the public storefront and the quote-request form.
What I learned
- Don't try to model everything in the database. I started with a "status" enum, then a "release stage" field, then realized the COMING_SOON boolean plus a name suffix (e.g. "Wave 2") covered every case I actually had.
- Cache aggressively, expire on writes. A 60-second cache on the inventory API was the difference between a snappy storefront and a TCGPlayer-rate-limited mess.
- Always have a placeholder. If TCGPlayer can't return an image, you still need to render something — that's why the gradient fallback is a critical-path component.
Next post: the residential proxy infrastructure that keeps the scraper alive.