Skip to content
pokemontcgapi.com

agents

Give an AI agent Pokémon card data

Expose search, lookup and prices as tools an assistant can call, so answers are grounded in data with a date attached instead of recalled.

Who this is for

You are building an assistant, a chat feature, or a retrieval pipeline, and you want it to answer card questions from data rather than from memory.

Plan you need

The free tier is enough to build and test an agent. Tool calls are ordinary requests, so an agent that searches before answering costs a handful of them per conversation.

Language models are confidently wrong about card prices and quietly wrong about card details, and both failure modes are worse than not answering. Prices move weekly and were never in the training data at the precision anyone wants; rules text and set membership are memorised approximately, which produces answers that read correctly and name a card that does not exist. Neither problem is fixable by prompting.

It is fixable by giving the model a way to look things up. Three tools — search, lookup, prices — cover nearly every card question a person asks, and because the responses carry ids, dates and provenance, the answers the model composes can carry them too. The result is an assistant that says "as of 16 August, the index puts it at 389.90 EUR, from 64 observations" instead of a number it invented.

The code

Three tool definitions in the shape most function-calling APIs accept, plus the handler behind them. The descriptions say when to call each tool, not just what it does, because that is what drives the model to call them at the right moment.

tools.json
[
  {
    "name": "search_pokemon_cards",
    "description": "Search the Pokémon TCG catalogue by name, type, set, rarity or artist. Call this whenever the user names a card without giving an id, or asks a question that depends on which cards exist — 'what Charizards are in Obsidian Flames', 'Fire-type Stage 2 cards', 'cards illustrated by Mitsuhiro Arita'. Returns a short list of matches with their ids; call get_pokemon_card afterwards for full detail on the one the user meant. Do not answer from memory: card names repeat across 25 years of printings and the set is usually the part that matters.",
    "input_schema": {
      "type": "object",
      "properties": {
        "query": {
          "type": "string",
          "description": "Fielded query, e.g. name:charizard set.id:sv3, or types:Fire subtypes:\"Stage 2\". Quote multi-word values."
        },
        "limit": {
          "type": "integer",
          "description": "How many matches to return. Keep it small — 5 is usually enough to disambiguate.",
          "default": 5
        }
      },
      "required": ["query"],
      "additionalProperties": false
    }
  },
  {
    "name": "get_pokemon_card",
    "description": "Fetch one card by its id — the printed coordinate, e.g. base1-4 or sv3-223. Call this once search_pokemon_cards has told you which printing the user means, or when the user supplies an id directly. Returns names in six languages, attacks and abilities as structured data, weaknesses, resistances, retreat cost, rarity, regulation mark, format legality, artist and image URLs.",
    "input_schema": {
      "type": "object",
      "properties": {
        "id": { "type": "string", "description": "Card id, e.g. base1-4." },
        "include_prices": {
          "type": "boolean",
          "description": "Attach current prices. Set true only when the user asked about value.",
          "default": false
        }
      },
      "required": ["id"],
      "additionalProperties": false
    }
  },
  {
    "name": "get_pokemon_card_prices",
    "description": "Current prices for one card. Call this for any question about what a card is worth, sells for, or costs — never answer a price question from memory, prices move weekly. Every result carries source, basis (a completed sale, an open listing, a published guide value, or a derived figure), sample size and capture date. Quote the date and the basis in your answer; a bare number without them is not a usable answer.",
    "input_schema": {
      "type": "object",
      "properties": {
        "id": { "type": "string", "description": "Card id, e.g. base1-4." },
        "currency": {
          "type": "string",
          "enum": ["EUR", "USD", "GBP", "JPY"],
          "description": "Currency to report in. Converted figures are flagged as converted.",
          "default": "EUR"
        }
      },
      "required": ["id"],
      "additionalProperties": false
    }
  }
]

Write descriptions that say when, not just what

A tool description is the only thing the model reads when deciding whether to call it. "Searches for cards" tells it what the tool does and nothing about when the moment has arrived. "Call this whenever the user names a card without giving an id" is a trigger condition, and trigger conditions are what move an assistant from answering from memory to actually looking things up.

The same applies to the negative case. Saying plainly that prices must never be answered from memory, because they move weekly, is more effective than any amount of system-prompt instruction, because it sits next to the tool that solves the problem. Parameter descriptions matter too: a query field that shows the fielded syntax by example gets well-formed queries, and one that says "the search query" gets prose.

Field projection is a token budget

A complete card object carries attacks, abilities, translations, images and external ids. That is the right payload for an application and the wrong one for a context window — five search results at full width is most of a small context spent on fields the model will never mention. Trim the search response to what disambiguation needs: id, name, set, number, rarity, release date.

Then let the model widen. Once it knows which printing the user means, the lookup returns the full object for one card, which is a reasonable cost for a specific question. This two-step shape — cheap search, expensive lookup — mirrors how a person uses a card database and keeps a conversation that touches a dozen cards from filling the window before it gets to an answer.

Return errors, do not throw them

When a tool call fails, the useful thing to hand back is the error, not an exception that kills the turn. Errors here are machine-readable: a stable code, a message that names the offending value, and details that list the valid options. A model that receives "unknown field hitpoints, valid fields are id, name, hp, …" corrects itself on the next call. One that receives a generic failure gives up or invents an answer.

The same reasoning covers empty results. Returning "no matches" with the query that produced it lets the model widen the search or ask the user a clarifying question. Returning nothing at all reads as a broken tool, and the most common recovery behaviour from there is to answer from memory — which is precisely the failure you added the tool to prevent.

What usually goes wrong

Not a disclaimer list: these are the failure modes that cost people time on this specific integration.

Stripping provenance to save tokens
The date, the basis and the sample size are the difference between a grounded answer and a confident one. They cost a handful of tokens. Keep them, and instruct the model to quote them.
One mega-tool with a mode parameter
A single tool with an action field is harder for a model to call correctly than three tools with clear boundaries. Split by intent — find, fetch, value — and let the descriptions do the routing.
No cache in front of the tools
Agents re-ask. A short in-process cache keyed by request path, revalidated with ETags, removes most repeat calls in a conversation and keeps a chatty assistant well inside a free tier.

Endpoints used

  • GET/v1/cardsThe search tool: fielded query, small limit, trimmed field set.
  • GET/v1/cards/{id}The lookup tool: full detail once the printing is known.
  • GET/v1/cards/{id}/pricesThe price tool: numbers that arrive with their provenance.
  • GET/v1/setsGround questions about blocks, release dates and what is current.

Related