Building a Pokémon Card Scanning & Pricing App
with Expo: Technical Analysis
Card Scanning & Recognition Methods
Developing a reliable Pokémon card scanner involves identifying a card from a camera feed under
various conditions. Below we compare multiple approaches:
• Image-Based Recognition (Feature Matching & CNNs): One strategy is to recognize the card
by its image pattern rather than text. For example, open-source prototypes have combined
OpenCV with perceptual hashing: they detect the card’s outline, apply a perspective transform to
get a flat image, then compute multiple image hashes (average hash, wavelet hash, phash,
dhash) to match against a database of known card images 1 . Using several hash algorithms
and pre-stored hashes of all cards helps improve accuracy and handle different orientations; one
implementation even stored hash variants for flipped or rotated images so cards can be
identified regardless of orientation 2 . This approach can be very accurate when the captured
image is clear 3 . The downside is data: you need a complete image dataset of Pokémon cards
(which can be obtained from sources like the Pokémon TCG API or scans) and storage for their
hashes. On mobile, computing a hash is fast, but comparing against thousands of hashes could
be slow in JavaScript; a native module or server-side lookup might be needed for performance.
Another image-based approach is training a CNN classifier or feature extractor: for instance, a
deep learning pipeline was shown to identify 20,000+ English Pokémon cards using a
combination of object detection, segmentation, and image matching 4 5 . In that project, a
fast instance segmentation model (RTM-Det) finds the card region in real-time, then a hashing or
embedding technique identifies the card among all known cards 5 . A custom CNN could
directly classify the card image, but with potentially thousands of card classes, the model would
be large and require a huge training set. A more feasible variant is using a CNN to generate an
image embedding and performing a nearest-neighbor search in embedding space to match the
closest card image (essentially a learned feature matching). Expo constraints: Running heavy
vision models on-device is challenging in the Expo managed workflow. There is no built-in
OpenCV in Expo, and while you can use TensorFlow.js in React Native, pure JS inference can be
slow for high-resolution images. To use optimized models (TFLite with GPU acceleration), you
would likely need to create a custom native module or use Expo’s EAS Build with a config plugin.
In fact, the community has developed a wrapper to use Google’s ML Kit natively in Expo apps
6 , meaning you can integrate on-device ML by ejecting or using development builds. In
summary, image-based methods can achieve high accuracy (even recognizing foils, full-art, etc.
4 ), and on-device inference avoids network latency. However, implementing this in Expo may
require additional native plugins or a backend service due to performance and model size
constraints.
• OCR-Based Recognition (Text Parsing): Another viable approach is to read the card’s text –
specifically the card name and card number/set – and use those to identify the card. Pokémon
cards have the name prominently at the top and a collector number (e.g. “12/108”) usually in the
bottom corner. By applying OCR (Optical Character Recognition) to the camera image, the app
can extract this text and look up the card in a database. This method is attractive because it
doesn’t require training a custom vision model for every card. If the OCR correctly reads
1
“Charizard – 4/102”, for example, that uniquely identifies the Base Set Charizard. There are open-
source examples of this approach: one project outlines scanning an image for the card, then
reading the card name and set symbol to determine the exact card 7 . The accuracy of OCR can
be quite good for clear images – modern OCR engines (like Tesseract or Google’s ML Kit Text
Recognition) can handle the standard fonts on cards. However, challenges include lighting glare
(holo cards can have shiny text), unusual characters (e.g., Pokémon names with accents or
symbols like “♂/♀”), and small text size for the card number. It may need image preprocessing
(increasing contrast, etc.) to improve results. The required dataset here is just a list of card
names (for spell-checking or validation) and maybe set codes, which is far simpler than
thousands of images. Mobile performance: OCR can run on-device efficiently. Google’s ML Kit
provides on-device text recognition that is optimized for mobile and works offline. With Expo,
you can integrate ML Kit via community modules – for instance, the
react-native-mlkit-ocr or Infinite Red’s MLKit wrapper allows using Google ML Kit in an
Expo app 6 . This would require an EAS build (since Expo Go won’t include it by default), but it’s
a well-trodden path to get fast, native OCR in React Native. The latency for on-device OCR on a
single image is typically a fraction of a second to a couple seconds on older devices – quite
feasible for a scanning workflow. Alternatively, you could use a cloud OCR service (like Google
Cloud Vision or AWS Textract) by sending the image to an API. Cloud OCR might be slightly more
accurate in tough cases and can return structured text, but it introduces network latency and
cost (beyond free tier). Since the user prefers free solutions, on-device OCR is appealing (no per-
scan cost, and images never leave the device – good for privacy). The main limitation is that OCR
alone might confuse cards with similar names or fail to distinguish variant prints (e.g. multiple
Pikachu cards). To mitigate this, you can combine it with other data: for example, if OCR gets the
name but not the set, you might prompt the user or attempt to recognize the set logo via image
classification. Despite these edge cases, a text-first approach is often the simplest to
implement in an Expo app, leveraging known libraries and requiring only that you maintain an
up-to-date card database to map “name + set/number” to a specific card ID.
• Barcode or QR Code Scanning: In many scanning apps, the easiest solution is to scan a barcode
– however, standard Pokémon TCG cards do not have barcodes or QR codes printed on them. The
only QR codes associated with Pokémon cards are on the separate online game code cards found
in booster packs, which are not useful for identifying the card’s name or value. Thus, for
identifying individual cards, barcode scanning isn’t applicable (unlike some sports cards or
graded slabs that might have barcodes). We mention it for completeness: Expo does have a
BarCodeScanner module that can quickly scan QR/barcodes if the context arises (for example,
if you extended the app to scan boxes or products with UPC codes). But for raw cards, you’ll rely
on image or text recognition.
• Pre-trained Vision APIs and Models: Rather than building a recognition system from scratch,
one could leverage existing APIs or pre-trained models:
• Cloud Vision APIs: Services like Google Cloud Vision can be used to identify objects in images. Out-
of-the-box, they might recognize a Pokémon card as “trading card” or even read the text on it,
but they won’t directly give the card name or ID without custom training. Google does offer a
Product Search API where you can upload a catalog of products (card images) and then do
image queries to find similar items – theoretically, you could feed all card images into it. This
would offload the heavy lifting to Google’s servers at the cost of usage fees and complexity.
Given budget constraints, this may not be ideal. Microsoft Azure and AWS Rekognition are
similar – they can detect generic objects or text, but to identify a specific card, you would need to
implement your own matching logic on the results.
2
• Custom Model via Platforms like Roboflow: Tools like Roboflow can help train a custom object
detection or classification model. For instance, you could train a model to detect the card in an
image (like a detector for the rectangular card and maybe read the name as a class). Roboflow
even has out-of-the-box models and datasets; a developer might use it to quickly train a
Pokémon card classifier if a labeled dataset is available. The output could be exported to
TensorFlow Lite. This route still requires gathering training data (Roboflow has some community
datasets, or you use the Pokémon TCG API images for training) and careful evaluation. On-
device vs Server: A TensorFlow Lite model with a reasonable size (few tens of MB at most) could
run on modern phones at acceptable speeds. If integrated natively (via a library like react-
native-tflite or via ML Kit’s custom models), inference might take e.g. 100-300ms per image
on mid-range devices. In Expo, you would need to include support for running TFLite (likely by
using a native module, since pure JS would be too slow). One alternative is using TensorFlow.js
with WebGL acceleration in React Native; this is doable (TensorFlow’s official site even provides a
React Native integration example using pose detection 8 ), but in practice the performance of
TF.js can be significantly slower than native TFLite. It might be acceptable for a single-image
classification (maybe ~1s to identify a card) but probably not for real-time video scanning.
Another concern is app bundle size – including a large model will increase the app’s size and
memory use. You also need to update the model when new cards release. By contrast, doing
inference on a server (send image to your server which runs the model) keeps the app light, but
incurs server costs and latency (and requires the user to be online). Given the subscription
monetization plan, running a modest cloud instance for ML could be feasible, but the user
specifically mentioned privacy and cost concerns, so an on-device approach using smaller
models or OCR might be preferred.
Accuracy and Robustness: Across these methods, the accuracy can vary with conditions: - Image-
based matching can be extremely accurate if the card image is captured clearly and the database is
comprehensive, but it might stumble on similar artworks (e.g., reprints or promo variants with nearly
identical art) or foil glare altering the image. Using multiple hashes and a strict similarity threshold
helps reduce false matches 3 , and indeed some projects report very high precision using this method
within a single set 9 . Maintaining orientation invariance (via rotated/flipped hashes or rotation-
agnostic features) is important unless you can require the user to align the card. - Text-based
identification might fail if the OCR misreads characters – for example, “Blastoise” could be misread if
part of the card name is obscured or stylized. However, text is quite invariant to lighting (as long as the
image is in focus) and can handle background noise by focusing on alphanumeric characters. A hybrid
approach can be powerful: one open-source scanner, for instance, planned to read the card name via
OCR and also identify the set by recognizing the set symbol on the card 7 . The set symbol is a tiny
icon, which could be classified by a small image model or matched against a set icon database.
Combining these cues (name + set) would nail down the exact edition and printing. - In all cases,
background/lighting conditions need consideration. You can guide the user via the UI: use Expo
Camera with guidelines or an overlay to help center the card, and maybe a flash option for lighting. The
scanning logic (whether OCR or image match) might benefit from first applying a card detection step
(like finding the card edges in the camera frame) to crop/transform the card area – this removes
background clutter and corrects perspective, improving recognition. Many implementations start with a
rectangle detection (like a document scanner) before doing OCR or hashing 10 . This is feasible in real-
time with lightweight edge detection (OpenCV algorithms or even custom logic) – though in Expo you
might have to implement this in JS or use a Vision Camera plugin for efficiency.
• Privacy & Cost: On-device methods (ML Kit OCR, embedded models, hashing) keep user images
local, addressing privacy. They also incur no per-scan cost. The trade-off is the development
effort to fit this into a mobile app (and potentially increased app size or need for custom builds).
Cloud-based recognition (Google Vision API, etc.) simplifies implementation but could become
3
costly at scale (e.g., Google Vision text detection beyond a free quota, or Roboflow hosted API
has usage limits) and requires sending images over the network. Given the preference for free
solutions, leaning on on-device capabilities and free open data is ideal.
Summary: A practical strategy might be to use on-device OCR for a first-pass (fast and no network
required) and fall back to an image-matching method if text recognition is inconclusive. For example,
the app could attempt to read the card’s name/number; if it gets a confident result, it retrieves that
card’s info. If OCR fails (or the card is foreign language, etc.), the app could upload the image to a
backend that does a hash or ML-based image match against a card image database. This two-tier
approach balances speed and accuracy, and limits server usage to the harder cases. In any event, Expo
can support this pipeline: use expo-camera (or a community module like VisionCamera) to capture
images, integrate native MLKit for on-device OCR, and use a backend service (discussed below) for any
heavy recognition tasks or database queries. Several open-source projects demonstrate pieces of these
techniques, giving a starting point for implementation.
Market Price Retrieval Options
Once a card is identified, the app needs to fetch real-time market prices (and ideally historical price
trends) for that specific card. The user prefers free data sources, so we will compare APIs and scraping
methods:
• Official Trading Card Market APIs: The two major online marketplaces for Pokémon cards are
TCGplayer (North America) and Cardmarket (Europe). Both have APIs that provide pricing data:
• TCGplayer API: TCGplayer offers a comprehensive API for product info and pricing. Using it
typically involves signing up as a partner (they encourage this by offering affiliate commissions
for sales referrals) 11 12 . The API can return various price points – market price (an average of
recent sales), lowest listing price, etc., for each card (identified by a product ID). This data is
updated regularly (market price is often updated daily on their site). The good news is that
accessing pricing via the API is free; in fact, TCGplayer explicitly states they have pre-built APIs
and will even pay commissions for referrals 13 14 . The catch is you must abide by their terms
(likely including not caching data for too long without refreshing, and ideally linking to their
marketplace for purchases). If your app directs collectors to TCGplayer to buy cards, this could
even generate a revenue stream to complement subscriptions. The API requires an OAuth token
workflow (you get a public/private key, obtain a bearer token, then call endpoints). It returns
data in JSON – for example, you can retrieve the current market price and a price history graph
for a given card. However, the history might not be directly exposed via API for free; TCGplayer’s
website shows price charts, but through the API you might only get current prices and maybe a
daily change. If historical data is crucial, you might need to log prices over time yourself (more
on that below).
• Pokémon TCG API (pokemontcg.io): This is a community-driven API that aggregates card data
and includes pricing from TCGplayer and Cardmarket for each card object 15 . It’s essentially
a free wrapper that returns card details (name, set, images, etc.) along with up-to-date market
prices. For example, if you query a card by its ID, the JSON includes a tcgplayer field with
prices (often market price, low, high) and a cardmarket field for European prices 15 . This API
is free to use with an API key (it has a generous rate limit, which can be increased by contacting
the maintainer). Using this could dramatically simplify price retrieval: once your scanner
identifies the card’s ID (the API uses IDs like “xy7-54” for set and number), you can fetch a
complete info bundle in one request. The data is updated regularly on their end (likely daily or
near-real-time from TCGplayer’s feed). The advantages here are zero cost, no need to handle
OAuth, and unified data. The potential downsides: you rely on a third-party service’s uptime
(Pokémon TCG API is generally reliable but not an official company service) and you get whatever
4
pricing they provide (which is usually market price, not necessarily last sold on eBay). Still, given
the user’s preferences, this is probably the easiest free solution for pricing. You get real-time
prices without scraping, and even access to historical average prices (some community APIs or
datasets like TCGdex compile 1-day/7-day/30-day averages 16 17 , though using those might
require pulling data from their repository).
• Cardmarket API: For European markets, Cardmarket (aka MagicCardMarket) has an API to get
card prices (min, trend, etc.). As noted, the Pokémon TCG API v2 already surfaces Cardmarket
prices for each card 15 . If you wanted, you could independently call Cardmarket’s API (requires
user authentication and is a bit complex if you need real-time queries). Most likely, leveraging
the consolidated data from an API like pokemontcg.io is sufficient – it gives you both TCGplayer
and Cardmarket in one shot.
• Other APIs / Services: There are emerging services like JustTCG which focus on pricing data as a
service. JustTCG supports multiple TCGs (including Pokémon) with very frequent updates and
even condition-specific pricing via API 18 19 . However, these are commercial (JustTCG has a
free tier but with limited usage, and paid plans for heavy use 20 21 ). Given the goal of using
free data, such services might be overkill, but it’s good to know they exist if you needed more
granular data (like separate prices for Near Mint vs Played conditions, or more frequent than
daily updates). Similarly, for Magic: The Gathering, many developers use Scryfall’s API for free
card and price data – unfortunately Scryfall is MTG-specific. For Pokémon, the community
equivalent is really the Pokémon TCG API mentioned above.
Evaluation of APIs: Using official or community APIs is legally and technically cleaner than scraping.
You get structured data, likely with fewer surprises. Rate limits are a consideration – e.g., Pokémon TCG
API might allow, say, 100 requests per hour by default (hypothetical) and you can request higher limits.
If your app is subscription-based and not hitting millions of users immediately, these limits should be
fine (especially if you implement some caching). Uptime for these sources is generally good. The cost is
zero or minimal, and you avoid any terms-of-service issues. The main limitation might be if you want
real-time listing prices or auction results, which these APIs (being aggregated and updated periodically)
might not provide. They focus on market price or average value rather than what’s happening on eBay
this very hour. For most use cases (casual collectors checking value), a daily-updated market price is
sufficient and actually less volatile than moment-to-moment eBay listings.
• EBay Scraping and Unofficial Sources: The user specifically mentioned eBay, likely because
eBay reflects the actual prices cards are being bought and sold at by collectors. It’s indeed a
rich data source, but accessing it freely has challenges:
• eBay APIs: eBay has official APIs (like the Browse API in their newer REST interface, or the older
Finding API in the XML/SOAP tradition) which allow search queries for listings. With a free eBay
developer account, you can use these to search active listings or completed sales by keywords.
For example, the app could query “Charizard 4/102 Pokemon” in sold listings to get recent sale
prices of Base Set Charizard. This is a legitimate approach and avoids HTML scraping. The data
returned will include titles, prices, condition, etc. However, interpreting this data requires
filtering out unrelated results and perhaps averaging sale prices. Also, eBay’s API rate limits are
something to check (they might allow a few thousand queries per day on the free tier, which
could be enough if cached).
• Web Scraping HTML: If one didn’t use the API, the alternative is to scrape eBay’s website directly
(or via a headless browser). This method is not officially allowed: eBay’s terms of service forbid
scraping, and they have measures to block it (like requiring logged-in sessions for some data,
rate limiting IPs, etc.). Scraping might work in the short term, but it can break without warning
whenever the site UI changes or if anti-scraping measures kick in 22 . It’s risky to rely on for a
production app – your app could suddenly lose pricing functionality or even get IP-banned if it’s
hitting eBay too aggressively 23 . Moreover, parsing the HTML to extract prices can be error-
5
prone (sites often change class names or structure). Therefore, if eBay data is desired, it’s highly
recommended to go through the official API or an intermediary service (some aggregators like
130point.com show eBay sold item prices for trading cards; one could scrape those, but that’s
just scraping a scraper).
• Data freshness and legality: eBay gives the advantage of real-time pricing. For instance, you
could fetch the current lowest Buy-It-Now price or the latest auction result for a specific card.
This might be attractive for power users. But consider that eBay prices fluctuate and may need
context (condition of card, language, etc.). Data normalization is a big task here: if a user scans
“Pikachu from XYZ set”, an eBay search might return listings for PSA-graded cards, lots of
multiple cards, foreign versions, etc. You’d need to filter or allow the user to filter (e.g., maybe
default to ungraded English card sales). This complexity is beyond just getting a number – it
requires some logic to ensure you’re comparing apples to apples. Official APIs like TCGplayer
avoid this ambiguity by giving you the price of the exact card in a given condition category.
• Legality and ToS: Aside from technical issues, using scraped eBay data in a commercial app
could violate eBay’s terms if not done via their API or affiliate program. If you do use eBay’s API,
you should comply with their requirements (which may include showing that the data is from
eBay and possibly linking back for purchases, especially if you use their affiliate network).
In summary, eBay data can be free but comes at the cost of reliability and effort. If implemented, a
good strategy is to use the eBay official API in a backend service that caches results. For example, a
cloud function could query eBay for “average sold price in last 30 days” for a card (somewhat available
via eBay’s completed items search) and store that. This call might be slow (a couple seconds) and
shouldn’t be done too often. You could run it once a day per card or on-demand when a user scans a
card that’s not in cache. And as best practice, join eBay’s Partner Network – they have an affiliate
program too. By using their API and linking to listings, you both stay within rules and potentially earn a
small commission if your users buy cards via eBay links.
• Web Scraping Other Sources: Beyond eBay, some websites aggregate price data. For instance,
PriceCharting.com has a Pokémon card price guide (they track ungraded and PSA-graded values
over time). They likely derive their data from eBay sales. They have an API, but it might be paid or
limited. Another site, PokeData.io, shows price graphs for cards – possibly sourcing from
TCGplayer or eBay. If those sites offer free APIs or downloadable data, they could be tapped, but
most likely they are intended for human use. Given the existence of the Pokémon TCG API (free)
and eBay’s official channels, scraping secondary sites isn’t necessary and could be redundant. It’s
usually better to go to the primary sources for data fidelity.
• Data Caching & Historical Sync: Regardless of source, caching is crucial. Real-time price calls
for every scan or app view can be expensive and hit rate limits. A common approach is:
• Maintain a local cache (in app state or storage) for recent prices the user fetched. E.g., if the user
scans the same card twice, you don’t need to call the API the second time within a short span.
• Use a backend database to store prices for cards. For example, you could have a “Prices”
collection where each card ID maps to a structure containing latest price, last updated
timestamp, and maybe a history array. When a user scans a card, the backend can quickly return
the cached price and separately decide if it needs to refresh from the upstream source (if the
data is older than X hours).
• Implement scheduled updates for popular cards or for the user’s collection. If the app is tracking
a user’s collection, you might run a daily job (with a cloud scheduler or cron) to update the prices
of all cards in all users’ collections. This would allow you to show users up-to-date values and
trends without them manually rescanning every card. Services like Firebase Cloud Functions can
6
be triggered periodically (using tools like Firebase Scheduled Functions or an external cron
hitting an endpoint) to perform these updates.
• Historical data: If you want to display price graphs (historical market value), you will need to log
prices over time. Using an external API, you might only get the current price. So your backend
could append the current price to a time-series for that card each day or week. Over a few
months, you accumulate a history that can be shown in the app. The Pokémon TCG API doesn’t
directly give a full history (though it might give a few recent points or at least you could leverage
the community data like TCGdex which compiles some history). But assuming you want your
own, storing a daily price point is not too costly – even 365 points per card per year, and you
likely wouldn’t do it for every card in existence, just those users have or scan.
• Another benefit of caching is reducing latency for the user. If your server already knows the
price, it can return it instantly. If it has to fetch from eBay or TCGplayer on demand, the user
might wait a couple of seconds every time.
• Data Normalization: A tricky part of price aggregation is ensuring consistency across card
versions, conditions, languages. When retrieving data from a structured API like TCGplayer’s, you
usually query a specific product ID that already encapsulates the version (e.g., “Charizard, Base
Set, Unlimited, Near Mint”). That yields a clear price. But if you are stitching together data from
multiple sources, you must be careful:
• For example, the same Pokémon card character can have many printings, which vary wildly in
price. Your identification step should yield a specific card (including set and edition). It’s
important that the price you display matches that exactly. Using the card’s unique identifier from
a card database (like a TCGplayer product ID or Pokémon TCG API ID) ensures you get the right
price.
• If you use eBay by name search, make sure to include enough detail in the search query (set
name, number, etc.) so that you’re not mixing results. You might programmatically construct
queries like “{Name} {SetName} {CardNumber} -PSA -BGS -CGC” to filter out graded cards, for
instance. Even then, you might get some noise (people listing proxies or misidentified cards).
• Language differences: If your app might be used on Japanese cards or others, note that an
English card’s pricing data from TCGplayer won’t apply to a Japanese card (which has its own
market). The Pokémon TCG API database has separate entries for foreign cards (with their own
IDs and often limited price data unless Cardmarket covers it). You may want to restrict scope to
English cards initially, or clearly label if a card is foreign and handle that (maybe only Cardmarket
or eBay could have that data).
• Condition: All the above assumes raw ungraded cards in near-mint condition. Real-world prices
depend on condition (damaged vs mint could be 50% difference or more). TCGplayer’s market
price is usually for Near Mint. If users have poorer condition cards, the app could allow them to
adjust condition and perhaps apply a multiplier or pull different pricing (TCGplayer API can give
prices for different conditions if you query their SKU data). This complicates the UI but is
something to consider for a truly professional app. For a simpler approach, one could assume
near-mint prices and just provide a disclaimer.
• Promo variants, misprints: These are niche but can confuse an automated system. E.g., a
promo with the same name might not appear in the normal set list. Ensure your card database
includes promos and special editions so they can be recognized and priced.
To evaluate the options: leveraging free APIs (Pokémon TCG API / TCGplayer) is generally the
recommended route for a hobby/commercial app that doesn’t want ongoing data headaches. It
provides consistency, ease of integration, and legal usage. Scraping eBay can complement this if
needed for additional insights (like showing recent auction results for a card as extra info for power
users), but it should be done via official channels or very carefully to avoid breakage. Given the plan to
7
monetize via subscriptions, you likely want the app to be stable and low-maintenance – using official
data sources helps achieve that. Plus, by using TCGplayer’s API, you keep the door open to affiliate
monetization (which in some cases can even cover API usage by referring sales).
Suggested Architecture & Workflow
Bringing it all together, we can outline an end-to-end architecture for the Expo app and its backend:
1. On-Device Card Scanning (Client-side): The Expo React Native app will handle image capture and
initial recognition: - Use expo-camera (or a similar camera module) to let the user point the phone at
a card. You can overlay a guide (e.g., an outline of a card shape) to assist alignment. When the user taps
a capture button (or potentially automatically when focus is good), capture a photo or frame. - For a
smoother UX, you might implement a preview screen or real-time detection feedback. However, real-
time CNN processing in an Expo app is tough; a more feasible approach is to capture one image at a
time for processing. This also allows use of higher resolution images for better OCR/identification. - On-
device processing: As discussed, an efficient route is to perform OCR on the captured image to get the
card’s name and number. For example, using ML Kit’s text recognition: the app passes the image to the
ML Kit module, which returns any text found. The app then parses that text – it might look for patterns
like “NAME” at the top (larger font text could be assumed to be the name) and a fraction like “{number}/
{setSize}” indicating the card number. Some heuristic or template matching might be needed (perhaps
crop the top area of the card for name OCR, and bottom area for number). - If OCR yields a plausible
card name and number, the app can immediately proceed to fetch pricing (step 2). If OCR fails (returns
nothing or gibberish), you could fall back to an alternate strategy: * Perhaps send the image to your
server for a more intensive recognition (e.g., image hash matching against the card database). The app
would call a “identifyCard” endpoint with the image (maybe resized to reduce bandwidth), and the
server would return the best match card ID. * Alternatively, you could present the user with a list of
likely matches if you have some way to rank (for example, if OCR got “Pika??” you could search your card
database for “Pikachu” and ask which set). * In many cases, however, good lighting and using the
phone’s camera autofocus will allow OCR to work well for standard English cards, making the fallback
rarely needed. - Expo’s managed workflow can support this by including the native MLKit plugin via EAS.
Keep in mind you’ll need to build the app through EAS (you cannot use the vanilla Expo Go for testing
the MLKit feature, since it’s a custom native module). EAS Build will produce an .apk and .ipa that
include the MLKit native code. The config plugin setup for react-native-mlkit or similar is usually
documented (often just adding a line in app.json and running eas build ).
2. Card Identification & Data Retrieval (Backend): Once the app has some identifier for the card
(either directly from OCR or via a server image-match), it needs to fetch the card’s details and price: -
The card identifier could be a composite of set and number (like “Base Set 4/102”) or an ID from a
database. If using the Pokémon TCG API, for instance, you might store a mapping of set codes and card
numbers to their API ID. The app could then call api.pokemontcg.io/v2/cards/{id} directly.
However, calling third-party APIs from the client is sometimes not ideal because you have to embed the
API key. The Pokémon TCG API key is not highly sensitive (and can be restricted by referer), but for
flexibility you might route all data calls through your own backend. - Backend service: You can
implement a lightweight server (or serverless functions) to handle data queries. For example: * An
endpoint /getCardInfo that takes something like cardName and setCode (or your own card ID)
and returns the card’s info along with prices. * Internally, this function could query your database or an
external API. If performance is a concern, hitting an external API for each request might be slow, so
caching in a DB is beneficial. You could have pre-loaded your database with all card data from the
Pokémon TCG API (they even provide bulk data dumps or you can pull all cards). Then your backend can
do a quick database lookup by name/number. * If you maintain your own DB, you can also store prices.
8
For instance, a “Cards” table might have fields for latest TCGplayer price, last updated date, etc. When /
getCardInfo is called, if the price timestamp is older than a threshold, the server can fetch fresh data
(from TCGplayer API or PokemonTCG API) and update the record, then return to the app. * A serverless
option is to use Firebase or Supabase. For example, using Firebase Firestore: when the app scans a
card, it could query a Firestore document like cards/{set}-{number} . That doc could contain
cached price info. If it’s missing or stale, the app (or a cloud function triggered by a read) could update
it. Firebase could also store the user’s collection data. * Using Firebase Cloud Functions (or Supabase
Edge Functions) is convenient for calling external APIs securely (so you don’t expose API secrets to the
app) and performing scheduled jobs. You could write a Cloud Function that runs on a schedule to
refresh prices for certain cards each day, as mentioned. - Latency considerations: For the user’s
perspective, after scanning the card, they want the price in maybe 1-2 seconds max. Hitting your own
cache or DB should be very fast (<100ms). If a cache miss causes an external API call, that might add
some delay (e.g., 500ms). Still, overall it should be acceptable. It’s wise to design the app UI to handle
this asynchronously – e.g., show a loading indicator “Fetching price…” after identification. - Architecture
pattern: A serverless, cloud-oriented architecture might look like: * Expo app (front-end) <-> Firebase
(for auth & data) <-> Cloud Functions (for any heavy lifting or external API calls). * The app can use
Firebase Auth for user login (perhaps required if you offer cloud sync of collections or subscription
gating). Expo works well with Firebase JS SDK for this. * The user’s collection could be stored in Firestore
under their user doc, and each entry might reference a card ID. A cloud function could listen for new
entries and fetch the initial price, or the client could call a function to add the card along with price. * If
not using Firebase, a simple Express.js server or a serverless function on AWS Lambda could achieve the
same. For instance, an AWS Lambda could handle HTTP requests for scanning results and query
TCGplayer API accordingly. - Expo Integration: The Expo app will communicate with the backend via
HTTP or Firebase SDK. You can use fetch or axios to hit your REST endpoints (for example, calling
your Cloud Function URL). Ensure you handle errors (e.g., if the price API is down, show a message or
try a different source).
3. Updating Collection & Displaying Data (Client-side): Once the price info is retrieved, the app
should: - Present the card details to the user – e.g., show the card’s name, an image (you can fetch a
high-res image URL from the Pokémon TCG API or use an image CDN), and the market price. If you
have historical data, a small chart can be shown. - Allow the user to add the card to their collection
with one tap. This would save the card’s ID (and perhaps the quantity, condition, etc. as entered by the
user) in persistent storage. With a backend, you’d also sync this to the cloud under the user’s profile.
This way, if they log in on another device their collection is there. - If the app is subscription-based,
certain features (like seeing extensive price history or adding unlimited cards) might be gated behind a
paywall. Expo can integrate with app store subscriptions via libraries (there are Expo-compatible
libraries like expo-in-app-purchases or third-party services like RevenueCat that simplify
subscription handling). You’d have to implement that part such that the app knows who is a premium
subscriber, possibly unlocking features like real-time eBay price lookup, detailed charts, or alerts. -
Realtime updates: If you want to get fancy, you can use websockets or Firebase realtime updates to
push price changes. For instance, if your backend updates the price of a card in the database (maybe it
increased overnight), the app could receive a notification or update the UI. This is optional – many apps
simply update on pull-to-refresh. But if using Firestore, you could have the collection screen listen to
price fields of the user’s cards and update live. Since pricing doesn’t change minute-to-minute
drastically for cards, updating on app launch or manual refresh is usually fine. - Architecture style: The
overall pattern here could be described as a server-assisted client. The client (Expo app) handles the
interactive parts (camera, UI, local logic), while the server side (cloud functions/DB) handles data heavy
tasks and storage. This is in contrast to a pure on-device solution (which would have to bundle all card
data and do everything offline – possible but would make the app large and harder to update data).
Using a backend means you can update card prices and even card recognition logic without forcing the
9
user to update the app (e.g., you can improve your identification algorithm on the server side over
time).
4. Expo-Friendly Tools & Considerations: - Expo EAS (Expo Application Services): You will rely on EAS
Build to compile the app for iOS/Android, especially since you need custom native modules (ML Kit for
OCR, possibly in-app purchase modules, etc.). EAS makes it relatively straightforward to include these
via config plugins. Be prepared to write some config in app.json or eas.json to include the MLKit
SDKs and any required permissions (camera permission of course). Expo’s managed workflow with EAS
can handle all the native pieces so you don’t have to eject fully to bare workflow. For example, the
Infinite Red MLKit modules are designed to be compatible with Expo via config plugins, so you can
continue using the Expo toolchain 6 . - Camera and Image Handling: Use expo-camera for the live
camera. After capturing an image, you might want to use expo-image-manipulator to crop or
resize it (for example, crop to the card area if you detected it, or downscale the image before sending to
server to save bandwidth). Keep an eye on memory – capturing full-resolution images and processing
them can be memory heavy on older phones, so you may want to limit the resolution (the OCR doesn’t
need a 12MP image; a smaller 1080p image might suffice and be much faster to process). - Offline
mode: If you want some offline capability (maybe a user at a card convention with poor signal still
wants to scan cards), having on-device recognition and a cached price database would be ideal. This is
ambitious, but you could bundle a lightweight database of average card prices (say a JSON or SQLite
with each card’s last known price) in the app and update it when online. That way, scanning offline can
still show an approximate price (with a disclaimer that it’s offline data). This is a nice-to-have feature, not
essential. More simply, you could ensure the app doesn’t crash if offline – maybe queue the request and
inform the user they need internet for pricing data. - Security: If you call third-party APIs directly from
the app, remember not to expose secret keys. For example, if you used TCGplayer’s API directly, you
wouldn’t want to put the private key in the app. That’s another reason to have your own backend: it can
securely store API credentials. The Pokémon TCG API uses a public API key which is less sensitive (still,
you may keep it in secure storage or in the backend to avoid hitting rate limits due to key sharing). -
Monetization & Scaling: With a subscription model, ensure your architecture can scale to many users.
Firebase/Supabase can scale automatically for reads/writes, but if you do a lot of external API calls (say
thousands of price fetches per minute if user base grows), you need caching and perhaps upgrading to
higher rate limits or dedicated services. Using affiliate APIs like TCGplayer might impose you use the
data in certain ways (usually just attribution and linking). This architecture is cloud-native enough that it
can grow with the user base; you’d mainly watch out for function cold starts or database performance if
you accumulate a lot of price history data. For initial stages, it should all comfortably run within free
tiers or low-cost tiers of these services.
In summary, the architecture is: Expo app for scanning -> on-device recognition (OCR/ML) -> send
identifier to backend service -> backend returns price info (using cached or real-time API data) -> app
displays and stores the card. This flow ensures the heavy tasks (maintaining price data, doing large
database searches) are offloaded to the cloud, while the user’s device does only the immediate,
interactive tasks (camera capture and maybe text extraction). This keeps the app responsive and
leverages Expo’s strengths (rapid development, cross-platform support) along with the power of cloud
functions for scalability.
10
Open-Source Projects & Known Challenges
To reinforce the feasibility of the above approaches, here are a few open-source projects that tackled
similar problems, and the insights we can draw from them:
• Pokemon Card Scanner by NolanAmblard: A Python project that uses OpenCV for card
detection and perceptual image hashing to identify cards 1 . In their demo (focused on the
Evolutions set), they perform edge detection and perspective correction on the card, then
compute multiple hashes of the image and compare to a precomputed hash database for that
set 3 . They even account for different orientations by storing mirrored/upside-down hash
variants 2 . This validated that hashing is a viable identification method with low error rates
when images are normalized. The limitation was that it was PC-based and only one set, but
scaling it up is conceptually straightforward (with a larger image database). Mobile apps can take
inspiration by possibly using a similar hashing technique server-side.
• jslok’s Real-Time Card Scanner: This project by Justin Lok is a proof-of-concept combining deep
learning and classic techniques to scan cards in real time 4 . It forgoes OCR entirely in favor of
visual recognition, using a trained instance segmentation model to detect card boundaries and
then matching the card via image features/hashes 5 . Impressively, it claims to handle ~20,000
cards including holographic variants purely from the image 4 . The author noted using the
RTM-Det model for its speed and high precision in finding card shapes 24 , and achieved a 0.90
mAP in segmentation by training on synthetic images of cards on various backgrounds 25 26 .
After segmentation and perspective warp, the identification is done via image hashing against a
complete card database. This project demonstrates that a highly accurate scanner is possible,
though the approach requires training data and careful model integration. In an Expo context,
one might not replicate the entire pipeline on-device, but could use a trimmed down version
(e.g., a simpler contour-finding instead of a learned segmentation, to avoid needing a heavy
model). It’s worth noting that this project was intended to be integrated into a React Native app
eventually, indicating it’s plausible with native modules (perhaps using something like
TensorFlow Lite for the model and a C++ addon for hashing). For our purposes, it reinforces that
combining detection + image matching is a proven solution.
• PokeCard-TCG-Detector by em4go: Another project that closely mirrors what we described: it
uses OpenCV to detect the card and then imagehash (pHash, dHash, wHash) to compare
against images from the Pokémon TCG API 27 . The card data (including images) is pulled from
the API, meaning the heavy lifting of maintaining card info is delegated to that API. This project
even provides a web interface via Flask to scan cards using a webcam 28 . It’s a great example of
using a free card database and simple algorithms to achieve identification. The success of this
approach will depend on the quality of the image match; combining multiple hash algorithms, as
they did, improves confidence 29 . For a mobile app, one could use a similar backend: store
hashes of all card images from the API, and when an image comes in, compute its hashes and
find the nearest match.
• CardScanner by KLuml: A generic TCG card scanner aimed at Magic: The Gathering and
Pokémon that highlights the OCR approach. It scans for a card in the image, then reads the card
name and tries to recognize the set symbol, returning the card name (set recognition was still in
progress) 7 . This indicates an approach where the heavy part (identifying the set symbol
image) might be a work in progress, but reading the name via OCR is doable. It’s a simpler
pipeline which might be more error-prone if two sets have the same name card, but it’s
straightforward. The code and README suggest using an existing document-scanner to find the
11
card (likely edges, similar to how one would scan a business card or paper) and then Tesseract or
a similar OCR to extract text. This is very relevant for an Expo app, since we can replicate this
with ML Kit’s OCR easily. The challenge, as noted, is reliably identifying the set – which could be
solved by also OCR-ing the card number and cross-referencing with a database of which set that
collector number belongs to.
• pokemon-card-recognizer by prateekt: This is an open-source Python package that can
recognize cards in images or even videos. It uses a combination of techniques, including OCR (by
default it uses the EasyOCR library, with an option for Tesseract) and a reference database built
from the Pokémon TCG API 30 31 . Essentially, it can process a folder of images or a pack
opening video and output identified cards. The reference building step leverages the API to get
card data, showing again how useful that API is. This project likely does something akin to:
detect card -> OCR text -> lookup in reference. The fact that it’s packaged and using GPU
acceleration for speed (if available) shows that even a combined approach can be optimized.
While a mobile app won’t have a GPU for such Python code, the logic can be ported – for
example, one could adapt its approach in a cloud function to handle identification if needed.
These projects underscore the common challenges and how they addressed them: - Card
Localization: Before recognition, finding the card in the camera image is needed (unless you ask the
user to fit the card exactly in view). All the above solutions use either OpenCV edge detection or a neural
network to localize the card. In our Expo app, implementing a full neural net for this might be heavy,
but OpenCV-like edge finding can potentially be done via a JS library or by leveraging the Document
Scanner module of ML Kit (which is listed as a beta feature in the Infinite Red MLKit wrapper 32 ). That
module might detect document corners – a Pokémon card is essentially a small document. - Lighting
and Image Quality: The accuracy of both OCR and image matching drops with poor image quality.
Glares on holo cards can confuse OCR or hashing (the holographic foil might appear as random noise).
Low light can cause motion blur. A solution is to encourage the user to use flash or move to better light,
and possibly do multiple frames. Some apps take a quick burst of photos and pick the clearest one or
combine them. For instance, taking two pictures – one with flash, one without – might help in getting
text from one and image features from the other. It increases complexity, but it’s an idea if you find one
method alone isn’t robust. - Similar Card Artworks: Certain cards (especially energy cards or reprints)
have very similar appearance. If using image-based ID, the system might confuse them. The remedy is
to lean on text (card name or subtle differences in the card number). Conversely, if text alone is used,
cards with the same name across sets (like many Pikachu cards) need the set number to differentiate.
Ensuring your identification yields a specific set is crucial to get correct prices – misidentifying a card’s
edition could mean a 1st Edition shadowless vs a regular unlimited edition, which is a huge price
difference. A robust approach might cross-verify: e.g., if image matching says it’s either Card A or Card
B, use OCR or user input to confirm the set. - OCR Inaccuracies: If relying on OCR, be prepared for
occasional errors (like “Illumise” vs “Illusion” due to font, or mistaking 0 for O). Incorporating a
dictionary of Pokémon names can help auto-correct minor spelling errors by choosing the closest valid
name. Similarly, if you OCR the number “14/108” and it comes as “I4/I08”, you can apply regex and
sanity checks (allowed characters for numbers are digits and maybe “/”). In short, some post-processing
on OCR output will be needed. - Performance on Mobile: Heavy image processing in pure JavaScript
can be slow. If you find that doing detection or hashing in JS is laggy, consider moving those parts to the
backend or to native modules. Using the GPU via TensorFlow.js might help for neural nets, but there’s
still overhead. With Expo, you have the option to write some critical code in Rust or C++ and use it via a
custom native module if truly needed (though that’s an advanced route). Most likely, leaning on existing
native modules (MLKit for text, perhaps OpenCV via a React Native plugin if one exists for Expo) is
enough. Also, not processing full 4K resolution images can vastly speed up algorithms with negligible
impact on accuracy for our case. - Multi-platform differences: Testing on both iOS and Android is
important. Camera focus and exposure might behave differently – ensure you use autoFocus in
12
expo-camera and maybe allow the user to tap to focus if needed. Android devices vary in camera quality
more, so your algorithm should handle slightly noisier images too. ML Kit’s text recognition works
offline on both iOS and Android now (on-device ML), but the models might have slight differences. From
experience, Google’s on-device OCR is quite good across platforms. - Legal/IP Issues: While building
the app, remember that Pokémon card images and data are copyrighted. The Pokémon TCG API
operates in a gray area but has an understanding in the community. Displaying card images in-app and
their prices should fall under fair use for a collection management purpose, but if you use official
imagery, adding attributions (like “Data from TCGplayer” or “Images © Pokémon”) might be prudent.
Especially if scraping eBay or using their API, you might need to adhere to their display requirements
(for example, eBay might require showing the word “eBay” with any data pulled, as per their policies).
Just keep an eye on API terms for any attribution requirements.
In conclusion, building a Pokémon card scanning and pricing app with Expo is certainly achievable by
combining the right techniques. For scanning, a combination of on-device OCR (for speed) and image
recognition (for accuracy in tough cases) covers all bases. For pricing, tapping into free APIs like
Pokémon TCG API (which provides TCGplayer/Cardmarket data) gives real-time info at zero cost, while a
backend can be used to cache and enrich this data (with historical trends or eBay results if needed). The
suggested architecture leverages Expo’s capabilities for a smooth cross-platform UI and uses cloud
services to handle heavy data tasks, aligning well with the goal of monetizing via subscription (since
most ongoing costs – data APIs – are minimal or free). By learning from prior projects and addressing
the challenges (lighting, similar cards, OCR errors) with thoughtful design, the app can provide users a
seamless experience: scan your card, see its market value instantly, and build your digital collection. 3 15
1 2 3 9 GitHub - NolanAmblard/Pokemon-Card-Scanner: A Pokemon card scanner that uses
10
OpenCV to detect the cards and a MySQL database to determine which card the scan is.
https://github.com/NolanAmblard/Pokemon-Card-Scanner
4 5 24 25 26 GitHub - jslok/card-scanner
https://github.com/jslok/card-scanner
6 32 React Native MLKit | Open Source at Infinite Red
https://docs.infinite.red/react-native-mlkit/
7 GitHub - KLuml/CardScanner: Generic card scanner meant for card games with similar card formats
as MTG & Pokemon. Takes an image, scans for a card, scans card for name and set symbol. Returns the
card name. Set recognition is still in progress.
https://github.com/KLuml/CardScanner
8 Use TensorFlow.js in a React Native app
https://www.tensorflow.org/js/tutorials/applications/react_native
11 12 13 14 How can I get access to your card pricing data? – TCGplayer.com
https://help.tcgplayer.com/hc/en-us/articles/201577976-How-can-I-get-access-to-your-card-pricing-data
15 Add cardmarket pricing data · Issue #97 · PokemonTCG/pokemon-tcg-api · GitHub
https://github.com/PokemonTCG/pokemon-tcg-api/issues/97
16 GitHub - tcgdex/price-history: Automatic Pricing history for Pokémon TCG cards from multiple
17
sources
https://github.com/tcgdex/price-history
18 19 20 21 22 23 JustTCG vs. The Alternatives: Choosing the Right TCG Pricing API | by JustTCG
Editor | Medium
https://justtcg.medium.com/justtcg-vs-the-alternatives-choosing-the-right-tcg-pricing-api-6d5d555ac7cd
13
27 28 29 GitHub - em4go/PokeCard-TCG-detector
https://github.com/em4go/PokeCard-TCG-detector
30 31 GitHub - prateekt/pokemon-card-recognizer: Recognize Pokemon Cards in Images or Videos.
https://github.com/prateekt/pokemon-card-recognizer
14