This is a submission for the Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle
What I Built
Someone walks into a small TCG shop in Mexicali with a shoebox of Pokémon cards and asks: how much will you give me?
Today the employee answers card by card. They figure out which print it is (a 151 Charmander looks almost exactly like the Paldean Fates one), look up the TCGplayer price, convert dollars to pesos, adjust for condition, and apply the shop's percentage. A few hundred cards take a long time, and two employees can come up with two different offers for the same box.
Buylist Scanner turns that into a few photos:
- The employee photographs one card or a whole 9-pocket binder page with their phone.
- The shop's PC finds each card, straightens it, and identifies it against the English catalog: 20,448 cards. Most come from TCGdex; 785 that TCGdex has no image for come from pokemontcg.io.
- It reads the printed set code and collector number to tell same-art reprints apart, and flags cards printed in Spanish or Japanese so a person checks them.
- It looks up the TCGplayer market price, converts it at Banxico's official rate for the day, and applies the shop's condition factors: 85% in cash, 90% in store credit.
- The phone shows a customer summary with every card's offer, the totals, and the exchange rate with its date. It exports to CSV for inventory.
Who pays: small TCG shops that buy collections and take trades. The plan is a monthly fee per shop for installing, updating the catalog, and support.
Testing it with real shops
At the counter, October 14–15
From the evening of October 14 to midday on the 15th, I used it on real cards from a phone:
| Real use, Oct 14–15 | |
|---|---|
| Phone photos | 40 |
| Card identifications | 84, binder pages of up to 9 cards per photo (some pages photographed more than once) |
| Time per photo, photo to identified cards | median 1.6 s after the fixes below (3.7 s before); slowest 7.8 s, a full 9-card page |
| Buy sessions | 12, offers in pesos at Banxico's rate of the day |
| Cards the employee removed or corrected | 5 removed, 3 corrected |
| Blurry photos rejected instead of guessed | 12, in the first minutes |
At a card expo in Mexicali, October 17
Two days later there was a Pokémon card expo here in Mexicali. I brought the scanner and demoed it to two different stores at their tables, scanning their own cards and pricing them on the spot. The server ran on my laptop, and my phone connected to it over Wi-Fi.
From the server log of one of those demos:
| Card expo, Oct 17 | |
|---|---|
| Phone photos | 10, in under 4 minutes |
| Cards identified | 42, including full 9-card binder pages |
| Time per photo | 0.7–1.5 s for a single card, 4.1–5.5 s for a 9-card page |
| Buy session | 29 cards, $4,678 MXN in cash offers at Banxico's rate of 18.493 |
| Cards without a price | 5: four from the brand-new 30th Celebration set, plus a promo neither price source covers yet |
| Photos it couldn't read | 2, answered "no card found" instead of a guess |
What the shops saw: how quickly it identifies cards and prices them in pesos. People from both stores showed strong interest in using it in their own shops, and I left with two contacts who want a trial in their store or at their next events.
What didn't work: some cards it couldn't identify at all, and some catalog gaps showed up, mostly very new cards that don't have prices yet. Those are the next fixes.
What I don't know yet: whether they'll pay, and how much. I didn't ask at the expo; that's the first question for the trials.
Demo
Accuracy on my labeled test photos. All on a laptop CPU limited to 4 threads:
| Result | |
|---|---|
| Correct card ranked 1st, 41 real counter photos | 100% (image search alone: 97.6%) |
| Same-art reprints told apart by the printed number | 10/10 synthetic pairs, 15/15 real photos |
| Prices found for the same 41 cards | 39/41 (the other 2 are from a set released a month ago) |
| Blurry photo / empty binder page | Clear error in Spanish, never a made-up card |
It isn't perfect, and it says so. A card it isn't sure about shows "¿Cuál es?" with 3 options instead of guessing: a near-tie, a card printed in Japanese, a card with no catalog image. The employee taps the right one, and that correction is saved to improve matching later. Showing uncertainty honestly matters more here than a confident wrong price.
Code
TCG Buylist Scanner
A Pokémon card scanner for small TCG shops in Mexicali, Mexico. An employee takes a photo of one card or a binder page with their phone. The shop's PC identifies each card, looks up its market price, and calculates the buy offer in cash and in store credit, using the shop's own rules.
Everything runs on the shop PC: open models, a local card index, and no cloud vision API. Customer photos never leave the shop. Only price and exchange-rate lookups use the internet, and both are cached so the counter keeps working offline.
How it works
flowchart TB
subgraph identify["Identify (on the shop PC)"]
direction LR
photo([Photo]) --> crop["Crop<br/>1–9 cards"]
crop --> search["Image search<br/>DINOv2 + FAISS"]
search --> ocr["OCR re-rank<br/>set code, number, language"]
ocr --> card([Card + confidence])
end
subgraph offer["Price and offer"]
direction LR
price["TCGplayer market price<br/>TCGdex, pokemontcg.io backup<br/>cached daily"]
price --> fx["× exchange rate<br/>Banxico…How I Built It
flowchart LR
P[Phone PWA] -- photo --> C[Crop 1–9 cards<br/>OpenCV]
C --> E[DINOv2 embeddings<br/>whole card + artwork]
E --> F[FAISS search<br/>20,448 cards]
F --> O[PaddleOCR<br/>set code · number · language]
O --> R[Ranked match +<br/>review flags]
R --> PR[TCGplayer price<br/>via TCGdex, cached daily]
PR --> FX[Banxico FIX rate]
FX --> OF[Offer: × condition ×<br/>85% cash / 90% credit]
OF --> P
Picking the model with real photos, not vibes. I photographed cards at the counter (in sleeves, in binders, with glare, lying sideways) and compared CLIP ViT-B/32 with DINOv2 ViT-B/14 on the same photos. CLIP groups cards by theme ("blue water Pokémon"), so its misses looked plausible but were wrong. DINOv2 keeps the fine detail of the artwork. Embedding the artwork box separately from the whole card raised top-1 from 84% to 91%, because foil, promo stamps and sloppy crops change the rest of the card but not the art.
Reprints need text. Some cards are reprinted with the exact same artwork (151 → Prismatic Evolutions, Paldean Fates). No image model can tell those apart, so PaddleOCR reads the bottom strip. Modern cards print MEW EN 004/165, but OCR returns things like PREEN, SYIEJ203/198 or CRLENAPOPE. The parser handles look-alike characters and merged tokens.
A local detail a generic API would miss. The Spanish cards sold in Mexico are Latin American prints, tagged LA next to the set code, not ES. I only found out by looking at the photos.
Prices. I started with pokemontcg.io. It had no prices for the newest sets and kept returning 502s: 19 of 41 cards priced, 68 s. Then I found that TCGdex, the open catalog I already used for images, publishes the same TCGplayer prices under the same card ids. 39 of 41 priced in 7 s, with pokemontcg.io kept as a backup. Every offer stores its price, source, date and exchange rate, so the shop can always explain a number.
Phone app. A React + Vite PWA, all in Mexican Spanish, with big buttons for one-handed use. If the Wi-Fi to the shop PC drops, the app says so, keeps the purchase on the phone, and sends queued changes when the server comes back. A removed card can be brought back with Deshacer for a few seconds.
What the first real test taught me
The counter was harsher than my test photos. Every problem below was found and fixed within the same day:
- A confident wrong answer. A Radiant Jirachi in a binder came back as Sneasel ex, without asking: 0.797 vs. 0.786. The sparkly Radiant finish fooled the artwork comparison, and OCR never read the number because the binder pocket pushed it out of the strip it was reading. Now a near-tie that the printed text doesn't settle always asks, and OCR reads a second, higher strip when the first finds no number. On 40 real crops that read 21 numbers instead of 15, and the Jirachi is now right in every photo.
- Glare hid 4 of 9 cards. Light on the plastic pockets erased card outlines. Now, when at least 4 cards form a grid, the missing pockets are inferred from the spacing and kept only if they contain a card. Empty pockets measure almost no edges. The same page went from 5 to 9 of 9 cards, and a page with empty pockets doesn't get invented cards.
-
A misread number. OCR read
112/086instead of117/086, three different sets have a card #112 of 86, and one of them was added without asking. Now a number alone has to point to exactly one card. - Cards that weren't in the catalog at all. A Team Rocket's Wobbuffet promo and HGSS-era Suicune and Entei promos kept failing. TCGdex lists them, but without an image. pokemontcg.io has images for many of those gaps, so a script now fills them: +785 cards, including whole sets like Shining Legends, Dragon Majesty and the Trainer Galleries. All three are recognized now.
Why Does Open Innovation Matter?
The customer's collection stays in the store. People bring cards they've had since they were kids. With DINOv2 and PaddleOCR running on the shop's own PC, no photo goes to a third-party API. Only price and exchange-rate lookups use the internet, and both are cached.
The business only works if a scan costs nothing. In under a day of testing I made 84 card identifications from 40 photos, and a real collection is hundreds of cards. A 500-card collection would be 500 paid vision-API calls, and a shop sees several collections a week. Card recognition services with per-scan pricing set the price for your product. Open weights on a CPU the shop already owns make the marginal cost zero.
It keeps working if the internet drops. Identification is fully local; prices and the exchange rate fall back to the daily cache and are marked as stale.
Open data made the fixes possible. Every problem from the first real test was fixed in hours, because every piece is open: the model, the OCR, the catalog. I could read the OCR output, add the Latin American LA tag, infer hidden binder pockets, and fill missing catalog images from a second open source. A closed recognition API would have given me a wrong answer and nothing to debug. Compare the cloud approach in Is this even a valid card? Zero-shot image classification in a Lambda container and What does the card say? Text extraction using Amazon Nova 2 Lite. Both are neat, but every card goes to someone else's servers, at someone else's price.
The honest trade-offs:
- Setup: the first run needs internet to download the catalog and model weights (about 2 GB), and the index takes about 35 minutes to build on a laptop. New sets are added in about a minute.
- Cards with no image anywhere: about 800 English cards have no image from either source yet (MEP promos, My First Battle, trainer kits), so those come back marked for manual review.
- Brand-new sets: cards can be identified before TCGplayer has a price for them, so the employee types the price by hand until it shows up.
-
Spanish prints: some aren't flagged as Spanish yet when OCR can't read the
LAtag.
Next: trials with the two contacts from the expo, in their stores or at their next events, where I'll ask the question I still can't answer: what a shop would pay for this. Before that, fix the cards it couldn't identify at the expo.

Top comments (0)