How we compute EUR and USD card prices
What a price row carries, why EUR and USD never get averaged, how the composite index is built as a median, and the mistake a customer caught this week.
"What is this card worth?" sounds like one question. In practice it is five: on which market, in which currency, in what condition, for which printing, and as of when. Most price APIs answer with a single number and leave the five questions to you. This one answers with rows, and every row carries its own answers. This article is about how those rows are built, which is also an honest account of the places where we got it wrong first.
The short version of our position: the API is built in Europe, so Cardmarket asking prices in EUR sit on every card we can price, next to TCGplayer guide figures in USD. A European collector recognises the first number; an American one recognises the second. We keep both and we do not blend them. Cardmarket and TCGplayer are marketplaces we quote as sources; we are not affiliated with either.
A price is a row, not a number
Every quote in a response has the same shape. source says who observed it. basis says what kind of figure it is: ASKING is what a seller wants today, GUIDE is a published reference computed by the source from its own history, SOLD is a completed transaction, and DERIVED is a number we computed ourselves. currency is the currency the figure was observed in. variant distinguishes a lowest listing from a market figure or a graded median. Then condition, printing, grading, as_of, sample_n where the source publishes a count, and a provenance string you can print next to the number.
# Set PTCG_API_KEY to your own key before running this.
curl --fail-with-body \
'https://api.pokemontcgapi.com/v1/cards/base1-4/prices' \
--header "X-Api-Key: ${PTCG_API_KEY}"That call costs 2 credits and returns data.index beside data.quotes. The rows below are the shape of what comes back; the amounts are illustrative, the field names are exact.
{
"source": "CARDMARKET",
"variant": "LOW",
"basis": "ASKING",
"amount": 412.00,
"currency": "EUR",
"locale": "en",
"condition": null,
"printing": "HOLOFOIL",
"grading": null,
"as_of": "2026-09-22",
"sample_n": null,
"provenance": "Cardmarket"
}
{
"source": "TCGPLAYER",
"variant": "MARKET",
"basis": "GUIDE",
"amount": 800.43,
"currency": "USD",
"locale": "en",
"condition": null,
"printing": "HOLOFOIL",
"grading": null,
"as_of": "2026-09-22",
"sample_n": null,
"provenance": "TCGplayer"
}Those two rows disagree, and they should. One is the cheapest copy a European seller is asking for today; the other is a guide figure computed on the US market. They are two facts about two markets. An API that averaged them would give you a number nobody can find on any website, and a customer who challenges it would be right.
EUR and USD stay separate
We never convert a quote for you. A Cardmarket row is in EUR because Cardmarket trades in EUR; a TCGplayer row is in USD for the same reason. Conversion is a decision that belongs to the product showing the price, because it depends on the day, the rate you trust and what you tell the user about it. The only place a currency conversion happens inside the API is the composite index, and the next section says exactly how.
- Cardmarket rows are lowest asking prices per printing language, so a German and a Japanese printing of the same card are two rows.
- CardTrader rows are asking prices per language and condition, in EUR, from a second European marketplace.
- TCGplayer rows are guide figures, low and market, in USD.
- The full list of sources, with the basis each one is quoted on and how old a figure is before we serve it, is on the price methodology page and in the sources endpoint, which returns the literal
sourcestrings you will switch on.
The composite index: a median, in EUR, over what we may redistribute
Sorting a collection by value, ranking a set, showing one number on a list row: these need a single comparable figure per card, and the raw rows will not give you one. So we compute PTCG_INDEX, and we publish the recipe rather than asking you to trust it.
- Take the current rows for one card, one language and one printing. Only raw copies: graded rows are a different object and never enter the index.
- Keep only the variants that describe a market level rather than an outlier: market and trend figures, rolling averages, and the lowest listing. Keep only sources whose figures we are allowed to redistribute; a source that is switched off leaves the index on the next daily run.
- Drop series with no observation in the last thirty days. A price that stopped moving because nobody sells the card any more is not a price.
- Express every remaining figure in EUR. EUR rows stay as they are; other currencies are converted with the European Central Bank daily reference rate for that currency.
- Take the median. Not the mean: with two or three sources, one bad quote moves a mean a long way and a median almost not at all.
The result is one row per card, language and printing, with source: "PTCG_INDEX", basis: "DERIVED", currency: "EUR" and sample_n equal to the number of figures behind it. On a single card it is always present as index_eur. On lists and batches you ask for it with include=index, at 1 credit per 50 cards requested, which is what makes a value-sorted list one request instead of a request per card.
What the index is not
It is not a market price and it does not claim to be one. It is a derived median that exists to compare cards with each other. Show a customer the row from their own market; use the index to sort.
Why the figure you read describes yesterday
Every upstream source carries a minimum delay, twenty-four hours for the marketplaces, before a figure is served. A reference price is not a live order book, and the two are different products. The delay is stored per source and enforced when the response is built, so what you read is a settled figure rather than one mid-flight. The index and community rows carry no delay because they are ours.
This is also the place where we broke the API once. The cache of current prices used to keep the most recent row per series, and the delay was applied only when serving. For a source that publishes daily, those two rules together made an entire series disappear: the row from today was the only one cached, it was younger than the delay, it was discarded, and the row from yesterday, which could have been served, was gone. Two hundred thousand rows in the cache and zero quotes in responses. The fix was to apply the delay when choosing what to cache, not only when serving, so the cache always holds the latest observation we have the right to give. The lesson generalises: a rule applied in two places has to agree with itself in both.
Graded copies are a different object
A PSA 10 and a CGC 10 of the same card are not two observations of one price; they are two markets. Graded rows carry variant: "MEDIAN_GRADED" and a grading object with the company and the score, so they stay separate from each other and from the raw rows. They never enter the index, and they are available from the Growth plan upwards; the plan a key is on decides which rows come back, and the response names what was held back so a missing row is never mistaken for a missing price.
A mistake a customer caught this week
One European marketplace publishes two lowest asking prices per language: one over all sellers, one over sellers shipping within the EU. The two figures arrive a few milliseconds apart. Our series identity did not include that distinction, so the second row overwrote the first, and wherever the two differed the API served the EU-only minimum as if it were the global one. A customer noticed that the number on a card page and the number from the API disagreed on the same card, the same day. He was right.
The correction makes that distinction part of the identity of a series, exposes it on the row so you can pick the one your product needs, and rewrites the historical rows in place rather than deleting them. It is in review as this is published, and the methodology page will say when it is live. Two things worth taking away: a dimension that is not in the key silently picks a winner, and a customer reading your rows carefully is the best quality check you will ever get, so answer within the hour.
What to show a customer
| You need | Use | Why |
|---|---|---|
| A price a European buyer recognises | The CARDMARKET row in EUR | It is the figure they will see on the marketplace. |
| A price an American buyer recognises | The TCGPLAYER row in USD | Same reason, other market. |
| One number to sort or rank a list | index_eur | A median across sources, comparable across cards. |
| A number to defend when challenged | Any row, with provenance and as_of printed beside it | The disclosure is a field, not a footnote. |
| The value of a slab | A MEDIAN_GRADED row with its company and score | Grades are markets of their own. |
Whatever you show, show the date. as_of is the day the figure is for, and a price without its date is an opinion. The current prices endpoint returns up to fifty cards in one call with the same rows, and the card object documents index_eur and last_price_at on the card itself.
Questions
- Why do two sources disagree on the same card?
- Because they measure different things. An asking price is what a seller wants today, a guide figure is a published reference built on history, and a graded median is a different object entirely. The rows keep them apart so you can choose, and the basis field says which is which.
- Is the composite index a market price?
- No. It is a median, in EUR, over the sources we may redistribute, computed per card, language and printing from series observed in the last thirty days. It exists to compare and sort cards. For a figure a buyer recognises, use the row from their own market. Price methodology
- Can I download all prices as a file?
- No, on any plan. Price rows are served per card through the API, which is where the per-source delay is enforced. Catalogue snapshots on some plans contain sets, cards, images and identifiers, never prices.