# 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.

- **Published**: 2026-09-24
- **Updated**: 2026-09-24
- **Tags**: prices

Source: https://pokemontcgapi.com/blog/how-we-compute-eur-and-usd-card-prices

"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.

```bash
# 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.

```json
{
  "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](https://pokemontcgapi.com/price-methodology) page and in the [sources endpoint](https://pokemontcgapi.com/docs/api/prices/sources), which returns the literal `source` strings 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.

1. 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.
2. 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.
3. 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.
4. 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.
5. 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](https://pokemontcgapi.com/docs/api/prices/current) endpoint returns up to fifty cards in one call with the same rows, and the [card object](https://pokemontcgapi.com/docs/api/cards/get) documents `index_eur` and `last_price_at` on the card itself.
