NeoSatoshi Card Recognition
by Cookiebreed
README.md
# NeoSatoshi Card Recognition API
Card recognition API for Pokémon, One Piece, Disney Lorcana, Yu-Gi-Oh!, Magic: The Gathering, Star Wars: Unlimited and football (soccer) cards: send a photo, get JSON with set, number, rarity, Cardmarket ID and daily prices (football: player, release and parallel).
This repository contains runnable examples (curl, Python, Node.js), an MCP server for AI assistants, a Postman collection and a copy of the OpenAPI spec. The API is run by [NeoSatoshi](https://neosatoshi.com) (NeoGen Solutions GmbH, Bern, Switzerland).
* Endpoint: `POST https://europe-west1-neosatoshi.cloudfunctions.net/detectApi`
* Auth: `Authorization: Bearer <API_KEY>`
* Public demo key (no signup, 10 detections per day per network): `nsk_live_9c253fbed995042a98280ad13e5c47d3c4a59c9b4f0f7274`
* Docs: https://neosatoshi.com/developers/detection-api
* Card search and corrections: `POST .../cardsApi/search`, `POST .../cardsApi/corrections`, `GET .../cardsApi/cards/{tcg}/{card_id}` (see [Card search and corrections](#card-search-and-corrections))
* OpenAPI: [openapi.json](openapi.json) (also at https://neosatoshi.com/openapi.json)
## Quickstart (30 seconds)
From a clone of this repository (or use any photo of a card). Works on macOS and Linux as is:
```bash
{ printf '{"tcg":"pokemon","image":"'; base64 < examples/images/example_api_single.jpg | tr -d '\n'; printf '"}'; } > body.json
curl -sS --max-time 120 -X POST https://europe-west1-neosatoshi.cloudfunctions.net/detectApi \
-H "Authorization: Bearer nsk_live_9c253fbed995042a98280ad13e5c47d3c4a59c9b4f0f7274" \
-H "Content-Type: application/json" \
--data-binary @body.json
```
One card usually takes about 4 seconds, about 10 to 12 seconds on the first request after a quiet period; a binder page with 9 cards about 17 seconds. Keep the client timeout at 120 seconds or more.
Ready-made scripts that print a per-card summary:
```bash
./examples/curl/detect.sh examples/images/example_api_single.jpg
python3 examples/python/detect.py examples/images/example_api_lorcana_de.jpg lorcana
node examples/node/detect.mjs examples/images/example_api_multi.jpg pokemon --json
```
All scripts read `NEOSATOSHI_API_KEY` and fall back to the demo key.
## Streaming
Add `"stream": true` to get Server-Sent Events: each card as soon as it is recognised, then the same result as a normal call, at the same cost. On a Pokémon binder page with 9 cards the first card came after about 6 seconds and all nine after about 17. Football (one card per photo) gets progress events, not a faster result.
```bash
./examples/curl/stream.sh https://neosatoshi.com/static/images/example_api_multi.jpg
python3 examples/python/stream.py https://neosatoshi.com/static/images/example_api_multi.jpg
node examples/node/stream.mjs https://neosatoshi.com/static/images/example_api_lorcana.jpg lorcana
```
Events: `accepted`, `progress`, `card` (`index`, `card`), then exactly one of `done` (same body as the JSON response) or `error` (`status`, `error`, `message`). A `: keep-alive` comment comes every 15 seconds. Errors before the detection starts stay plain JSON with their status code. A stream cannot be resumed: a reconnect is a new, charged detection. Details: [documentation](https://neosatoshi.com/developers/detection-api#streaming).
## Supported games and languages
| Game | `tcg` value | Card languages | Cards per photo |
|---|---|---|---|
| Pokémon TCG | `pokemon` (default) | English, German, French, Italian, Spanish, Japanese, Korean, Simplified Chinese | Several (binder pages work, best up to about 20) |
| One Piece Card Game | `onepiece` | English, Japanese | Several (binder pages work) |
| Disney Lorcana | `lorcana` | English, German, French, Italian | Several (binder pages work) |
| Yu-Gi-Oh! TCG (not OCG) | `yugioh` | English, German, French, Italian, Spanish, Portuguese | Several (binder pages work) |
| Magic: The Gathering | `magic` | English, German, French, Italian, Spanish | Several (binder pages work) |
| Star Wars: Unlimited | `star_wars_unlimited` | English, German, French, Italian, Spanish | Several (binder pages work) |
| Football (soccer) cards: Topps, Panini and other makers | `football` | (no card language) | One, the card front, at most 4 MB |
## Request
`POST` with a JSON body:
```json
{ "image": "<base64 JPEG or PNG, max 12 MB>", "tcg": "pokemon" }
```
| Field | Required | Description |
|---|---|---|
| `image` | one of `image` or `image_url` | Base64 encoded JPEG or PNG, at most 12 MB (football: 4 MB). |
| `image_url` | one of `image` or `image_url` | Public `https://` URL of a JPEG or PNG, at most 12 MB, fetched by the API. Alternative to `image`. |
| `tcg` | no | `pokemon` (default), `onepiece`, `lorcana`, `yugioh`, `magic`, `star_wars_unlimited` or `football`. |
| `stream` | no | `true` for Server-Sent Events, see [Streaming](#streaming). The header `Accept: text/event-stream` does the same. |
## Response
Recorded response for [examples/images/example_api_single.jpg](examples/images/example_api_single.jpg) (Pokémon, Cinderace ex):
```json
{
"request_id": "req_adec9d16-14dc-40a8-9d8c-2df9de2ad3a5",
"count": 1,
"cards": [
{
"name": "Cinderace ex",
"name_ja": null,
"name_ko": null,
"name_zh": null,
"language": "en",
"set": { "id": "svp", "ptcgo_code": "PR-SV", "name": "Scarlet & Violet Black Star Promos", "number": "163" },
"rarity": "Promo",
"cardmarket_id": 796939,
"reverse_holo": false,
"first_edition": false,
"special_variant": null,
"prices": { "cardmarket": { "trend": 3.59, "low": 1, "avg7": 3.71, "avg30": 4.1, "currency": "EUR" } },
"images": {
"small": "https://d1m1gwcombja0u.cloudfront.net/public_images/cards_en/svp/svp-163_small.webp",
"large": "https://d1m1gwcombja0u.cloudfront.net/public_images/cards_en/svp/svp-163.png"
},
"confidence": 0.85,
"image_quality": { "blurred": false, "unusual_dimensions": false },
"alternatives": [
{ "name": "Cinderace ex", "set": "Stellar Crown", "number": "28", "score": 1, "possible_variants": null }
]
}
],
"demo": { "limit_per_day": 10, "remaining_today": 9 }
}
```
(Shortened; the full recording is in [examples/responses/pokemon_single_cinderace.json](examples/responses/pokemon_single_cinderace.json).) For the 9 card binder page in [example_api_multi.jpg](examples/images/example_api_multi.jpg) the API returns every card in `cards`, one entry each.
More recordings:
* [lorcana_en_genie.json](examples/responses/lorcana_en_genie.json): Disney Lorcana, English, photo [example_api_lorcana.jpg](examples/images/example_api_lorcana.jpg)
* [lorcana_de_blume.json](examples/responses/lorcana_de_blume.json): Disney Lorcana, German (foil detected), photo [example_api_lorcana_de.jpg](examples/images/example_api_lorcana_de.jpg)
* [football_beste_autograph.json](examples/responses/football_beste_autograph.json): football, a numbered autograph (Gold Wave Refractor 37/50), shortened to two of its four alternatives
* [catalog-shape/](examples/responses/catalog-shape/): One Piece, Lorcana, Yu-Gi-Oh! and Magic responses recorded from clean catalog images, included only to show the response shape per game (the catalog images themselves are not in this repo)
### Fields shared by every game
| Field | Description |
|---|---|
| `request_id` | ID of the request, quote it in support questions. |
| `count` | Number of detected cards. |
| `cards[]` | One object per detected card (see below). |
| `credits.remaining` | Remaining card scans of your key's plan (own keys only). |
| `demo` | Public demo key only, instead of credits: `limit_per_day` and `remaining_today` for your network. |
| `name` | Card name as printed. |
| `language` | Card language: `en`, `de`, `fr`, `it`, `es`, `pt`, `ja`, `ko`, `zh`. |
| `tcg` | `onepiece`, `lorcana`, `yugioh`, `magic`, `star_wars_unlimited` or `football`. Absent for Pokémon. |
| `set` | `id`, `ptcgo_code` (Pokémon, One Piece, Lorcana) or `code` (Yu-Gi-Oh!, Magic), `name`, `number`. |
| `rarity` | Rarity as named by the game. |
| `cardmarket_id` | Cardmarket product ID, or null. |
| `prices.cardmarket` | `trend`, `low`, `avg7`, `avg30` in EUR, updated daily. |
| `prices.tcgplayer` | TCGplayer prices in USD, where available. |
| `images` | `small` and `large` reference image URLs. |
| `confidence` | 0 to 1. |
| `image_quality` | `blurred`, `unusual_dimensions` (booleans about the photo). |
| `alternatives[]` | Other likely matches for a manual pick. The shape differs per game. |
A missing price is `null`, never `0`.
### Game-specific fields
| Game | Fields |
|---|---|
| Pokémon | `name_ja`, `name_ko`, `name_zh`, `reverse_holo`, `first_edition`, `special_variant` |
| One Piece | `card_id` (for example `OP01-003`) |
| Disney Lorcana | `version`, `full_name`, `name_en`, `prices.cardmarket_foil` |
| Yu-Gi-Oh! TCG | `passcode`, `set.code`, `edition`, `price_versions`, `*_unconfirmed` flags (for example `print_unconfirmed`: the exact print could not be confirmed, check `alternatives`) |
| Magic: The Gathering | printing (`set.code`, `set.number`, `print_unconfirmed`), `prices.cardmarket_foil`, TCGplayer foil price (`prices.tcgplayer.foil`) |
| Football | `card_id` (`fb-...`), `season`, `subset`, `teams`, `rookie`, `card_type`, `sure`, `score`, `parallel` (`pick` only when the recognizer is sure, `candidates` with `print_run`, `serial_number`). `prices` and `images` are null: no football prices in the API yet, and no reference images. |
Graded slabs: a photo of a PSA, BGS or CGC slab (football also SGC) fills `graded` with the grader, the grade and the cert number as read from the label; `null` for a raw card. The grade is never estimated.
The full field reference is in the [documentation](https://neosatoshi.com/developers/detection-api).
## Errors
Errors return JSON with `error` (a code) and usually `message`.
| HTTP | `error` | Meaning |
|---|---|---|
| 400 | `missing_image`, `bad_image`, `image_too_large`, `unsupported_image`, `unsupported_tcg` | Fix the request. Only JPEG and PNG up to 12 MB (football 4 MB). |
| 400 | `bad_image_url`, `image_url_fetch_failed` | Only with `image_url`: the URL is invalid or could not be fetched. |
| 401 | `missing_api_key`, `invalid_api_key` | Check the `Authorization` header. |
| 402 | `out_of_credits` | The plan has no card scans left. |
| 405 | `method_not_allowed` | Only `POST`. |
| 422 | `no_card_detected`, `unreadable_image` (football) | No card recognised. Not charged. |
| 429 | `rate_limited`, `demo_limit_reached`, `football_busy` | Slow down, or the demo key's 10 per day per network are used up, or the key already has 5 football photos in progress (`Retry-After`). |
| 502 | (backend step failed) | Retry. |
| 503 | `football_daily_limit`, `football_unavailable`, `football_busy` | Football only: the daily football capacity is used up (`resets_at`), or the recognizer is unavailable or busy. Not charged; the other games still work. |
| 504 | `detection_timeout` | Often a cold start. Retry. |
## Limits and pricing
* Images: JPEG or PNG, at most 12 MB (football 4 MB). All six trading card games: several cards per photo (binder pages work; Pokémon best up to about 20), one game per photo. Football: one card per photo.
* One card usually takes about 4 seconds, about 10 to 12 seconds on the first request after a quiet period; a football card about 6 to 25 seconds. Use a client timeout of at least 120 seconds.
* Demo key: 10 detections per day per network, no signup. Good for trying the API, not for production.
* Own keys: create them in the dashboard at https://neosatoshi.com/dashboard/settings/settings#api-keys . Any paid plan works; the smallest, Start-Up, costs 19 per month and includes 1,000 card scans. One credit per detected card; images without a card are not charged.
* The API identifies cards. It does not grade card condition.
## Card search and corrections
Base URL: `https://europe-west1-neosatoshi.cloudfunctions.net/cardsApi`, same Bearer key. All six games and football.
* **Correct a detection (free):** every detected card has `card_index`, the response has `correction` with the `request_id`. Within 24 hours, `POST /search` with `request_id`, `card_index` and `q` (a card name, or `set` + `number`) returns up to 5 full card objects; `POST /corrections` with `request_id`, `card_index` and the chosen `card_id` returns the corrected card. 5 searches and 3 corrections per detected card.
* **Card search (paid):** `POST /search` with `tcg` and `q` (or `set` + `number`), optional `language` and `limit` (up to 10). `GET /cards/{tcg}/{card_id}` returns one card, for example to refresh a price. 0.5 credits per call that returns a card, from the same balance as detection; no result is free. An `Idempotency-Key` header makes a repeat free. Daily limit per plan; the demo key allows 10 searches and 3 lookups per day per network.
```bash
curl -X POST https://europe-west1-neosatoshi.cloudfunctions.net/cardsApi/search \
-H "Authorization: Bearer nsk_live_9c253fbed995042a98280ad13e5c47d3c4a59c9b4f0f7274" \
-H "Content-Type: application/json" \
-d '{"tcg": "pokemon", "q": "charizard 4/102", "limit": 3}'
```
**Football:** `{"tcg": "football", "q": "kane chrome 2023"}`, text only (the player, optionally with set or season words), no `language`, no prices yet. Each result carries `ref`; `GET /cards/football/{card_id}?ref=...` (same key, within 24 hours) returns the card with all parallels of its subset and their print runs. Without a valid `ref` the lookup is 400 and free. Football detections cannot be corrected yet.
Details: https://neosatoshi.com/developers/detection-api#card-search
## When to use this instead of a general vision LLM
A general vision model can often read a card name from a photo. Use this API when you need:
* **Structured catalog IDs**: set ID, set code and number in a fixed JSON shape, matched against a card database, plus alternatives when the print is ambiguous.
* **Cardmarket product IDs and daily prices**: `cardmarket_id` and Cardmarket trend, low, 7 day and 30 day averages (TCGplayer where available). General vision LLMs do not return these.
* **Predictable per-card cost**: one credit per detected card, nothing for images without a card.
If you only need a rough description of a photo, a general vision LLM may be enough.
## MCP server (Claude, Cursor, Codex and other AI assistants)
[mcp-server/](mcp-server/) ([npm: neosatoshi-card-mcp](https://www.npmjs.com/package/neosatoshi-card-mcp)) is a stdio MCP server with eight tools: `recognize_cards` (local file, https URL or base64; cards reported one by one, catalog images on request), `recognize_folder` (a folder of photos to a CSV or JSON file), `search_cards`, `correct_card`, `get_card`, `export_cards` (the same custom CSV or JSON file the NeoSatoshi website exports, free), `list_export_presets` and `list_supported_games`, for all six games and football. It is listed in the [official MCP registry](https://registry.modelcontextprotocol.io) as `io.github.Cookiebreed/card-recognition-mcp`. Quick setup for Claude Code:
```bash
claude mcp add neosatoshi-cards -e NEOSATOSHI_API_KEY=your_key -- npx -y neosatoshi-card-mcp
```
Without `NEOSATOSHI_API_KEY` it uses the demo key. See [mcp-server/README.md](mcp-server/README.md) for Claude Desktop, Cursor and Codex, and for the file access restriction `NEOSATOSHI_ALLOWED_DIRS`.
## Repository layout
| Path | Contents |
|---|---|
| [examples/curl/detect.sh](examples/curl/detect.sh) | Bash and curl, summary via `jq` if installed |
| [examples/python/detect.py](examples/python/detect.py) | Python with `requests` |
| [examples/python/stream.py](examples/python/stream.py), [examples/node/stream.mjs](examples/node/stream.mjs), [examples/curl/stream.sh](examples/curl/stream.sh) | Streaming: each card as soon as it is recognised |
| [examples/python/batch_to_csv.py](examples/python/batch_to_csv.py) | Process a folder, write a CSV (name, set, number, rarity, language, cardmarket_id, Cardmarket trend) |
| [examples/node/detect.mjs](examples/node/detect.mjs) | Node.js 18+, built-in `fetch`, no dependencies |
| [examples/images/](examples/images/) | Test photos (testing the API only) |
| [examples/responses/](examples/responses/) | Recorded API responses |
| [mcp-server/](mcp-server/) | MCP server (TypeScript) |
| [postman/](postman/) | Postman collection v2.1 |
| [openapi.json](openapi.json) | OpenAPI 3.1 spec |
| [llms.txt](llms.txt) | Plain-text summary for LLMs |
## FAQ
**Is there a free API to identify Pokémon cards from a photo?**
Yes, for testing: the public demo key `nsk_live_9c253fbed995042a98280ad13e5c47d3c4a59c9b4f0f7274` allows 10 detections per day per network without signup. For more, create your own key on any paid plan.
**Can it recognise several cards in one photo?**
Yes, in all six trading card games: binder pages work (Pokémon best up to about 20 cards per photo; an 8 card Lorcana page took about 8 seconds). One game per photo. Football cards: one card per photo.
**Does it return prices?**
Yes. Each card comes with its Cardmarket product ID and daily Cardmarket prices in EUR (trend, low, 7 and 30 day average), plus TCGplayer prices in USD where available. A missing price is `null`. Football cards have no prices in the API yet.
**Does it recognise Japanese, Korean or Chinese cards?**
Pokémon: Japanese, Korean and Simplified Chinese, besides English, German, French, Italian and Spanish. One Piece: English and Japanese. See the table above for the other games.
**Does it grade card condition?**
No. It identifies which card it is (set, number, rarity, language, variant fields). It does not judge condition.
**Does it recognise football (soccer) cards?**
Yes, with `"tcg": "football"`: Topps, Panini and other makers, one card front per photo (at most 4 MB). It returns the player, the release (set and number), season, subset, teams, rookie flag and the parallel (three candidates with print runs, a pick only when it is sure). No prices yet. `search_cards` and the card search find football cards by player name.
**Does it support Yu-Gi-Oh! OCG (Asian) cards?**
No, only the TCG in English, German, French, Italian, Spanish and Portuguese.
**How long does a request take?**
One card usually takes about 4 seconds, about 10 to 12 seconds on the first request after a quiet period, and a binder page with 9 cards about 17 seconds. Use a 120 second client timeout. 502 and 504 can be retried.
**Can I get the cards one by one?**
Yes. With `"stream": true` each card arrives as a Server-Sent Event as soon as it is recognised. It helps with binder pages; see [Streaming](#streaming).
**What does it cost?**
One credit per detected card, from the plan's monthly card scans. Images without a card are not charged. The smallest paid plan, Start-Up, costs 19 per month and includes 1,000 card scans.
**Can I send an image URL instead of base64?**
Yes, send `"image_url": "https://..."` instead of `"image"`. The URL must be public https and point to a JPEG or PNG of at most 12 MB.
**Can AI agents call it directly?**
Yes. Agents may call the endpoint with the demo key, use the [OpenAPI spec](openapi.json), or use the [MCP server](mcp-server/).
**Where do I get support?**
api@neosatoshi.com . Include the `request_id` from the response.
## License and terms
The code in this repository is MIT licensed (see [LICENSE](LICENSE)). The API itself is a commercial service governed by https://neosatoshi.com/termsofuse . The example images in [examples/images/](examples/images/) may only be used for testing the API.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues