DEV Community

lulzasaur
lulzasaur

Posted on

Building a Trading Card Price Tracker with Free APIs

Building a Trading Card Price Tracker with Free APIs

If you collect trading cards -- Pokemon, Magic: The Gathering, Yu-Gi-Oh -- you know the pain of checking prices manually. You also know that a card's market price only tells half the story. The other half is scarcity: how many PSA 10 copies exist?

In this tutorial, we will build a Node.js script that:

  1. Looks up current market prices from TCGPlayer
  2. Pulls graded population data from PSA
  3. Combines them into a price-to-scarcity ratio
  4. Sends alerts when undervalued cards are found

Total code: about 50 lines. Total cost: $0.

Prerequisites

  • Node.js 18+ installed
  • A text editor
  • Optional: a Slack workspace for alerts

Step 1: Fetch TCGPlayer Prices

The TCGPlayer API returns current market prices, lowest listings, and card metadata. Here is how to search for a card:

const BACKEND = "https://rapidapi-backend-production.up.railway.app";

async function getCardPrices(cardName) {
  const res = await fetch(
    `${BACKEND}/tcgplayer/search?query=${encodeURIComponent(cardName)}&limit=10`
  );
  const data = await res.json();

  if (!data.success) throw new Error(data.error);

  return data.results.map((card) => ({
    name: card.name || card.title,
    set: card.set || card.subtitle,
    marketPrice: card.marketPrice || card.price,
    lowestPrice: card.lowestPrice || card.lowPrice,
    imageUrl: card.imageUrl || card.image,
    url: card.url,
  }));
}
Enter fullscreen mode Exit fullscreen mode

Try it out:

curl "https://rapidapi-backend-production.up.railway.app/tcgplayer/search?query=charizard&limit=5"
Enter fullscreen mode Exit fullscreen mode

You will get back something like:

{
  "success": true,
  "count": 5,
  "results": [
    {
      "name": "Charizard ex",
      "set": "Obsidian Flames",
      "marketPrice": 12.50,
      "lowestPrice": 9.99,
      "imageUrl": "https://..."
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Fetch PSA Population Data

PSA grades cards from 1-10 and publishes how many of each grade exist. A card with 500 PSA 10s is much less scarce than one with 12.

The PSA Population API takes a certification number and returns the full population breakdown:

async function getPopulationReport(certNumber) {
  const res = await fetch(
    `${BACKEND}/psa/pop?certNumber=${encodeURIComponent(certNumber)}`
  );
  const data = await res.json();

  if (!data.success || data.count === 0) return null;

  const report = data.results[0];
  return {
    cardName: report.subject || report.cardName,
    year: report.year,
    brand: report.brand,
    grade: report.grade,
    totalPopulation: report.totalPop || report.populationTotal,
    psa10Count: report.psa10 || report.pop10 || 0,
    psa9Count: report.psa9 || report.pop9 || 0,
  };
}
Enter fullscreen mode Exit fullscreen mode

Step 3: The Complete Price Tracker

Here is the full working script that ties it all together. Save this as card-tracker.js:

// card-tracker.js -- Trading card price + scarcity tracker
const BACKEND = "https://rapidapi-backend-production.up.railway.app";

// Cards to track: name (for TCGPlayer) + PSA cert number (for pop data)
const WATCHLIST = [
  { name: "Charizard Base Set", certNumber: "40522040" },
  { name: "Pikachu Illustrator", certNumber: "10217569" },
  { name: "Black Lotus Alpha", certNumber: "42003653" },
];

async function fetchJSON(url) {
  const res = await fetch(url);
  return res.json();
}

async function trackCard({ name, certNumber }) {
  // Fetch price and population in parallel
  const [priceData, popData] = await Promise.all([
    fetchJSON(`${BACKEND}/tcgplayer/search?query=${encodeURIComponent(name)}&limit=1`),
    fetchJSON(`${BACKEND}/psa/pop?certNumber=${certNumber}`),
  ]);

  const price = priceData.results?.[0];
  const pop = popData.results?.[0];

  const marketPrice = price?.marketPrice || price?.price || 0;
  const psa10Pop = pop?.psa10 || pop?.pop10 || 0;
  const totalPop = pop?.totalPop || pop?.populationTotal || 0;

  // Scarcity score: lower PSA 10 count = higher scarcity
  // Value ratio: price relative to scarcity (lower = potentially undervalued)
  const scarcityScore = psa10Pop > 0 ? Math.round(marketPrice / psa10Pop * 100) / 100 : null;

  return {
    name,
    marketPrice,
    psa10Population: psa10Pop,
    totalGradedPopulation: totalPop,
    pricePerPsa10: scarcityScore,
    cardUrl: price?.url || null,
  };
}

async function runTracker() {
  console.log("Card Price + Scarcity Report");
  console.log("=".repeat(60));
  console.log(`Generated: ${new Date().toISOString()}\n`);

  for (const card of WATCHLIST) {
    try {
      const result = await trackCard(card);
      console.log(`${result.name}`);
      console.log(`  Market Price:     $${result.marketPrice}`);
      console.log(`  PSA 10 Pop:       ${result.psa10Population}`);
      console.log(`  Total Graded:     ${result.totalGradedPopulation}`);
      console.log(`  $/PSA 10 Copy:    ${result.pricePerPsa10 ? `$${result.pricePerPsa10}` : "N/A"}`);
      console.log("");
    } catch (err) {
      console.log(`${card.name}: Error - ${err.message}\n`);
    }
  }
}

runTracker();
Enter fullscreen mode Exit fullscreen mode

Run it:

node card-tracker.js
Enter fullscreen mode Exit fullscreen mode

Output:

Card Price + Scarcity Report
============================================================
Generated: 2026-03-20T14:30:00.000Z

Charizard Base Set
  Market Price:     $285.00
  PSA 10 Pop:       3407
  Total Graded:     45210
  $/PSA 10 Copy:    $0.08

Pikachu Illustrator
  Market Price:     $5100000.00
  PSA 10 Pop:       1
  Total Graded:     41
  $/PSA 10 Copy:    $5100000.00
Enter fullscreen mode Exit fullscreen mode

The $/PSA 10 Copy metric is what makes this interesting. A card with a high market price but very low PSA 10 population is scarce at that grade. A card with a moderate price and a dropping PSA 10 population (fewer high-grade copies surfacing) might be undervalued.

Step 4: Add Price Alerts

Now let's add email or Slack alerts when a tracked card drops below a target price or when new PSA 10s appear (diluting scarcity).

Slack Webhook Alerts

const SLACK_WEBHOOK = process.env.SLACK_WEBHOOK_URL;

async function sendAlert(message) {
  if (!SLACK_WEBHOOK) {
    console.log("[ALERT]", message);
    return;
  }

  await fetch(SLACK_WEBHOOK, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ text: message }),
  });
}

// Add price targets to your watchlist
const WATCHLIST_WITH_TARGETS = [
  { name: "Charizard Base Set", certNumber: "40522040", targetPrice: 250 },
  { name: "Black Lotus Alpha", certNumber: "42003653", targetPrice: 400000 },
];

async function checkAlerts() {
  for (const card of WATCHLIST_WITH_TARGETS) {
    const result = await trackCard(card);

    if (result.marketPrice <= card.targetPrice) {
      await sendAlert(
        `Price Alert: ${result.name} is at $${result.marketPrice} (target: $${card.targetPrice}). PSA 10 pop: ${result.psa10Population}`
      );
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Email Alerts (using Nodemailer)

import nodemailer from "nodemailer";

const transporter = nodemailer.createTransport({
  service: "gmail",
  auth: {
    user: process.env.GMAIL_USER,
    pass: process.env.GMAIL_APP_PASSWORD, // Use an App Password, not your real password
  },
});

async function sendEmailAlert(subject, body) {
  await transporter.sendMail({
    from: process.env.GMAIL_USER,
    to: process.env.ALERT_EMAIL,
    subject,
    text: body,
  });
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Run on a Schedule

The simplest way to run this daily is a cron job:

# Run every day at 9am
crontab -e
0 9 * * * /usr/bin/node /path/to/card-tracker.js >> /var/log/card-tracker.log 2>&1
Enter fullscreen mode Exit fullscreen mode

Or if you prefer a cloud option, deploy it as a GitHub Action:

# .github/workflows/card-tracker.yml
name: Card Price Tracker
on:
  schedule:
    - cron: "0 14 * * *" # 9am EST
jobs:
  track:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: node card-tracker.js
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
Enter fullscreen mode Exit fullscreen mode

Extending It

Some ideas for where to take this:

  • Historical tracking: Save results to a JSON file or SQLite database, then chart price trends over time
  • Multi-marketplace: Add Reverb (for music gear) or Poshmark (for fashion) to track prices across different collectible categories
  • Portfolio valuation: Track your entire collection and get a daily portfolio value report
  • Arbitrage detection: Compare TCGPlayer prices to eBay sold listings to find mispriced cards

The APIs used in this tutorial (TCGPlayer and PSA Population) are available at RapidAPI with 50 free requests per month -- more than enough for a daily tracker on a small watchlist.

Top comments (0)