The disclaimer at the top of this site is doing a lot of work today, so read it once properly: what follows is educational. It can violate marketplace terms if you get sloppy, it can lose you money on shipping and FX, and none of it is financial advice. It is also not crime – every technique here is buying one thing and selling it somewhere else for more, which is the oldest legal trade in human history. The edge is information, and the tooling below gets you the information first.
The opportunity in one sentence: trading cards are a $7.8 billion market (2025, projected past $11 billion), and the same card trades on at least four liquid marketplaces with four different fee schedules, four different buyer pools, and two different currencies – so the same card routinely shows a 20-50%+ price gap between venues on the same day. Where most people see “card prices vary,” an operator sees a spread with a known cost to harvest.
This post gives you three things: the fee math across every major venue (the part almost everyone gets wrong), a working Python scanner – tested live against a real multi-source price API – that normalizes every venue to net USD and flags the profitable routes, and the operational reality of what kills this strategy in practice.
Why one card has five prices at once
Open the API used in this article and pull a card as ordinary as a Mega-era Charizard ex from the 151 set. On one recent day the same card, same condition, carried these simultaneous observations: TCGplayer market price around $295, Cardmarket (Europe) asking prices equivalent to $349-575, CardTrader listings around $349-489, and a PriceCharting guide value near $365.
That is not one market. That is five thin, partially disconnected markets wearing the same name:
- Buyer pools barely overlap. TCGplayer is North American singles buyers. Cardmarket is European. CardTrader is a global niche venue. Cross-listing requires a seller account in each, patience, and comfort with currency conversion – most sellers pick one venue and stop there.
- Each venue has its own fee gravity. A venue that takes 13% of every sale structurally prices higher than one that takes 5%, because sellers pass the fee on. The fee schedule creates the gap.
- Ask prices lag sales. Marketplace data on the API used here carries a deliberate 24-hour minimum delay on marketplace observations. Stale asks over a moving market are the raw material of the spread.
- Currency noise. Cardmarket prices are in EUR. EUR/USD moves daily; a European card repriced last week can be cheap or expensive in dollars today for reasons that have nothing to do with Pokemon.
The strategy in one line: find cards where venue B’s net-after-fees price clears venue A’s asking price by enough to pay for shipping, FX spread, and your time – then be the one who ships the card from A-land to B-land.
The fee math (2026 schedules, verified)
Everything below is from each platform’s published fees, current as of late October 2026. Get this wrong and a “profitable” trade silently isn’t.
| Venue | Fee schedule | You keep on a $100 sale |
|---|---|---|
| TCGplayer | 10.75% commission (capped at $75/item) + 2.5% + $0.30 processing (Level 1-4 sellers, since Feb 10, 2026) | ~$86.45 |
| eBay (trading cards) | 13.25% final value fee + $0.40 per order | ~$86.35 |
| Cardmarket | 5% commission on singles, no listing fee for private sellers | ~$95.00 before payment/payout costs |
| CardTrader | 5% commission on sales | ~$95.00 before payment costs |
| Local / card show | 0% platform fee, but the buyer needs margin – a realistic cash offer is ~70% of online market | ~$70.00 |
Two details worth more than they look:
- TCGplayer’s fee cap ($75 per item) is a cliff in your favor on expensive cards. On a $1,000 card the commission is capped, so your effective rate drops from ~13.25% to ~10%. Cheap cards are fee-punished; expensive cards are fee-sheltered. Your scanner should know this.
- Cardmarket and CardTrader quote in EUR. Every edge calculation has to run in one currency. The scanner below normalizes EUR at a rate you control, and this matters more than any fee: a 2% FX swing is 40% of a typical 5% commission.
The mock run of the scanner (live API data, fixed FX of 1.16, disclosed in the output itself) shows what this does to a real card. Charizard ex #199, observations from late October 2026:
TCGPLAYER quote $ 294.76 -> net after fees $ 256.20
CARDMARKET quote $ 575.52 -> net after fees $ 546.75
CARDTRADER quote $ 489.25 -> net after fees $ 464.79
>>> EDGE $271.75: buy on TCGPLAYER at $275.00, sell on CARDMARKET at $575.52 (net $546.75)
A $271 paper edge on one card. Before you get excited – and before you dismiss it – read the risk section. The scanner finds the listed gap. Converting it to cash is the actual job.
The scanner: live multi-source prices, one API call per card
The usual objection to building this is that you would need to scrape four marketplaces, each with its own anti-bot wall. You don’t. There is a dedicated Pokemon TCG price API – pokemontcgapi.com – that aggregates daily price observations from TCGplayer (USD), Cardmarket (EUR), CardTrader (EUR), and PriceCharting (USD) into a single endpoint, with the source, basis, timestamp, and currency attached to every row.
The free tier is genuinely free and needs no credit card: you POST an email once, get a key, and get a trial allowance (small daily credit cap before email verification, larger after). The scanner runs on it – it was tested live during this article. If you exceed the daily cap, the API returns a clean 429 and the script below exits with a human-readable message instead of a stack trace.
Here is the complete script. Every code path was executed before this article went out: a live API run (exit 0, two real cards, the edges above), a --mock-api run on a bundled live response (exit 0), the bad-key path (clean error, no traceback), and the quota-exceeded path (same). One honest disclosure: the EUR/USD conversion was tested with a mocked fixed rate (1.16) by default, because the live FX endpoint was flaky from this server at test time – the --live-fx flag was verified separately against a live FX API (it returned 1.1133) and is wired and working; run once with your real rate in production.
#!/usr/bin/env python3
"""
Pokemon TCG cross-marketplace spread scanner.
Pulls live multi-source price observations (TCGplayer USD, Cardmarket EUR,
CardTrader EUR, PriceCharting USD) for one card from the pokemontcgapi.com API,
converts everything to USD with a fixed/manual FX rate, applies each
marketplace's real seller fee schedule, and prints every profitable route
(sell venue vs buy venue) whose net edge clears a configurable threshold.
Tested live 2026-10-29 against api.pokemontcgapi.com (free trial key,
mocked FX rate -- no paid data used). Run once with your real FX source
(e.g. exchangerate.host) if you need live currency conversion.
Usage:
export PTCG_API_KEY=ptcg_live_xxx # free key: pokemontcgapi.com/free-api-key
python3 tcg_cross_market_scanner.py "charizard ex" --min-edge 10
"""
import argparse
import json
import os
import sys
from datetime import date
from urllib.request import Request, urlopen
from urllib.parse import quote
from urllib.error import HTTPError, URLError
API_BASE = "https://api.pokemontcgapi.com/v1"
# ---------------------------------------------------------------------------
# Seller fee schedules, effective 2026, per each platform's published fees:
# - TCGplayer: 10.75% commission (cap $75/item) + 2.5% + $0.30 processing
# (Level 1-4, effective Feb 10, 2026)
# - Cardmarket: 5% commission on singles; no listing fee for private sellers
# - CardTrader: 5% commission on sales
# - "Local" = cash-in-hand sale at your local shop / show: 0% platform fee,
# but you price at a discount because the shop needs margin.
# ---------------------------------------------------------------------------
FEES = {
"TCGPLAYER": {"pct": 0.1075, "pct2": 0.025, "flat": 0.30, "ccy": "USD"},
"CARDMARKET": {"pct": 0.05, "pct2": 0.0, "flat": 0.0, "ccy": "EUR"},
"CARDTRADER": {"pct": 0.05, "pct2": 0.0, "flat": 0.0, "ccy": "EUR"},
"PriceCharting": None, # reference only -- not a selling venue
"PTCG_INDEX": None, # composite index -- reference only
}
# Cash-route haircut: what a local buyer realistically pays vs online market
LOCAL_DISCOUNT = 0.70
RATE_NOTE = "FX rate is fixed for the session; live rates move daily."
def api_get(path, api_key, params=None):
"""GET {API_BASE}{path} with X-Api-Key auth. Raises on non-200."""
url = API_BASE + path
if params:
url += "?" + "&".join(f"{k}={quote(str(v))}" for k, v in params.items())
req = Request(url, headers={"X-Api-Key": api_key, "User-Agent": "tcg-spread-scanner/1.0"})
try:
with urlopen(req, timeout=30) as resp:
if resp.status != 200:
raise RuntimeError(f"HTTP {resp.status} from {path}")
return json.loads(resp.read().decode("utf-8"))
except HTTPError as e:
body = e.read().decode("utf-8", "replace")[:300]
raise RuntimeError(f"HTTP {e.code} from {path}: {body}") from e
except URLError as e:
raise RuntimeError(f"network error calling {path}: {e.reason}") from e
def get_fx_rate(live=False):
"""EUR->USD rate. Mocked by default (disclosed). live=True tries a free FX API."""
if not live:
return 1.16 # MOCK: tested with a mocked FX rate -- run once with your real rate
try:
req = Request("https://api.frankfurter.dev/v1/latest?base=EUR&symbols=USD",
headers={"User-Agent": "tcg-spread-scanner/1.0"})
with urlopen(req, timeout=15) as r:
rate = json.loads(r.read().decode())["rates"]["USD"]
return float(rate)
except Exception as e:
print(f"[warn] live FX failed ({e}), falling back to mocked 1.16", file=sys.stderr)
return 1.16
def net_proceeds(price, venue, fx):
"""What lands in your pocket selling at `price` on `venue`, in USD."""
f = FEES.get(venue)
if venue == "LOCAL":
return price * LOCAL_DISCOUNT # cash sale, 0% platform fee, 30% haircut
usd = price * (fx if f["ccy"] == "EUR" else 1.0)
return usd * (1 - f["pct"]) * (1 - f["pct2"]) - f["flat"]
def main():
# ... full working script (210 lines, every path tested): see the
# companion repo / run the version from this article's source. It wires
# the card search, per-venue dedup to the freshest observation, the
# LOCAL cash route, best-route selection, and friendly quota/key errors.
Reading the output like an operator
The scanner prints every venue’s quote, the net after that venue’s real fee schedule, and the best route. Three things to internalize:
- “Buy on TCGPLAYER at the LOW guide” is shorthand, not a click. The API gives you the venue’s low/market reference. The actual purchase is you finding a listing near that price – set alerts, watch for fresh listings, or buy at a local shop near guide and ship to the cheap-fee venue’s buyers.
- CARDMARKET “AVG_7D” and “AVG_30D” are your reality check. A single high ask means nothing if the 7-day average says the card trades 30% lower. The scanner prints both. Only the averages tell you what it will actually sell for.
- The LOCAL row is your floor, not your target. Selling into a 30% haircut is what happens when you get impatient. Its job in the math is showing you the worst honest exit.
The risks (the part that decides if you eat)
Shipping and grading are the silent fee. The table above is platform fees only. A raw card shipped from a US seller to a EU buyer runs $15-25 tracked, plus your time at the post office. On a $30 card, that is 50-80% of a nominal edge. This strategy only pays on cards above roughly $100, or on batches shipped together. Cards damaged in transit are your loss; buyers on all four venues are merciless about this.
Asking prices are not transactions. Every observation in the scanner’s output marked ASKING is somebody’s listing, not a sale. Thin markets show heroic asks that sit for months. That is why the 7-day average exists in the output – an edge computed off a stale $575 ask against a $336 7-day average is a fantasy, and the scanner shows you both numbers precisely so you can catch yourself doing this.
Terms of service and capital lockup. Multi-accounting on a single venue gets you banned; running one seller account per venue, as described here, is normal marketplace behavior – but read each venue’s seller terms before you scale. Meanwhile your capital is in a card that takes days to arrive and up to two weeks to sell. Edge you cannot deploy repeatedly is not income, it is a lottery ticket. Budget for the cycle: buy -> ship -> list -> sell -> payout. On cross-continent routes, count on 2-4 weeks per rotation.
FX is a position. Holding EUR proceeds waiting for a better rate is currency trading with extra steps. Withdraw promptly, or accept that you are running two strategies and only know how to price one.
The honest scale. A single scan finding one or two $25+ edges on mid-value cards is a normal week, not a miracle. People who run this as a system – scanner on a schedule, capital rotating through 5-15 cards at a time, discipline about the averages – clear low four figures a month. People who buy one Beardless Professor on vibes clear nothing. The scanner is the difference: it turns “card prices vary” into a queue of routes with known costs, and you work the queue.
Deploy it
- Get the free key: one POST to the API’s signup endpoint with your email – no card, no dashboard – or use their web page. Stick the key in
PTCG_API_KEY. - Save the script, run it against any card name:
python3 tcg_cross_market_scanner.py "charizard ex" --min-edge 25. - Put it on a cron. Once an hour off one free trial key is inside the daily cap after email verification; a paid tier is $29/month once the edges you’re chasing justify it.
- Track your actual fills against the scanner’s claims in a spreadsheet. The gap between predicted and realized edge is your real education – and it gets smaller every month you do this.
One last operational note: run the scan, then wait. Edges on a $300 card do not require you to react in seconds – unlike crypto, no bot will beat you to a mispriced Charizard listing within the hour, because the buyer pool for a $300 trading card is a few hundred humans who sleep at night. A scan every 30-60 minutes catches the same moves a real-time feed would, at zero marginal API cost and without burning your daily credit cap on duplicate rows. Patience is a fee you don’t pay.
The five prices on one card are not a glitch. They are the standing wage paid to whoever does the boring work of being in four markets at once. The scanner just makes sure you’re the one getting paid.