Portfolio and analytics
A portfolio marked at retail is a portfolio marked wrong
Build card portfolio and valuation tools on daily price history, dealer exit prices and per-printing identity across eight games.
The problem
Where this project usually goes wrong
Cards are traded as assets and increasingly held as them. That raises the standard for the numbers you show: a user deciding whether to sell is making a financial decision on your figure, and a figure that overstates the exit by half is not a rounding error.
Retail is the wrong mark for a position because it is the acquisition price, and nobody exits at it. The realisable number is the dealer bid, and the gap between the two is not a constant — it widens sharply as cards get cheaper, so a portfolio full of mid-value cards is overstated by a different amount than one holding a few expensive ones.
History is the other half. A single current price cannot tell a user whether they are up or down, and a series stitched together from whatever you happened to record is full of gaps exactly where the interesting movements were.
What it needs
The fields that decide it
- Daily price history
- A dense daily series per printing, so a position curve does not depend on when your job happened to run.
- listType: buylist
- The realisable exit price, which is what a position is genuinely worth rather than what it would cost to replace.
- Per-printing identity
- A position is in a specific printing. Marking it against a card-level average destroys the entire point.
- Both markets
- The same card is a different asset in Europe and North America, and a holder should be marked against the one they can sell into.
- Webhooks on price thresholds
- Alert a holder when a position crosses a level, instead of asking them to check.
In practice
The two queries that matter most
Written for investment, valuation and analytics products. Both work the same way for every game we carry.
Chart a position over a year
curl -G "https://api.tcggraph.com/v1/cards/mtg_f6555d1f-d4c/history" \
-d source=cardmarket \
-d from=2025-09-01 \
-d to=2026-09-01 \
-d interval=DAY \
-H "Authorization: Bearer $TCGGRAPH_KEY"The series comes back dense: a day with no recorded change carries the last known price forward, so you never have to fill gaps yourself or explain a chart that drops to zero.
Mark a portfolio to exit rather than to retail
{
cards(filter: { ids: ["mtg_f6555d1f-d4c", "mtg_5465a1b2-9cd"] }) {
nodes {
name
retail: prices(listType: RETAIL) { market currency }
exit: prices(listType: BUYLIST) { market currency }
}
}
}Aliasing both sides in one query gives you the spread per position without a second request or a second reconciliation.
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.
Applying one discount to model the exit
The realisable share of retail varies by nearly four times across price bands. A flat haircut flatters cheap positions and penalises expensive ones.
Building history from your own polling
Your series will have gaps wherever your job failed, and those gaps cannot be backfilled from anything you hold. Read a series that was recorded properly.
Ignoring which market the holder is in
A European holder marked against American prices is looking at an asset they cannot sell at that number.
Questions
Portfolio tracker, answered
Also built on this
Other projects, same catalogue
Ship your card app this weekend
$19 a month for 25,000 credits, no sales call and no contract. Pick a plan and your first query runs in under a minute.