Building a TCG price API solo: asking price is not sold price
I build Flipzi, a price API for trading cards, on my own. It began as a collection tracker, where I kept running into the same problem: a card's visible price usually describes what a seller wants, not what a buyer paid. That distinction became the central design decision behind the API. A card has

at least two useful prices, and applications need to know which one they are showing. This tutorial retrieves both values for the same card, checks the evidence attached to them, and exposes the same workflow to an AI assistant through MCP. A card listed at EUR 2,275 tells you what a seller hopes for. It does not tell you what buyers pay.
For a collection tracker, store tool, or pricing bot, the second number is often more useful, and harder to calculate. Completed sales are spread across eBay sites in different countries and currencies. Listing titles are free text, often written in the seller's language, and grades change the value dramatically. Flipzi separates these concepts: /price returns the current asking price and names the market it reflects.
/sold returns a price blended from completed eBay sales across up to eight eBay sites, per grade. Python 3 The requests package A Flipzi API key The free plan covers search and current ungraded prices. The sold-price endpoint used later in the tutorial requires Pro or above. pip install requests export FLIPZI_KEY='fz_live_...' Card IDs are UUIDs, so a lookup usually begins with search.
import os import sys import requests BASE = "https://data.flipzi.io/v1" session = requests.Session() session.headers["X-API-Key"] = os.environ["FLIPZI_KEY"] def get(path, **params): response = session.get(BASE + path, params=params, timeout=15) if response.status_code != 200: error = response.json()["error"] sys.exit( f"{response.status_code} {error['code']}: {error['message']}" ) return response.json()["data"] for card in get( "/cards/search", q="umbreon vmax", game="pokemon", page_size=5, ): print( card["id"], card["name"], card["set"]["name"], "#" + card["number"], card["rarity"], ) Sample output: a677e10e-1197-4ae7-b8fa-2117f84d1c9b Umbreon VMAX Evolving Skies #215 Secret Rare 13507a53-1efe-4b04-ad8c-43a5bfe269ff Umbreon VMAX Brilliant Stars #TG23 Ultra Rare f566afa2-b52d-4231-afb0-5fedbc44eb92 Umbreon VMAX Evolving Skies #95 Holo Rare VMAX e5b791b0-5095-4876-80a4-113280d0c0a8 Umbreon VMAX Evolving Skies #214 Secret Rare Four cards share the name. Pick by set and number rather than by position. The rest of this example uses Evolving Skies #215. One easy-to-miss detail: Python's standard-library urllib client may be refused at the edge when it sends its default User-Agent.
The requests client works as shown above. If you use urllib, set a User-Agent of your own. CARD = "a677e10e-1197-4ae7-b8fa-2117f84d1c9b" price = get(f"/cards/{CARD}/price") asking = {item["grade"]: item for item in price["prices"]} raw = asking["RAW"] print( raw["price_formatted"], "on", raw["market"], "| confidence", raw["confidence"]["score"], "| updated", raw["hours_ago"], "hours ago", ) Example output from 16 October 2026: EUR 1,499.99 on cardmarket | confidence 100 | updated 8 hours ago A price without context is not very useful. I would read these three fields every time: market identifies the market behind the asking price.
confidence.score runs from 0 to 100, so your application can set a minimum threshold. hours_ago tells you how stale the value is. Prices are integer cents in price_cents. Use that field for arithmetic and price_formatted for display.
A German seller lists Glurak, not Charizard. Pass lang and search by the localized name: for card in get( "/cards/search", q="glurak", game="pokemon", lang="de", page_size=3, ): print(card["name"], "|", card["set"]["name"], "#" + card["number"]) Charizard | Expedition Base Set #39 Charizard | Power Keepers #6 Charizard | PokΓ©mon GO #010 The returned IDs are the same IDs used by English search, so the rest of the workflow does not change. The sold-price endpoint requires Pro or above. On a free key it returns 403 PLAN_FEATURE_LOCKED, and the refused request is not billed.
sold = { item["grade"]: item for item in get(f"/cards/{CARD}/sold")["sold"] } for grade in ("RAW", "PSA_9", "PSA_10"): ask = asking.get(grade) paid = sold.get(grade) if not ask or not paid: print(f"{grade:7} no price on one side") continue print( f"{grade:7} asking {ask['price_formatted']:>10}" f" sold {paid['price_formatted']:>10}" f" {paid['comps_30d']} sales in 30 days" f" on {paid['marketplace_count']} eBay sites" ) Output from 16 October 2026: RAW asking EUR 1,499.99 sold EUR 1,602.91 60 sales in 30 days on 5 eBay sites PSA_9 asking EUR 2,180.00 sold EUR 1,835.26 11 sales in 30 days on 3 eBay sites PSA_10 asking EUR 2,275.00 sold EUR 3,708.79 43 sales in 30 days on 5 eBay sites The direction of the difference is not consistent. The PSA 9 sold below the current asking price. The PSA 10 sold about EUR 1,434 above it. An app that collapses those values into a single number loses important information.
It may undervalue one grade and overvalue another. comps_30d is also important. Eleven sales are evidence, but 43 give you more reason to trust the aggregate. I would show the sample size next to the price rather than hiding it.
The API also has a local MCP server, so an assistant can make the same read-only calls using your own key. For Claude Code: claude mcp add \ --env FLIPZI_API_KEY=fz_live_... \ --transport stdio \ flipzi -- npx -y @flipzitcg/mcp For Claude Desktop or Cursor, use the equivalent configuration: { "mcpServers": { "flipzi": { "command": "npx", "args": ["-y", "@flipzitcg/mcp"], "env": { "FLIPZI_API_KEY": "fz_live_..." } } } } Then ask: What is Umbreon VMAX, Evolving Skies 215, worth right now? The assistant can search for the card ID and retrieve the same price JSON used above.
Three guardrails in the MCP wrapper are worth calling out: It exposes all 11 read-only tools, while plan-locked calls explain which plan is required. It limits a session to 150 units by default, so a looping agent cannot consume the whole monthly allowance. Set FLIPZI_MCP_MAX_UNITS to change the limit. Its account tool costs no units, allowing the assistant to check the key's capabilities before it tries a paid endpoint.
The server is MIT-licensed, and its source is available on GitHub. There are no official SDKs, GraphQL API, or webhooks. The OpenAPI specification can be used to generate a client. The free plan does not include sold comps, history, graded prices, or commercial use.
The practical lesson is simple: do not label every number βprice.β Name the market, distinguish asking from sold, include freshness and confidence, and show the number of comparable sales behind an aggregate. To run the code above, get a free key. If a price looks wrong, post the card ID in the comments; that is the most useful bug report you can send. I wrote this article with help from an AI assistant and checked its factual claims against the product and launch notes before publishing.
Key Takeaways
- β’This tutorial retrieves both values for the same card, checks the evidence attached to them, and exposes the same workflow to an AI assistant through MCP.
- β’This story was reported by Dev.to, covering developments in the dev space.
- β’AI advancements continue to reshape industries β read the full article on Dev.to for complete coverage.
π Continue reading the full article:
Read Full Article on Dev.to βShare this article



