Skip to main content

Building a Card Price Tracking Application

Learn how to build a comprehensive card price tracking application using Trading Card API. This guide covers everything from basic card identification to advanced portfolio monitoring features.

🎯 What You'll Build​

By the end of this guide, you'll have a complete price tracking application that can:

  • Track card prices across multiple marketplaces
  • Send price alerts when cards hit target values
  • Monitor portfolios with real-time value calculations
  • Analyze trends with historical price data
  • Identify cards accurately using API data

πŸ“‹ Prerequisites​

  • Basic knowledge of your chosen programming language (JavaScript, Python, PHP)
  • Understanding of REST APIs and JSON data
  • A Trading Card API account with active subscription
  • Access to external pricing data sources (eBay API, TCGPlayer, etc.)

πŸš€ Architecture Overview​

A robust price tracking application typically consists of:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Trading Card β”‚ β”‚ Your Price β”‚ β”‚ External β”‚
β”‚ API β”‚ β”‚ Tracking App β”‚ β”‚ Price APIs β”‚
β”‚ β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ β€’ Card Data │◄───│ β€’ Card ID │───►│ β€’ Current Pricesβ”‚
β”‚ β€’ Set Info β”‚ β”‚ β€’ Price History β”‚ β”‚ β€’ Market Data β”‚
β”‚ β€’ Player Data β”‚ β”‚ β€’ Alerts β”‚ β”‚ β€’ Sales History β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Step 1: Card Identification System​

Understanding Card Data Structure​

The foundation of price tracking is accurate card identification. Trading Card API provides rich metadata to uniquely identify cards:

// Example card data structure
const cardData = {
"id": "tcg-card-123",
"type": "cards",
"attributes": {
"name": "Michael Jordan",
"year": "1986",
"brand": "Fleer",
"number": "57",
"variation": null,
"graded": false,
"condition": null,
"image_uuid": "550e8400-e29b-41d4-a716-446655440000" // v0.6.0+: UUID reference to card image
},
"relationships": {
"set": {
"data": { "id": "fleer-1986-basketball", "type": "sets" }
},
"player": {
"data": { "id": "michael-jordan", "type": "players" }
},
"images": { // v0.7.0+: Related CardImage resources
"data": [
{ "id": "550e8400-e29b-41d4-a716-446655440000", "type": "card_images" }
]
}
}
}

Building a Card Lookup Function​

class CardIdentifier {
constructor(apiKey) {
this.apiKey = apiKey;
this.baseUrl = 'https://api.tradingcardapi.com/v1';
}

async identifyCard(searchParams) {
const { name, year, brand, number } = searchParams;

const queryParams = new URLSearchParams({
'filter[name]': name,
'filter[year]': year,
'filter[brand]': brand,
'filter[number]': number,
'include': 'set,player'
});

const response = await fetch(`${this.baseUrl}/cards?${queryParams}`, {
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Accept': 'application/vnd.api+json'
}
});

const data = await response.json();
return this.processCardData(data);
}

processCardData(apiResponse) {
return apiResponse.data.map(card => ({
id: card.id,
name: card.attributes.name,
year: card.attributes.year,
brand: card.attributes.brand,
number: card.attributes.number,
setName: this.findIncluded(apiResponse.included, 'sets', card.relationships.set.data.id)?.attributes.name,
uniqueIdentifier: this.generateUniqueId(card)
}));
}

generateUniqueId(card) {
// Create a unique identifier for price tracking
return `${card.attributes.brand}-${card.attributes.year}-${card.attributes.number}-${card.id}`;
}

findIncluded(included, type, id) {
return included.find(item => item.type === type && item.id === id);
}
}

Python Implementation​

import requests
import hashlib

class CardIdentifier:
def __init__(self, api_key):
self.api_key = api_key
self.base_url = 'https://api.tradingcardapi.com/v1'
self.headers = {
'Authorization': f'Bearer {api_key}',
'Accept': 'application/vnd.api+json'
}

def identify_card(self, search_params):
"""Identify cards based on search parameters"""
params = {
'filter[name]': search_params.get('name'),
'filter[year]': search_params.get('year'),
'filter[brand]': search_params.get('brand'),
'filter[number]': search_params.get('number'),
'include': 'set,player'
}

response = requests.get(
f'{self.base_url}/cards',
headers=self.headers,
params=params
)
response.raise_for_status()

return self.process_card_data(response.json())

def process_card_data(self, api_response):
"""Process API response into trackable card objects"""
cards = []
for card in api_response['data']:
set_info = self.find_included(
api_response.get('included', []),
'sets',
card['relationships']['set']['data']['id']
)

cards.append({
'id': card['id'],
'name': card['attributes']['name'],
'year': card['attributes']['year'],
'brand': card['attributes']['brand'],
'number': card['attributes']['number'],
'set_name': set_info['attributes']['name'] if set_info else None,
'unique_identifier': self.generate_unique_id(card)
})

return cards

def generate_unique_id(self, card):
"""Generate unique identifier for price tracking"""
identifier_string = f"{card['attributes']['brand']}-{card['attributes']['year']}-{card['attributes']['number']}-{card['id']}"
return hashlib.md5(identifier_string.encode()).hexdigest()

def find_included(self, included, type_name, id_value):
"""Find included resource by type and id"""
for item in included:
if item['type'] == type_name and item['id'] == id_value:
return item
return None

Step 2: Price Data Integration​

External Price API Integration​

Since Trading Card API focuses on card identification and metadata, you'll need external price sources:

class PriceDataManager {
constructor(priceApiKeys) {
this.ebayApiKey = priceApiKeys.ebay;
this.tcgPlayerApiKey = priceApiKeys.tcgPlayer;
this.priceHistory = new Map();
}

async getCurrentPrices(cardIdentifier) {
const pricePromises = [
this.getEbayPrice(cardIdentifier),
this.getTCGPlayerPrice(cardIdentifier),
// Add more price sources as needed
];

const prices = await Promise.allSettled(pricePromises);

return {
cardId: cardIdentifier,
timestamp: new Date(),
prices: {
ebay: prices[0].status === 'fulfilled' ? prices[0].value : null,
tcgPlayer: prices[1].status === 'fulfilled' ? prices[1].value : null,
},
averagePrice: this.calculateAveragePrice(prices)
};
}

async getEbayPrice(cardIdentifier) {
// Example eBay API integration
const searchQuery = this.buildEbaySearchQuery(cardIdentifier);
const response = await fetch(`https://api.ebay.com/buy/browse/v1/item_summary/search?q=${encodeURIComponent(searchQuery)}`, {
headers: {
'Authorization': `Bearer ${this.ebayApiKey}`,
'X-EBAY-C-MARKETPLACE-ID': 'EBAY_US'
}
});

const data = await response.json();
return this.processBayPriceData(data);
}

buildEbaySearchQuery(cardData) {
// Build specific search query for eBay
return `${cardData.name} ${cardData.year} ${cardData.brand} #${cardData.number}`;
}

processBayPriceData(ebayData) {
if (!ebayData.itemSummaries || ebayData.itemSummaries.length === 0) {
return null;
}

const prices = ebayData.itemSummaries
.filter(item => item.price && item.price.value)
.map(item => parseFloat(item.price.value))
.sort((a, b) => a - b);

if (prices.length === 0) return null;

return {
low: prices[0],
high: prices[prices.length - 1],
average: prices.reduce((sum, price) => sum + price, 0) / prices.length,
sampleSize: prices.length
};
}

calculateAveragePrice(priceResults) {
const validPrices = priceResults
.filter(result => result.status === 'fulfilled' && result.value)
.map(result => result.value.average)
.filter(price => price > 0);

if (validPrices.length === 0) return null;

return validPrices.reduce((sum, price) => sum + price, 0) / validPrices.length;
}

async storePriceHistory(cardId, priceData) {
// Store price data for historical tracking
if (!this.priceHistory.has(cardId)) {
this.priceHistory.set(cardId, []);
}

this.priceHistory.get(cardId).push(priceData);

// In a real application, you'd store this in a database
await this.savePriceToDatabase(cardId, priceData);
}
}

Step 3: Price Alert System​

Building Price Monitoring​

class PriceAlertManager {
constructor(cardIdentifier, priceDataManager) {
this.cardIdentifier = cardIdentifier;
this.priceDataManager = priceDataManager;
this.alerts = new Map();
this.monitoringInterval = null;
}

addPriceAlert(cardId, alertConfig) {
const alert = {
id: this.generateAlertId(),
cardId: cardId,
targetPrice: alertConfig.targetPrice,
condition: alertConfig.condition, // 'below', 'above', 'change'
percentageThreshold: alertConfig.percentageThreshold,
active: true,
created: new Date(),
notificationMethods: alertConfig.notificationMethods || ['email']
};

this.alerts.set(alert.id, alert);
return alert.id;
}

async startMonitoring(intervalMinutes = 60) {
console.log(`Starting price monitoring every ${intervalMinutes} minutes`);

this.monitoringInterval = setInterval(async () => {
await this.checkAllAlerts();
}, intervalMinutes * 60 * 1000);

// Initial check
await this.checkAllAlerts();
}

async checkAllAlerts() {
const activeAlerts = Array.from(this.alerts.values()).filter(alert => alert.active);

for (const alert of activeAlerts) {
try {
await this.checkAlert(alert);
} catch (error) {
console.error(`Error checking alert ${alert.id}:`, error);
}
}
}

async checkAlert(alert) {
// Get card info from Trading Card API
const cardData = await this.cardIdentifier.getCardById(alert.cardId);
if (!cardData) return;

// Get current prices
const priceData = await this.priceDataManager.getCurrentPrices(cardData);
if (!priceData.averagePrice) return;

const shouldTrigger = this.evaluateAlertCondition(alert, priceData.averagePrice);

if (shouldTrigger) {
await this.triggerAlert(alert, cardData, priceData);
}
}

evaluateAlertCondition(alert, currentPrice) {
switch (alert.condition) {
case 'below':
return currentPrice <= alert.targetPrice;
case 'above':
return currentPrice >= alert.targetPrice;
case 'change':
return this.checkPercentageChange(alert, currentPrice);
default:
return false;
}
}

async checkPercentageChange(alert, currentPrice) {
// Get price history for percentage change calculation
const history = this.priceDataManager.priceHistory.get(alert.cardId) || [];
if (history.length < 2) return false;

const previousPrice = history[history.length - 2].averagePrice;
const percentageChange = Math.abs((currentPrice - previousPrice) / previousPrice) * 100;

return percentageChange >= alert.percentageThreshold;
}

async triggerAlert(alert, cardData, priceData) {
const alertMessage = {
alertId: alert.id,
cardName: cardData.name,
cardDetails: `${cardData.year} ${cardData.brand} #${cardData.number}`,
currentPrice: priceData.averagePrice,
targetPrice: alert.targetPrice,
condition: alert.condition,
timestamp: new Date(),
priceData: priceData
};

// Send notifications
for (const method of alert.notificationMethods) {
await this.sendNotification(method, alertMessage);
}

// Mark alert as triggered (optional: deactivate after trigger)
alert.lastTriggered = new Date();
console.log(`Alert triggered for ${cardData.name}: $${priceData.averagePrice}`);
}

async sendNotification(method, alertMessage) {
switch (method) {
case 'email':
await this.sendEmailNotification(alertMessage);
break;
case 'webhook':
await this.sendWebhookNotification(alertMessage);
break;
case 'push':
await this.sendPushNotification(alertMessage);
break;
}
}

generateAlertId() {
return 'alert_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
}
}

Step 4: Portfolio Management​

Tracking Card Collections​

import json
from datetime import datetime, timedelta
from typing import List, Dict, Optional

class PortfolioManager:
def __init__(self, card_identifier, price_data_manager):
self.card_identifier = card_identifier
self.price_data_manager = price_data_manager
self.portfolios = {}

def create_portfolio(self, user_id: str, portfolio_name: str) -> str:
"""Create a new portfolio for a user"""
portfolio_id = f"{user_id}_{portfolio_name}_{int(datetime.now().timestamp())}"

self.portfolios[portfolio_id] = {
'id': portfolio_id,
'user_id': user_id,
'name': portfolio_name,
'cards': [],
'created_at': datetime.now().isoformat(),
'total_value': 0.0,
'last_updated': None
}

return portfolio_id

def add_card_to_portfolio(self, portfolio_id: str, card_data: Dict, quantity: int = 1, condition: str = 'NM') -> bool:
"""Add a card to a portfolio with quantity and condition"""
if portfolio_id not in self.portfolios:
return False

portfolio = self.portfolios[portfolio_id]

# Check if card already exists in portfolio
existing_card = next(
(card for card in portfolio['cards']
if card['card_id'] == card_data['id'] and card['condition'] == condition),
None
)

if existing_card:
existing_card['quantity'] += quantity
else:
portfolio_card = {
'card_id': card_data['id'],
'name': card_data['name'],
'year': card_data['year'],
'brand': card_data['brand'],
'number': card_data['number'],
'set_name': card_data['set_name'],
'quantity': quantity,
'condition': condition,
'date_added': datetime.now().isoformat(),
'purchase_price': None, # User can set this
'current_value': 0.0
}
portfolio['cards'].append(portfolio_card)

return True

async def update_portfolio_values(self, portfolio_id: str) -> Dict:
"""Update current values for all cards in a portfolio"""
if portfolio_id not in self.portfolios:
return None

portfolio = self.portfolios[portfolio_id]
total_value = 0.0

for card in portfolio['cards']:
try:
# Get current price data
card_identifier = {
'id': card['card_id'],
'name': card['name'],
'year': card['year'],
'brand': card['brand'],
'number': card['number']
}

price_data = await self.price_data_manager.get_current_prices(card_identifier)

if price_data and price_data['average_price']:
# Adjust price based on condition
condition_multiplier = self.get_condition_multiplier(card['condition'])
adjusted_price = price_data['average_price'] * condition_multiplier

card['current_value'] = adjusted_price * card['quantity']
card['price_per_unit'] = adjusted_price
card['last_price_update'] = datetime.now().isoformat()

total_value += card['current_value']

except Exception as e:
print(f"Error updating price for {card['name']}: {e}")
continue

portfolio['total_value'] = total_value
portfolio['last_updated'] = datetime.now().isoformat()

return {
'portfolio_id': portfolio_id,
'total_value': total_value,
'card_count': len(portfolio['cards']),
'last_updated': portfolio['last_updated']
}

def get_condition_multiplier(self, condition: str) -> float:
"""Get price multiplier based on card condition"""
condition_multipliers = {
'M': 1.0, # Mint
'NM': 0.85, # Near Mint
'EX': 0.70, # Excellent
'VG': 0.55, # Very Good
'G': 0.40, # Good
'P': 0.25 # Poor
}
return condition_multipliers.get(condition, 0.85)

def get_portfolio_analytics(self, portfolio_id: str) -> Dict:
"""Generate analytics for a portfolio"""
if portfolio_id not in self.portfolios:
return None

portfolio = self.portfolios[portfolio_id]
cards = portfolio['cards']

if not cards:
return {'error': 'Portfolio is empty'}

# Calculate analytics
total_cards = sum(card['quantity'] for card in cards)
total_value = portfolio['total_value']

# Top cards by value
top_cards = sorted(
[card for card in cards if card.get('current_value', 0) > 0],
key=lambda x: x['current_value'],
reverse=True
)[:10]

# Distribution by brand
brand_distribution = {}
for card in cards:
brand = card['brand']
if brand not in brand_distribution:
brand_distribution[brand] = {'count': 0, 'value': 0.0}
brand_distribution[brand]['count'] += card['quantity']
brand_distribution[brand]['value'] += card.get('current_value', 0)

# Year distribution
year_distribution = {}
for card in cards:
year = card['year']
if year not in year_distribution:
year_distribution[year] = {'count': 0, 'value': 0.0}
year_distribution[year]['count'] += card['quantity']
year_distribution[year]['value'] += card.get('current_value', 0)

return {
'portfolio_summary': {
'total_cards': total_cards,
'unique_cards': len(cards),
'total_value': total_value,
'average_card_value': total_value / total_cards if total_cards > 0 else 0
},
'top_cards': top_cards,
'brand_distribution': brand_distribution,
'year_distribution': year_distribution,
'last_updated': portfolio['last_updated']
}

Step 5: Data Storage and Caching​

Database Schema Design​

-- Cards table (synced from Trading Card API)
CREATE TABLE cards (
id VARCHAR(255) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
year VARCHAR(4),
brand VARCHAR(100),
number VARCHAR(50),
set_id VARCHAR(255),
set_name VARCHAR(255),
player_id VARCHAR(255),
unique_identifier VARCHAR(255) UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_name_year_brand (name, year, brand),
INDEX idx_unique_identifier (unique_identifier)
);

-- Price history table
CREATE TABLE price_history (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
card_unique_id VARCHAR(255) NOT NULL,
source VARCHAR(50) NOT NULL, -- 'ebay', 'tcgplayer', etc.
price_low DECIMAL(10,2),
price_high DECIMAL(10,2),
price_average DECIMAL(10,2),
sample_size INT,
recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_card_date (card_unique_id, recorded_at),
INDEX idx_source (source),
FOREIGN KEY (card_unique_id) REFERENCES cards(unique_identifier)
);

-- User portfolios
CREATE TABLE portfolios (
id VARCHAR(255) PRIMARY KEY,
user_id VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
total_value DECIMAL(12,2) DEFAULT 0.00,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user_id (user_id)
);

-- Portfolio cards
CREATE TABLE portfolio_cards (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
portfolio_id VARCHAR(255) NOT NULL,
card_unique_id VARCHAR(255) NOT NULL,
quantity INT DEFAULT 1,
condition VARCHAR(10) DEFAULT 'NM',
purchase_price DECIMAL(10,2),
current_value DECIMAL(10,2),
date_added TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (portfolio_id) REFERENCES portfolios(id) ON DELETE CASCADE,
FOREIGN KEY (card_unique_id) REFERENCES cards(unique_identifier),
INDEX idx_portfolio (portfolio_id),
INDEX idx_card (card_unique_id)
);

-- Price alerts
CREATE TABLE price_alerts (
id VARCHAR(255) PRIMARY KEY,
user_id VARCHAR(255) NOT NULL,
card_unique_id VARCHAR(255) NOT NULL,
target_price DECIMAL(10,2) NOT NULL,
condition VARCHAR(10) NOT NULL, -- 'above', 'below', 'change'
percentage_threshold DECIMAL(5,2),
active BOOLEAN DEFAULT TRUE,
notification_methods JSON,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_triggered TIMESTAMP NULL,
FOREIGN KEY (card_unique_id) REFERENCES cards(unique_identifier),
INDEX idx_user_alerts (user_id),
INDEX idx_active (active),
INDEX idx_card_alert (card_unique_id)
);

Caching Strategy​

class PriceTrackingCache {
constructor() {
this.redis = new Redis(process.env.REDIS_URL);
this.cacheTTL = {
cardData: 24 * 60 * 60, // 24 hours
priceData: 15 * 60, // 15 minutes
portfolioData: 60 * 60 // 1 hour
};
}

async getCachedCardData(cardId) {
const cacheKey = `card:${cardId}`;
const cached = await this.redis.get(cacheKey);
return cached ? JSON.parse(cached) : null;
}

async setCachedCardData(cardId, data) {
const cacheKey = `card:${cardId}`;
await this.redis.setex(cacheKey, this.cacheTTL.cardData, JSON.stringify(data));
}

async getCachedPriceData(cardUniqueId) {
const cacheKey = `price:${cardUniqueId}`;
const cached = await this.redis.get(cacheKey);
return cached ? JSON.parse(cached) : null;
}

async setCachedPriceData(cardUniqueId, priceData) {
const cacheKey = `price:${cardUniqueId}`;
await this.redis.setex(cacheKey, this.cacheTTL.priceData, JSON.stringify(priceData));
}

async invalidatePriceCache(cardUniqueId) {
const cacheKey = `price:${cardUniqueId}`;
await this.redis.del(cacheKey);
}
}

Step 6: Advanced Features​

Market Trend Analysis​

import numpy as np
from datetime import datetime, timedelta

class MarketAnalyzer:
def __init__(self, price_data_manager):
self.price_data_manager = price_data_manager

def analyze_price_trends(self, card_unique_id: str, days: int = 30) -> Dict:
"""Analyze price trends for a specific card"""
end_date = datetime.now()
start_date = end_date - timedelta(days=days)

# Get price history from database
price_history = self.get_price_history(card_unique_id, start_date, end_date)

if len(price_history) < 2:
return {'error': 'Insufficient price data'}

prices = [record['price_average'] for record in price_history]
dates = [record['recorded_at'] for record in price_history]

# Calculate trend metrics
trend_analysis = {
'card_id': card_unique_id,
'analysis_period': f"{days} days",
'data_points': len(prices),
'current_price': prices[-1],
'starting_price': prices[0],
'highest_price': max(prices),
'lowest_price': min(prices),
'price_change': prices[-1] - prices[0],
'percentage_change': ((prices[-1] - prices[0]) / prices[0]) * 100,
'volatility': np.std(prices),
'trend_direction': self.calculate_trend_direction(prices),
'support_level': self.calculate_support_level(prices),
'resistance_level': self.calculate_resistance_level(prices)
}

return trend_analysis

def calculate_trend_direction(self, prices: List[float]) -> str:
"""Calculate overall trend direction using linear regression"""
if len(prices) < 2:
return 'insufficient_data'

x = np.arange(len(prices))
slope, _ = np.polyfit(x, prices, 1)

if slope > 0.05:
return 'upward'
elif slope < -0.05:
return 'downward'
else:
return 'sideways'

def calculate_support_level(self, prices: List[float]) -> float:
"""Calculate support level (recent low points)"""
if len(prices) < 5:
return min(prices)

# Find local minima
local_lows = []
for i in range(2, len(prices) - 2):
if prices[i] <= min(prices[i-2:i+3]):
local_lows.append(prices[i])

return np.percentile(local_lows, 25) if local_lows else min(prices)

def calculate_resistance_level(self, prices: List[float]) -> float:
"""Calculate resistance level (recent high points)"""
if len(prices) < 5:
return max(prices)

# Find local maxima
local_highs = []
for i in range(2, len(prices) - 2):
if prices[i] >= max(prices[i-2:i+3]):
local_highs.append(prices[i])

return np.percentile(local_highs, 75) if local_highs else max(prices)

Step 7: API Rate Limiting and Error Handling​

Robust Error Handling​

class PriceTrackingService {
constructor(config) {
this.cardIdentifier = new CardIdentifier(config.tradingCardApiKey);
this.priceDataManager = new PriceDataManager(config.priceApiKeys);
this.cache = new PriceTrackingCache();
this.rateLimiter = new RateLimiter();
}

async trackCard(cardSearchParams, options = {}) {
try {
// Rate limiting
await this.rateLimiter.checkLimit('trading-card-api');

// Try cache first
const cacheKey = this.generateCacheKey(cardSearchParams);
let cardData = await this.cache.getCachedCardData(cacheKey);

if (!cardData) {
// API call with retry logic
cardData = await this.retryApiCall(
() => this.cardIdentifier.identifyCard(cardSearchParams),
{ maxRetries: 3, backoffMs: 1000 }
);

if (cardData && cardData.length > 0) {
await this.cache.setCachedCardData(cacheKey, cardData);
}
}

if (!cardData || cardData.length === 0) {
throw new Error('Card not found');
}

// Get price data with fallback
const priceData = await this.getPriceDataWithFallback(cardData[0]);

return {
success: true,
card: cardData[0],
pricing: priceData,
timestamp: new Date()
};

} catch (error) {
return this.handleError(error, cardSearchParams);
}
}

async retryApiCall(apiCall, options = {}) {
const { maxRetries = 3, backoffMs = 1000 } = options;
let lastError;

for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await apiCall();
} catch (error) {
lastError = error;

// Don't retry on authentication errors
if (error.status === 401 || error.status === 403) {
throw error;
}

// Exponential backoff
if (attempt < maxRetries) {
await this.sleep(backoffMs * Math.pow(2, attempt - 1));
}
}
}

throw lastError;
}

async getPriceDataWithFallback(cardData) {
try {
// Try primary price source
return await this.priceDataManager.getCurrentPrices(cardData.uniqueIdentifier);
} catch (error) {
console.warn('Primary price source failed, trying fallback:', error);

// Try cached price data
const cachedPrice = await this.cache.getCachedPriceData(cardData.uniqueIdentifier);
if (cachedPrice && this.isCacheStillValid(cachedPrice, 60)) { // 1 hour tolerance
return { ...cachedPrice, source: 'cache' };
}

// Return estimated price or null
return {
cardId: cardData.uniqueIdentifier,
averagePrice: null,
source: 'unavailable',
timestamp: new Date(),
error: 'Price data temporarily unavailable'
};
}
}

handleError(error, context = {}) {
const errorResponse = {
success: false,
error: {
message: error.message,
code: error.code || 'UNKNOWN_ERROR',
timestamp: new Date(),
context
}
};

// Log error for monitoring
console.error('Price tracking error:', error, context);

// Different handling based on error type
if (error.status === 429) {
errorResponse.error.message = 'Rate limit exceeded. Please try again later.';
errorResponse.error.retryAfter = error.retryAfter || 60;
} else if (error.status >= 500) {
errorResponse.error.message = 'Service temporarily unavailable. Please try again.';
}

return errorResponse;
}

sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

generateCacheKey(params) {
return `card_search_${JSON.stringify(params)}`;
}

isCacheStillValid(cachedData, toleranceMinutes) {
const now = new Date();
const cached = new Date(cachedData.timestamp);
const diffMinutes = (now - cached) / (1000 * 60);
return diffMinutes <= toleranceMinutes;
}
}

Step 8: Card Image Management​

Overview​

Adding card images to your price tracking application enhances the user experience significantly. The Card Images API allows you to upload, store, and display high-quality images of trading cards with automatic thumbnail generation for optimal performance.

Fetching Cards with Images (v0.7.0+)​

Starting with v0.7.0, you can fetch cards and include their related images in a single API request using the ?include=images parameter:

class CardImageFetcher {
constructor(apiClient) {
this.client = apiClient;
}

async getCardWithImages(cardId) {
// Fetch card with related images included
const response = await this.client.cards.get(cardId, {
'include': 'images,set'
});

const card = response.data;
const images = this.extractImages(response.included || []);

return {
card: card,
frontImage: images.find(img => img.attributes.side === 'front'),
backImage: images.find(img => img.attributes.side === 'back')
};
}

async getPortfolioCardsWithImages(setId) {
// Fetch multiple cards with images in one request
const response = await this.client.cards.list({
'filter[set_id]': setId,
'include': 'images',
'page[limit]': 50
});

// Group images by card using relationships
return response.data.map(card => {
const cardImages = this.getImagesForCard(card, response.included || []);
return {
...card,
hasImages: card.attributes.image_uuid !== null,
frontImage: cardImages.find(img => img.attributes.side === 'front'),
backImage: cardImages.find(img => img.attributes.side === 'back')
};
});
}

extractImages(included) {
return included.filter(item => item.type === 'card_images');
}

getImagesForCard(card, included) {
// Match images using the card's image_uuid (v0.6.0+)
if (!card.attributes.image_uuid) return [];

return included.filter(item =>
item.type === 'card_images' &&
item.id === card.attributes.image_uuid
);
}
}

This approach is more efficient than fetching cards and images separately, especially when displaying portfolio lists.

Uploading Card Images​

When users add cards to their portfolio, allow them to upload front and back images:

class CardImageManager {
constructor(apiClient) {
this.client = apiClient;
}

async uploadCardImage(cardId, imageFile, imageType = 'front') {
const formData = new FormData();
formData.append('file', imageFile);
formData.append('data', JSON.stringify({
type: 'card_images',
attributes: {
card_id: cardId,
image_type: imageType
}
}));

try {
const response = await this.client.cardImages.create(formData);
return {
success: true,
imageId: response.data.id,
width: response.data.attributes.width,
height: response.data.attributes.height,
fileSize: response.data.attributes.file_size
};
} catch (error) {
return this.handleUploadError(error);
}
}

handleUploadError(error) {
if (error.status === 422) {
// Validation error
const errors = error.response.errors.map(e => e.detail);
return {
success: false,
errors: errors
};
}
return {
success: false,
errors: ['An unexpected error occurred during upload']
};
}
}

Displaying Card Images in Portfolio​

Use thumbnail variants for list views to optimize performance:

class PortfolioDisplay {
getCardImageUrl(imageId, size = 'medium') {
// Size options: small (400px), medium (800px), large (1200px), original
return `https://api.tradingcardapi.com/v1/card-images/${imageId}/download?size=${size}`;
}

renderPortfolioCard(card, imageId) {
return `
<div class="portfolio-card">
<img
src="${this.getCardImageUrl(imageId, 'small')}"
srcset="
${this.getCardImageUrl(imageId, 'small')} 1x,
${this.getCardImageUrl(imageId, 'medium')} 2x
"
alt="${card.name} - ${card.year}"
loading="lazy"
/>
<div class="card-info">
<h3>${card.name}</h3>
<p>${card.year} ${card.brand} #${card.number}</p>
<p class="price">${card.currentPrice}</p>
</div>
</div>
`;
}

renderCardDetail(card, frontImageId, backImageId) {
return `
<div class="card-detail">
<div class="card-images">
<div class="image-viewer">
<img
src="${this.getCardImageUrl(frontImageId, 'large')}"
alt="${card.name} Front"
id="main-image"
/>
</div>
<div class="image-thumbnails">
<img
src="${this.getCardImageUrl(frontImageId, 'small')}"
alt="Front"
onclick="switchImage('${frontImageId}')"
/>
${backImageId ? `
<img
src="${this.getCardImageUrl(backImageId, 'small')}"
alt="Back"
onclick="switchImage('${backImageId}')"
/>
` : ''}
</div>
</div>
<div class="card-details">
<!-- Card information and price data -->
</div>
</div>
`;
}
}

Image Caching Strategy​

Implement client-side caching for better performance:

class ImageCache {
constructor() {
this.cache = new Map();
this.maxCacheSize = 50; // Maximum number of images to cache
}

async getImage(imageId, size) {
const cacheKey = `${imageId}-${size}`;

// Check cache first
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}

// Fetch from API
const imageUrl = `https://api.tradingcardapi.com/v1/card-images/${imageId}/download?size=${size}`;
const response = await fetch(imageUrl, {
headers: {
'Authorization': `Bearer ${this.accessToken}`
}
});

if (response.ok) {
const blob = await response.blob();
const objectUrl = URL.createObjectURL(blob);

// Add to cache
this.addToCache(cacheKey, objectUrl);

return objectUrl;
}

throw new Error('Failed to load image');
}

addToCache(key, value) {
// Implement LRU cache eviction
if (this.cache.size >= this.maxCacheSize) {
const firstKey = this.cache.keys().next().value;
const oldUrl = this.cache.get(firstKey);
URL.revokeObjectURL(oldUrl); // Clean up blob URL
this.cache.delete(firstKey);
}

this.cache.set(key, value);
}

clearCache() {
this.cache.forEach(url => URL.revokeObjectURL(url));
this.cache.clear();
}
}

Client-Side Image Validation​

Validate images before uploading to improve user experience:

class ImageValidator {
static MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
static MAX_DIMENSIONS = 4000;
static ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp'];

static async validateImage(file) {
const errors = [];

// Check file size
if (file.size > this.MAX_FILE_SIZE) {
errors.push(`File size must be under 10MB (current: ${(file.size / 1024 / 1024).toFixed(2)}MB)`);
}

// Check file type
if (!this.ALLOWED_TYPES.includes(file.type)) {
errors.push(`File must be JPEG, PNG, or WebP (current: ${file.type})`);
}

// Check dimensions
const dimensions = await this.getImageDimensions(file);
if (dimensions.width > this.MAX_DIMENSIONS || dimensions.height > this.MAX_DIMENSIONS) {
errors.push(`Image dimensions must not exceed ${this.MAX_DIMENSIONS}x${this.MAX_DIMENSIONS}px (current: ${dimensions.width}x${dimensions.height}px)`);
}

return {
valid: errors.length === 0,
errors: errors
};
}

static getImageDimensions(file) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
resolve({
width: img.width,
height: img.height
});
};
img.onerror = reject;
img.src = URL.createObjectURL(file);
});
}
}

Best Practices for Card Images​

  1. Use thumbnail variants - Load small for list views, medium or large for detail views
  2. Lazy load images - Only load images when they enter the viewport
  3. Implement progressive loading - Show low-quality placeholder while high-quality image loads
  4. Cache aggressively - Store downloaded images locally to reduce API calls
  5. Validate before upload - Check file size, type, and dimensions client-side
  6. Handle missing images gracefully - Show placeholder for cards without images
  7. Allow batch uploads - Let users upload multiple card images at once
  8. Show upload progress - Provide feedback during file uploads

Example: Complete Image Upload Form​

async function handleImageUpload(event) {
const file = event.target.files[0];
const cardId = document.getElementById('card-id').value;
const imageType = document.getElementById('image-type').value;

// Validate
const validation = await ImageValidator.validateImage(file);
if (!validation.valid) {
showErrors(validation.errors);
return;
}

// Show progress
const progressBar = document.getElementById('upload-progress');
progressBar.style.display = 'block';

// Upload
const manager = new CardImageManager(apiClient);
const result = await manager.uploadCardImage(cardId, file, imageType);

if (result.success) {
showSuccess(`Image uploaded successfully! (${(result.fileSize / 1024).toFixed(2)} KB)`);
refreshCardDisplay(cardId);
} else {
showErrors(result.errors);
}

progressBar.style.display = 'none';
}

🎯 Best Practices Summary​

Performance Optimization​

  • Cache aggressively: Card data changes infrequently, cache for hours
  • Price data caching: Cache for 15-30 minutes to balance freshness and performance
  • Batch API calls: Group related requests when possible
  • Use database indexes: On frequently queried fields

Data Accuracy​

  • Unique identifiers: Always use Trading Card API's comprehensive card identification
  • Condition adjustments: Apply appropriate multipliers for card condition
  • Multiple price sources: Don't rely on a single price source
  • Data validation: Validate price data for outliers

Monitoring and Alerts​

  • Rate limit monitoring: Track API usage across all services
  • Price anomaly detection: Alert on unusual price movements
  • System health checks: Monitor all external API dependencies
  • User notification preferences: Allow users to customize alert frequency

Security Considerations​

  • API key security: Never expose keys in client-side code
  • Input validation: Validate all user inputs
  • Rate limiting: Implement user-level rate limiting
  • Data encryption: Encrypt sensitive pricing and portfolio data

πŸš€ Deployment Considerations​

Infrastructure Requirements​

  • Database: PostgreSQL or MySQL for structured data
  • Cache: Redis for session and price data caching
  • Queue system: For background price updates and alerts
  • Monitoring: Application performance monitoring (APM)

Scaling Strategy​

  • Horizontal scaling: Design for multiple application instances
  • Database sharding: By user ID or geographic region
  • CDN: For static assets and frequently accessed data
  • Load balancing: Distribute API calls across instances

This comprehensive guide provides the foundation for building a professional-grade card price tracking application. The Trading Card API serves as your source of truth for card identification, while external price APIs provide the market data needed for tracking and alerts.

Next Steps​

  1. Set up your Trading Card API account
  2. Explore the API endpoints
  3. Check out more code examples
  4. Join our developer community for support and tips

Need help implementing your price tracking application? Reach out to our developer support team or check our comprehensive API documentation.