Collection tracking
A collection tracker is a pricing problem wearing a catalogue costume
Build a collection tracker across nine games with per-printing identity, both markets priced, and a valuation your users can actually act on.
The problem
Where this project usually goes wrong
The catalogue half of a collection tracker looks like the hard part and is not. Users add cards, you store ids, you render images. The part that decides whether people keep using it is the total at the top of the screen.
That total is where most trackers quietly fall apart. A user owns a specific printing — this set, this collector number, this finish — and if your data model collapses those into one card record, you show the same value for a bulk reverse holo as for the version that is worth two hundred times more. Users spot it immediately and stop trusting every other number you display.
The second failure is currency. A European user shown a dollar figure converted at today's rate is being told a number that does not exist in their market, because European and American prices diverge for reasons that have nothing to do with the exchange rate.
What it needs
The fields that decide it
- Per-printing identity
- Every finish, language and promo treatment is its own row with its own price, so a user's specific copy is the thing being valued.
- prices[] by source
- European and American quotes on the same object, each in its native currency, so you show the user the market they are actually in.
- listType: buylist
- What the collection would fetch if sold, which is the honest companion to what it would cost to rebuild.
- updatedAt on every quote
- Lets you show how fresh a valuation is rather than implying every number is live.
- 100 cards a page, 2 credits
- A page carries every card's prices, so a local copy of the games you track is a few hundred credits to build and the same to refresh.
In practice
The two queries that matter most
Written for apps where people catalogue what they own. Both work the same way for every game we carry.
Value a shelf of cards in one round trip
{
a: card(id: "pkm_sv3pt5_6") { ...Valued }
b: card(id: "op_op05_119") { ...Valued }
c: card(id: "lor_9_242") { ...Valued }
}
fragment Valued on Card {
id
name
set { name code }
collectorNumber
prices {
source
listType
currency
market
updatedAt
}
}Aliases put the whole shelf in one request, with both markets and both sides of each. Looping one HTTP call per card is the single most common reason a tracker feels slow.
Build the local catalogue, then refresh it nightly
# Page a game at 100 cards a time; every card arrives with its prices.
# 20,000 printings is 200 pages and 400 credits, at setup and again each night.
page=1
while :; do
body=$(curl -sG "https://api.tcggraph.com/v1/cards" \
-d game=pokemon -d limit=100 -d page=$page \
-H "Authorization: Bearer $TCGGRAPH_KEY")
echo "$body" | jq -c '.data[]' >> pokemon.ndjson
[ "$(echo "$body" | jq '.meta.hasMore')" = "true" ] || break
page=$((page + 1))
doneThere is no bulk export or changed-since filter; the page is the unit. Between passes, set.published and card.created webhooks say when something new exists.
Avoid these
Three mistakes that are expensive to undo
Each of these is cheap to get right at the start and painful to retrofit once you have user data shaped the wrong way.
Storing a card name instead of a printing id
Names are not unique across sets, reprints or languages, and a user who owns the expensive printing will not accept being shown the cheap one's price. Store the printing id; render the name.
Valuing everything at retail
A collection marked at retail is a number the owner can never realise. Show both, or at minimum label clearly which one you are showing.
Converting one market's price into another currency
An FX conversion of an American price is not a European price. Ask for the market your user is in and show it natively.
Questions
Collection tracker, answered
Also built on this
Other projects, same catalogue
Ship your card app this weekend
$29 a month for 10,000 credits, no sales call and no contract. Pick a plan and your first query runs in under a minute.