A collection tracker answers the question every collector asks: what is my collection worth today, and is it going up? This guide shows how to store collections, keep their values current without running out of requests, and draw a price chart for any card.
What to store
For every card a user owns, store four things:
| Column | Where it comes from |
|---|---|
card_id |
From a search, a scan or a set list. It never changes. |
sku_id |
The printing, condition and language the user owns, picked from the card's skus. It never changes either. |
quantity |
How many copies |
added_at |
When they added it, if you want "since you bought it" changes |
Storing the sku_id matters. A Near Mint holofoil and a played normal copy of the same card can be worth very different amounts, and the SKU is how you value the copy the user actually has.
Adding cards
Give users three ways in:
- Search:
GET /v1/cards?game=Pokemon&name=Pikachu, then let them pick the card, and then the printing and condition from its SKUs. - Browse a set:
GET /v1/setslists every set, andGET /v1/cards?game=Pokemon&set=...&limit=100lists a set's cards (useoffsetfor the next page), so users can tick off what they own. - Scan: see the card scanner guide. One request finds the card and its prices.
Keeping values current
Don't fetch prices every time a user opens the app. Refresh them on a schedule instead:
- Collect the distinct
card_idvalues across all your users. Popular cards appear in many collections, so this list is far shorter than the total number of cards owned. - Look them up in batches with
POST /v1/cards/lookup. - Save each SKU's
market_pricein your own database, and value every collection from there.
import os
import requests
API = "https://tcgpriceapi.com"
HEADERS = {"X-API-Key": os.environ["TCGPRICEAPI_KEY"]}
BATCH = 100 # your plan's cards per request
def refresh_prices(card_ids):
prices = {}
for start in range(0, len(card_ids), BATCH):
batch = card_ids[start:start + BATCH]
answer = requests.post(f"{API}/v1/cards/lookup", headers=HEADERS,
json={"card_ids": batch}, timeout=60)
answer.raise_for_status()
for card in answer.json()["cards"]:
for sku in card["skus"]:
prices[sku["sku_id"]] = sku["market_price"]
return prices # save these, then value each collection as quantity x price
Market prices refresh every 6 hours, so there's no gain in refreshing more often than that. Once a day is plenty for most trackers.
A market_price can be null when a SKU has no recent sales. Show "no recent price" rather than counting it as zero.
How many requests that takes
Each lookup counts as one request, whatever the number of cards in it. Say your users own 20,000 different cards between them:
| Plan | Cards per request | Requests for one refresh of 20,000 cards |
|---|---|---|
| Pro | 100 | 200 |
| Business | 200 | 100 |
| Enterprise | 250 | 80 |
Pro allows 6,000 requests a day, so even a refresh every 6 hours leaves most of the day's requests for searches and charts. Starter fits a smaller tracker: 20 cards per request, up to 1,000 cards a day.
Price charts
Sold history gives you the chart for any card:
GET /v1/cards/{card_id}/history?timeframe=30d&condition=Near Mint
Each row has a date, the market price, the lowest and highest sale and the quantity sold. To chart several cards at once, such as a user's ten most valuable cards, send them together:
POST /v1/history/lookup
{"card_ids": ["card_...", "card_..."], "timeframe": "30d", "condition": "Near Mint"}
On Pro and up that takes up to 20 cards per request. History for 7 or 30 days counts as one request. History for 3 months or more counts one request per card, so load long charts when a user opens a card rather than for the whole collection.
How far back charts go depends on the plan: 7 days on Free, 30 days on Starter and a full year on Pro and up.
Features collectors ask for
- Total value and daily change. Save each day's total so you can show the change without extra requests.
- Biggest movers. Compare today's saved prices with last week's.
- Want lists and alerts. Let users set a target price, and check it after each refresh. The cheapest live listing (
listing_price_low, pluslisting_price_low_shipping) is what they'd actually pay. - Set completion. List a set's cards and show which ones the user still needs, with what each would cost.
- Pictures. Every card has an
image_urlfor your grid.
Picking a plan
- Free is enough to build and test with, for personal projects.
- Starter allows commercial use, fits a tracker with a few thousand distinct cards, and includes 30 days of history.
- Pro is where most public trackers end up: no daily card limit, 100 cards per request and a full year of history for charts.
Compare every plan on the pricing table.