Back to Blog
Build a React Native Pokemon Card Scanner App with OCR
PokemonPriceTracker Team
•4 min read

Build a React Native Pokemon Card Scanner
Scan Cards with Your Phone Camera
Point your phone at a Pokemon card and instantly get:
- Card name and set
- Current market price
- PSA grading recommendations
- Add to collection with one tap
This tutorial shows you how to build it with React Native + Expo.
Tech Stack
- React Native with Expo
- expo-camera for camera access
- Tesseract.js for OCR
- PokemonPriceTracker API
- SQLite for offline storage
Quick Start
npx create-expo-app pokemon-scanner
cd pokemon-scanner
npx expo install expo-camera expo-image-picker expo-file-system
npm install tesseract.js axios react-native-sqlite-storage
Step 1: Camera Component
import React, { useState } from 'react';
import { View, Button, Image } from 'react-native';
import { Camera } from 'expo-camera';
import * as ImagePicker from 'expo-image-picker';
export function CardScanner() {
const [hasPermission, setHasPermission] = useState(null);
const [image, setImage] = useState(null);
React.useEffect(() => {
(async () => {
const { status } = await Camera.requestCameraPermissionsAsync();
setHasPermission(status === 'granted');
})();
}, []);
const takePicture = async () => {
const result = await ImagePicker.launchCameraAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
quality: 1,
});
if (!result.canceled) {
setImage(result.assets[0].uri);
processImage(result.assets[0].uri);
}
};
const processImage = async (uri: string) => {
// OCR and API call coming next
};
return (
<View>
<Button title="Scan Card" onPress={takePicture} />
{image && <Image source={{ uri: image }} style={{ width: 200, height: 280 }} />}
</View>
);
}
Step 2: OCR Processing
import Tesseract from 'tesseract.js';
async function extractCardText(imageUri: string): Promise<string> {
try {
const { data: { text } } = await Tesseract.recognize(
imageUri,
'eng',
{
logger: m => console.log(m)
}
);
return text;
} catch (error) {
console.error('OCR Error:', error);
return '';
}
}
Step 3: API Integration
import axios from 'axios';
const API_KEY = 'your_api_key';
const API_BASE = 'https://www.pokemonpricetracker.com/api/v2';
async function identifyCard(extractedText: string) {
try {
const response = await axios.post(
`${API_BASE}/parse-title`,
{ title: extractedText },
{
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
}
}
);
if (response.data.matches && response.data.matches.length > 0) {
return response.data.matches[0].card;
}
return null;
} catch (error) {
console.error('API Error:', error);
return null;
}
}
Step 4: Complete Scanner Flow
import React, { useState } from 'react';
import { View, Text, Button, Image, ActivityIndicator } from 'react-native';
export function CardScannerApp() {
const [loading, setLoading] = useState(false);
const [card, setCard] = useState(null);
const handleScan = async () => {
setLoading(true);
// Step 1: Take picture
const result = await ImagePicker.launchCameraAsync({
quality: 0.8,
});
if (result.canceled) {
setLoading(false);
return;
}
// Step 2: OCR
const text = await extractCardText(result.assets[0].uri);
// Step 3: Identify card
const cardData = await identifyCard(text);
if (cardData) {
setCard(cardData);
} else {
Alert.alert('Card Not Found', 'Could not identify the card. Try again with better lighting.');
}
setLoading(false);
};
return (
<View style={{ padding: 20 }}>
<Button title="Scan Pokemon Card" onPress={handleScan} />
{loading && <ActivityIndicator size="large" />}
{card && (
<View style={{ marginTop: 20 }}>
<Image source={{ uri: card.image?.url }} style={{ width: 200, height: 280 }} />
<Text style={{ fontSize: 20, fontWeight: 'bold' }}>{card.name}</Text>
<Text>Set: {card.setName}</Text>
<Text style={{ fontSize: 18, color: 'green' }}>
Market Price: ${card.prices.market.toFixed(2)}
</Text>
</View>
)}
</View>
);
}
Step 5: SQLite Storage
import SQLite from 'react-native-sqlite-storage';
const db = SQLite.openDatabase({ name: 'pokemon_collection.db' });
function initDatabase() {
db.transaction(tx => {
tx.executeSql(
`CREATE TABLE IF NOT EXISTS scanned_cards (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tcg_player_id TEXT UNIQUE,
name TEXT,
set_name TEXT,
price REAL,
scanned_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`,
[]
);
});
}
function saveCard(card) {
db.transaction(tx => {
tx.executeSql(
'INSERT OR REPLACE INTO scanned_cards (tcg_player_id, name, set_name, price) VALUES (?, ?, ?, ?)',
[card.tcgPlayerId, card.name, card.setName, card.prices.market]
);
});
}
Deployment
# iOS
eas build --platform ios
# Android
eas build --platform android
# Both
eas build --platform all
Tips for Better Scanning
- Good lighting: Natural daylight works best
- Flat surface: Lay card flat, avoid glare
- Card name focus: Focus on card name and set info
- Retry logic: Allow multiple scan attempts
- Manual fallback: Let users type card name if OCR fails
Cost Analysis
- Free tier: 100 scans/day
- API tier ($9.99/mo): 20,000 scans/day
- Perfect for personal use
PokemonPriceTracker Team
Stay Updated
Subscribe to our newsletter for the latest Pokemon card market trends, investment opportunities, and exclusive insights delivered straight to your inbox.