I Reverse Engineered a Pokémon Card Pricing Site and Built My Own Tracker

I was sitting on my couch at 11pm, three tabs deep into TCGPlayer, trying to figure out whether a PSA 9 Charizard VMAX Alt Art was actually going to appreciate or if I'd missed the window. The prices were right there on the screen. Historical data, market trends, recent sales. But I couldn't do anything useful with it. I couldn't set alerts. I couldn't compare across sets. I couldn't answer the one question I actually cared about: which cards in my collection are likely to go up?
So I did what any reasonable person would do. I opened DevTools.
And that's when I saw it. A clean JSON API response sitting right there in the Network tab. No auth header. No obfuscation. Just... data. Hundreds of fields per card. Price history, sale volumes, market averages. Everything I needed to build exactly what I wanted.
The only problem? I had no idea what half the fields meant. That's where AI came in.
The Workflow: Reverse Engineering with AI as Your Co-Pilot
Let me be clear about what I mean by "reverse engineering" here. I'm not talking about disassembling binaries or analyzing malware. I'm talking about something much more mundane and, honestly, much more useful for most developers: figuring out how existing web products work by observing their behavior, and then building your own thing.
The workflow looks like this:
- Open DevTools on a site you're curious about
- Watch the network requests as you interact with the page
- Find the API endpoints the frontend is calling
- Copy confusing responses or minified code into an LLM
- Ask it to explain what you're looking at
- Map out the architecture from the outside in
That's it. No hacking. No exploits. You're reading publicly available information that your browser already downloaded. You're just paying attention to it.
Pro tip: Filter network requests by
Fetch/XHRin Chrome DevTools. Most of the interesting stuff lives there, not in the document or script requests.
The first time I did this with a pricing site, I felt like I'd been given x-ray vision. The pretty UI was just a skin over a straightforward REST API. And once you see the API, you understand the product.
The Pokémon Card Tracker: Why I Built It
Here's my problem. I collect Pokémon cards. Not seriously enough to call myself a collector (my wife would disagree), but seriously enough that I've spent more money than I'd like to admit on sealed product and singles. I wanted to know: am I actually making money on any of this, or am I just hoarding cardboard?
Sites like TCGPlayer and PriceCharting already track this data. But they're built for everyone, which means they're built for no one in particular. I wanted something specific:
- Track only the cards I actually own
- Show me percentage gains/losses over 30, 60, 90 days
- Flag cards with unusual volume spikes (usually means something is happening)
- Tell me which cards in a set are undervalued relative to the set's overall trend
None of the existing tools did exactly this. So I built it myself.
Step 1: Finding the API
I went to a popular card pricing site (I'll keep it vague for obvious reasons) and started clicking around while watching the Network tab. Within about 30 seconds I found an endpoint that looked like this:
GET /api/v2/products/{productId}/priceHistory?range=90d&granularity=dailyThe response was beautiful. Clean JSON, well-structured, obvious field names. Mostly.
// What the API actually returned (simplified)
interface PriceHistoryResponse {
productId: number;
name: string;
prices: {
date: string;
mktPrice: number;
avgSoldPrice: number;
lowPrice: number;
highPrice: number;
qtyAvail: number;
qtySold: number;
}[];
metadata: {
setName: string;
rarity: string;
cardNumber: string;
variant: string;
};
}Some fields were obvious. mktPrice is market price. avgSoldPrice is the average sold price. But what's the difference? How do they calculate mktPrice? Is it a rolling average? Weighted? Median? This matters a lot when you're trying to build trend analysis.
Step 2: Asking AI to Explain the Schema
I grabbed a few sample responses and pasted them into Claude with a simple prompt:
Here are three API responses from a card pricing platform. Based on the data patterns,
can you figure out how mktPrice is calculated relative to avgSoldPrice and qtySold?
Look at the relationship between these fields across different dates.Claude noticed something I'd missed completely. On days with higher qtySold, the mktPrice tracked closer to avgSoldPrice. On low-volume days, mktPrice stayed flat or barely moved. Its conclusion: market price is probably a volume-weighted moving average that only updates meaningfully when there's enough sales data to be statistically relevant.
Was it right? I couldn't be 100% sure. But when I built my tracker using that assumption, the numbers matched the site's displayed prices almost exactly. Close enough.
Step 3: Building the Scraper
Here's a simplified version of the scraper I wrote. Nothing fancy, just Bun with a rate limiter:
import { sleep } from "bun";
interface CardPrice {
productId: number;
date: string;
marketPrice: number;
averageSold: number;
quantitySold: number;
quantityAvailable: number;
}
const BASE_URL = "https://api.example-pricing-site.com/v2";
const RATE_LIMIT_MS = 2000; // Be nice. Seriously.
async function fetchPriceHistory(
productId: number,
days: number = 90,
): Promise<CardPrice[]> {
const url = `${BASE_URL}/products/${productId}/priceHistory?range=${days}d&granularity=daily`;
const response = await fetch(url, {
headers: {
"User-Agent": "pokemon-tracker/1.0 (personal project)",
Accept: "application/json",
},
});
if (!response.ok) {
console.error(`Failed to fetch ${productId}: ${response.status}`);
return [];
}
const data = await response.json();
return data.prices.map(
(p: {
date: string;
mktPrice: number;
avgSoldPrice: number;
qtySold: number;
qtyAvail: number;
}) => ({
productId,
date: p.date,
marketPrice: p.mktPrice,
averageSold: p.avgSoldPrice,
quantitySold: p.qtySold,
quantityAvailable: p.qtyAvail,
}),
);
}
async function scrapeCollection(productIds: number[]): Promise<void> {
console.log(`Scraping ${productIds.length} cards...`);
for (const id of productIds) {
const prices = await fetchPriceHistory(id);
if (prices.length > 0) {
await savePrices(prices); // Prisma upsert, covered below
console.log(`✓ ${id}: ${prices.length} price points`);
}
await sleep(RATE_LIMIT_MS);
}
}Notice the 2-second delay between requests. This isn't optional. If you're going to scrape someone's site, don't be a jerk about it. More on ethics later.
Step 4: The Database and Trend Analysis
I used Prisma with SQLite (it's a personal project, I don't need Postgres for 200 cards). The schema is simple:
// Simplified trend analysis
function calculateTrend(prices: CardPrice[], windowDays: number): number {
const now = new Date();
const windowStart = new Date(
now.getTime() - windowDays * 24 * 60 * 60 * 1000,
);
const recentPrices = prices.filter((p) => new Date(p.date) >= windowStart);
if (recentPrices.length < 2) return 0;
const first = recentPrices[0].marketPrice;
const last = recentPrices[recentPrices.length - 1].marketPrice;
return ((last - first) / first) * 100;
}
function detectVolumeSpike(
prices: CardPrice[],
threshold: number = 2.5,
): boolean {
const volumes = prices.map((p) => p.quantitySold);
const avgVolume = volumes.reduce((a, b) => a + b, 0) / volumes.length;
const recentVolume = volumes.slice(-3).reduce((a, b) => a + b, 0) / 3;
return recentVolume > avgVolume * threshold;
}The volume spike detection turned out to be genuinely useful. Twice now it's flagged cards that were about to jump in price because of a tournament result or a YouTuber featuring them. I didn't act fast enough either time (story of my life), but at least I know the signal works.
Using AI to Deobfuscate and Understand Code
The API was the easy part. Some sites aren't that generous. They load pricing data through heavily minified JavaScript bundles, calculated client-side from raw sales data. When I hit one of these, I had to get creative.
I found a pricing site that calculated its "recommended buy price" entirely in the browser. The logic lived in a 400KB minified JS file. I'm not going to read that manually. Nobody is.
So I grabbed the relevant chunk (about 2000 lines of minified code that I narrowed down using breakpoints) and pasted it into Claude:
Here's minified JavaScript from a card pricing site. This function calculates
what they call "recommended buy price." Can you rename the variables to something
meaningful and explain the algorithm?What came back was genuinely impressive. Claude identified:
- A weighted average calculation using the last 10 sales
- A standard deviation filter that throws out outliers (sales more than 2 sigma from the mean)
- A "freshness" multiplier that weighs recent sales more heavily
- A final adjustment that rounds down to the nearest quarter dollar
The renamed code looked something like this:
// What Claude produced from the minified mess
function calculateRecommendedBuyPrice(recentSales: number[]): number {
const sortedSales = [...recentSales].sort((a, b) => a - b);
// Remove statistical outliers
const mean = sortedSales.reduce((sum, s) => sum + s, 0) / sortedSales.length;
const stdDev = Math.sqrt(
sortedSales.reduce((sum, s) => sum + Math.pow(s - mean, 2), 0) /
sortedSales.length,
);
const filtered = sortedSales.filter((s) => Math.abs(s - mean) <= stdDev * 2);
// Apply recency weighting (newer sales count more)
const weights = filtered.map((_, i) => 1 + i * 0.15);
const totalWeight = weights.reduce((sum, w) => sum + w, 0);
const weightedAvg =
filtered.reduce((sum, sale, i) => sum + sale * weights[i], 0) / totalWeight;
// Round down to nearest 0.25
return Math.floor(weightedAvg * 4) / 4;
}Now, was this 100% accurate? Probably not. Claude might have gotten the exact weight factor wrong (maybe it's 0.12, not 0.15). But the structure was right, and that's what mattered. I could verify the output against the site's displayed prices and tweak the constants until my implementation matched.
Where AI Gets It Wrong
I want to be honest here because I think people oversell AI's capabilities with this stuff. About 30% of the time, Claude would confidently explain business logic that was just... wrong. It would say things like "this function calculates shipping cost" when it was actually calculating sales tax. It confuses similar-looking algorithms constantly.
The trick is verification. Always run your understanding against real data. If your reimplementation produces numbers that match the site, great. If they're off by a consistent factor, you've probably got the structure right but a constant wrong. If they're wildly different, AI guessed wrong about something fundamental.
Building the Full Thing
Once I understood the data sources and pricing logic, building my tracker took about a weekend. Here's the rough architecture:
- Bun script on a cron job: Runs daily, scrapes price data for my collection
- SQLite via Prisma: Stores all historical prices
- Simple Next.js frontend: Shows my portfolio value, trends, alerts
- Notification script: Sends me a Telegram message when volume spikes are detected
The scraping script runs at 6am. By the time I check my phone over coffee, I know if anything interesting happened in the Pokémon card market overnight. It's deeply satisfying in a way that's hard to explain to people who don't collect things.
I scaffolded most of the frontend with AI too. Told Claude what data I had, what I wanted to display, and it generated the components. The whole project, from "I wonder how this site works" to "I have a working tracker," took maybe 15 hours spread over a couple weekends.
Ethics and Practical Limits
I need to talk about this because it matters. Just because you can reverse engineer something doesn't always mean you should.
Things I think are fine:
- Scraping public data for personal use at respectful rates
- Understanding how a pricing algorithm works out of curiosity
- Building a personal tool that does something the original site doesn't offer
- Learning from how other people solved problems
Things that get sketchy:
- Ignoring robots.txt or rate limits
- Scraping at a volume that affects the site's performance
- Building an exact clone and competing with the original
- Redistributing scraped data commercially without permission
- Violating explicit Terms of Service (read them, they matter)
My tracker is for me. It scrapes maybe 200 data points per day with a polite delay between requests. That's less load than me manually refreshing 200 pages. I'm not selling the data or building a competing product. I'm just a guy who wants to know if his cardboard rectangles are appreciating.
If you're going to do this professionally or at scale, talk to a lawyer. I'm serious. The legal lines around scraping and reverse engineering vary wildly by jurisdiction, and "some guy on a blog said it was fine" isn't a legal defense.
Why This Skill Matters (Beyond Pokémon Cards)
I've used this same workflow professionally, though I obviously can't share those details. The pattern is always the same: you find a product that does something interesting, you figure out how it works, and that understanding makes you better at building your own things.
Every product you admire is built from ideas that already existed, just recombined and improved. Understanding how existing things work isn't cheating. It's engineering. It's literally how the entire field progresses.
What AI changed is the speed. Before LLMs, figuring out a minified codebase or an undocumented API was a multi-day affair involving lots of guessing, console logging, and staring at Chrome DevTools until your eyes glazed over. Now I can paste 2000 lines of obfuscated code into Claude and have a working mental model in 20 minutes. It's not perfect, but it's fast enough to make projects like this actually worth doing on a weekend.
The combination is what makes it powerful: your curiosity about how something works, your ability to use DevTools and read network requests, and AI's ability to quickly make sense of large amounts of unfamiliar code. Any one of those alone is useful. Together, they're something else entirely.
So next time you're using a product and you think "I wonder how they do that," open DevTools. You might be surprised how readable the answer is. And if it's not readable, well, that's what the AI is for.