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
},
"relationships": {
"set": {
"data": { "id": "fleer-1986-basketball", "type": "sets" }
},
"player": {
"data": { "id": "michael-jordan", "type": "players" }
}
}
}

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;
}
}

🎯 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.