Build a Discord Bot for Pokemon Card Prices - Complete 2025 Guide

PokemonPriceTracker Team

Build a Discord Bot for Pokemon Card Prices
Why Your Discord Server Needs a Pokemon Price Bot
Imagine your Pokemon collecting Discord community being able to:
- Type
/price Charizard VMAXand instantly get current market prices - Ask
/compare Umbreon Alt Art, Espeon Alt Artto see side-by-side analysis - Check
/grading Pikachu VMAXfor PSA ROI calculations - Track
/trendingto see hot cards this week
No more leaving Discord to check prices. No more outdated information. Just instant, accurate data powered by a custom bot.
What You'll Build
A full-featured Discord bot with these commands:
- π
/price <card>- Get current card prices with embed - π
/compare <card1>, <card2>- Compare multiple cards - π
/grading <card>- PSA grading ROI analysis - π₯
/trending- See market movers - π
/history <card>- Price history chart - β
/set <name>- View set information - π―
/search <query>- Advanced card search
Prerequisites
- Node.js 18+ installed
- Discord account & server for testing
- PokemonPriceTracker API key (get free)
- Basic JavaScript knowledge
Step 1: Set Up Discord Bot
1.1 Create Discord Application
- Go to Discord Developer Portal
- Click "New Application"
- Name it "Pokemon Price Bot"
- Go to "Bot" section β "Add Bot"
- Copy your bot token (keep this secret!)
1.2 Set Bot Permissions
In the "Bot" section, enable these intents:
- β Presence Intent
- β Server Members Intent
- β Message Content Intent
Under "OAuth2" β "URL Generator":
- Scopes:
bot,applications.commands - Bot Permissions:
- Send Messages
- Embed Links
- Read Message History
- Use Slash Commands
Copy the generated URL and invite your bot to your test server.
Step 2: Initialize Project
mkdir pokemon-price-bot
cd pokemon-price-bot
npm init -y
# Install dependencies
npm install discord.js dotenv axios canvas chart.js chartjs-node-canvas
Project Structure
pokemon-price-bot/
βββ .env
βββ .gitignore
βββ package.json
βββ index.js
βββ commands/
β βββ price.js
β βββ compare.js
β βββ grading.js
β βββ trending.js
β βββ search.js
βββ utils/
β βββ api.js
β βββ embeds.js
β βββ charts.js
βββ deploy-commands.js
Configure Environment Variables (.env)
DISCORD_TOKEN=your_discord_bot_token
CLIENT_ID=your_discord_client_id
GUILD_ID=your_test_server_id
POKEMON_API_KEY=your_pokemonpricetracker_api_key
Step 3: Create API Wrapper
Create utils/api.js:
const axios = require('axios');
const API_BASE = 'https://www.pokemonpricetracker.com/api/v2';
class PokemonAPI {
constructor(apiKey) {
this.axios = axios.create({
baseURL: API_BASE,
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
}
});
}
async searchCards(query, options = {}) {
const { data } = await this.axios.get('/cards', {
params: {
search: query,
limit: options.limit || 10,
...options
}
});
return data;
}
async parseTitle(title) {
const { data } = await this.axios.post('/parse-title', {
title
});
return data;
}
async getCardDetails(tcgPlayerId) {
const { data } = await this.axios.get(`/cards/${tcgPlayerId}`);
return data;
}
async getSets(limit = 20) {
const { data } = await this.axios.get('/sets', {
params: { limit }
});
return data;
}
async getMarketMovers() {
const { data } = await this.axios.get('/market-movers');
return data;
}
}
module.exports = PokemonAPI;
Step 4: Create Embed Builder
Create utils/embeds.js for beautiful Discord embeds:
const { EmbedBuilder } = require('discord.js');
function createPriceEmbed(card) {
const embed = new EmbedBuilder()
.setColor(0x3B82F6)
.setTitle(card.name)
.setDescription(`**${card.setName}** β’ ${card.rarity}`)
.setThumbnail(card.imageUrl || card.image?.url)
.addFields(
{
name: 'π° Market Price',
value: `$${card.prices.market.toFixed(2)}`,
inline: true
},
{
name: 'π 30d Change',
value: card.priceChange30d ? `${card.priceChange30d > 0 ? '+' : ''}${card.priceChange30d.toFixed(1)}%` : 'N/A',
inline: true
},
{
name: 'π’ TCGPlayer ID',
value: card.tcgPlayerId,
inline: true
}
)
.setFooter({
text: 'Data from PokemonPriceTracker.com',
iconURL: 'https://www.pokemonpricetracker.com/icon.svg'
})
.setTimestamp();
// Add pricing breakdown if available
if (card.prices) {
let priceFields = [];
if (card.prices.low) priceFields.push(`Low: $${card.prices.low.toFixed(2)}`);
if (card.prices.mid) priceFields.push(`Mid: $${card.prices.mid.toFixed(2)}`);
if (card.prices.high) priceFields.push(`High: $${card.prices.high.toFixed(2)}`);
if (priceFields.length > 0) {
embed.addFields({
name: 'π΅ Price Range',
value: priceFields.join(' β’ '),
inline: false
});
}
}
return embed;
}
function createGradingEmbed(card, gradingData) {
const rawValue = card.prices.market;
const psa9Value = gradingData.psa9Average || rawValue * 1.15;
const psa10Value = gradingData.psa10Average || rawValue * 1.75;
const gradingCost = 30; // Average PSA cost
const psa9Profit = psa9Value - rawValue - gradingCost;
const psa10Profit = psa10Value - rawValue - gradingCost;
const embed = new EmbedBuilder()
.setColor(0x10B981)
.setTitle(`π― PSA Grading Analysis: ${card.name}`)
.setDescription(`**${card.setName}**`)
.setThumbnail(card.imageUrl || card.image?.url)
.addFields(
{
name: 'π¦ Raw Card Value',
value: `$${rawValue.toFixed(2)}`,
inline: true
},
{
name: 'π₯ PSA 9 Average',
value: `$${psa9Value.toFixed(2)}`,
inline: true
},
{
name: 'π₯ PSA 10 Average',
value: `$${psa10Value.toFixed(2)}`,
inline: true
},
{
name: 'π° PSA 9 ROI',
value: psa9Profit > 0
? `β
+$${psa9Profit.toFixed(2)} (${((psa9Profit/rawValue)*100).toFixed(1)}% profit)`
: `β -$${Math.abs(psa9Profit).toFixed(2)} (loss)`,
inline: false
},
{
name: 'π PSA 10 ROI',
value: psa10Profit > 0
? `β
+$${psa10Profit.toFixed(2)} (${((psa10Profit/rawValue)*100).toFixed(1)}% profit)`
: `β -$${Math.abs(psa10Profit).toFixed(2)} (loss)`,
inline: false
}
);
// Recommendation
let recommendation = '';
if (psa10Profit > 50) {
recommendation = 'π’ **Strong Grade Candidate** - Excellent ROI potential if card is gem mint.';
} else if (psa9Profit > 20) {
recommendation = 'π‘ **Moderate Grade Candidate** - Worthwhile if confident in PSA 9+.';
} else {
recommendation = 'π΄ **Not Recommended** - Better to sell raw unless pristine condition.';
}
embed.addFields({
name: 'π Recommendation',
value: recommendation,
inline: false
});
embed.setFooter({
text: 'Grading cost estimate: $30 β’ Data from PokemonPriceTracker.com',
iconURL: 'https://www.pokemonpricetracker.com/icon.svg'
});
return embed;
}
function createCompareEmbed(cards) {
const embed = new EmbedBuilder()
.setColor(0x8B5CF6)
.setTitle(`π Card Comparison (${cards.length} cards)`);
cards.forEach((card, index) => {
embed.addFields({
name: `${index + 1}. ${card.name}`,
value: [
`**Set**: ${card.setName}`,
`**Price**: $${card.prices.market.toFixed(2)}`,
`**Rarity**: ${card.rarity}`,
`**30d**: ${card.priceChange30d ? (card.priceChange30d > 0 ? '+' : '') + card.priceChange30d.toFixed(1) + '%' : 'N/A'}`
].join('\n'),
inline: true
});
});
embed.setFooter({
text: 'Data from PokemonPriceTracker.com',
iconURL: 'https://www.pokemonpricetracker.com/icon.svg'
});
return embed;
}
module.exports = {
createPriceEmbed,
createGradingEmbed,
createCompareEmbed
};
Step 5: Implement Commands
Create commands/price.js:
const { SlashCommandBuilder } = require('discord.js');
const { createPriceEmbed } = require('../utils/embeds');
module.exports = {
data: new SlashCommandBuilder()
.setName('price')
.setDescription('Get the current price of a Pokemon card')
.addStringOption(option =>
option.setName('card')
.setDescription('Card name (e.g., "Charizard VMAX Darkness Ablaze")')
.setRequired(true)
),
async execute(interaction, api) {
await interaction.deferReply();
try {
const cardQuery = interaction.options.getString('card');
// Use parse-title for fuzzy matching
const parseResult = await api.parseTitle(cardQuery);
if (!parseResult.matches || parseResult.matches.length === 0) {
return interaction.editReply({
content: `β Could not find a card matching "${cardQuery}". Try being more specific!`,
ephemeral: true
});
}
const topMatch = parseResult.matches[0];
const card = topMatch.card;
const confidence = topMatch.confidence;
const embed = createPriceEmbed(card);
// Add confidence indicator if not perfect match
if (confidence < 1.0) {
embed.setDescription(
`${embed.data.description}\n\n*Match confidence: ${(confidence * 100).toFixed(0)}%*`
);
}
await interaction.editReply({ embeds: [embed] });
} catch (error) {
console.error('Error in price command:', error);
await interaction.editReply({
content: 'β An error occurred while fetching card data. Please try again.',
ephemeral: true
});
}
}
};
Create commands/grading.js:
const { SlashCommandBuilder } = require('discord.js');
const { createGradingEmbed } = require('../utils/embeds');
module.exports = {
data: new SlashCommandBuilder()
.setName('grading')
.setDescription('Analyze PSA grading ROI for a card')
.addStringOption(option =>
option.setName('card')
.setDescription('Card name')
.setRequired(true)
),
async execute(interaction, api) {
await interaction.deferReply();
try {
const cardQuery = interaction.options.getString('card');
const parseResult = await api.parseTitle(cardQuery);
if (!parseResult.matches || parseResult.matches.length === 0) {
return interaction.editReply({
content: `β Could not find "${cardQuery}"`,
ephemeral: true
});
}
const card = parseResult.matches[0].card;
// Fetch detailed data with PSA info
const details = await api.getCardDetails(card.tcgPlayerId);
const gradingData = {
psa9Average: details.ebayData?.psa9Average,
psa10Average: details.ebayData?.psa10Average,
population: details.psaPopulation
};
const embed = createGradingEmbed(card, gradingData);
await interaction.editReply({ embeds: [embed] });
} catch (error) {
console.error('Error in grading command:', error);
await interaction.editReply({
content: 'β Error analyzing grading ROI',
ephemeral: true
});
}
}
};
Create commands/compare.js:
const { SlashCommandBuilder } = require('discord.js');
const { createCompareEmbed } = require('../utils/embeds');
module.exports = {
data: new SlashCommandBuilder()
.setName('compare')
.setDescription('Compare prices of multiple cards')
.addStringOption(option =>
option.setName('cards')
.setDescription('Card names separated by commas (e.g., "Charizard VMAX, Pikachu VMAX")')
.setRequired(true)
),
async execute(interaction, api) {
await interaction.deferReply();
try {
const cardsInput = interaction.options.getString('cards');
const cardQueries = cardsInput.split(',').map(q => q.trim());
if (cardQueries.length < 2) {
return interaction.editReply({
content: 'β Please provide at least 2 cards separated by commas',
ephemeral: true
});
}
if (cardQueries.length > 5) {
return interaction.editReply({
content: 'β Maximum 5 cards for comparison',
ephemeral: true
});
}
const cards = [];
for (const query of cardQueries) {
const parseResult = await api.parseTitle(query);
if (parseResult.matches && parseResult.matches.length > 0) {
cards.push(parseResult.matches[0].card);
}
}
if (cards.length === 0) {
return interaction.editReply({
content: 'β Could not find any matching cards',
ephemeral: true
});
}
const embed = createCompareEmbed(cards);
await interaction.editReply({ embeds: [embed] });
} catch (error) {
console.error('Error in compare command:', error);
await interaction.editReply({
content: 'β Error comparing cards',
ephemeral: true
});
}
}
};
Step 6: Create Main Bot File
Create index.js:
require('dotenv').config();
const { Client, Collection, GatewayIntentBits } = require('discord.js');
const fs = require('fs');
const path = require('path');
const PokemonAPI = require('./utils/api');
// Initialize Discord client
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent
]
});
// Initialize Pokemon API
const pokemonAPI = new PokemonAPI(process.env.POKEMON_API_KEY);
// Load commands
client.commands = new Collection();
const commandsPath = path.join(__dirname, 'commands');
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
for (const file of commandFiles) {
const filePath = path.join(commandsPath, file);
const command = require(filePath);
client.commands.set(command.data.name, command);
}
// Bot ready event
client.once('ready', () => {
console.log(`β
Bot is online as ${client.user.tag}!`);
client.user.setActivity('/price | Pokemon Card Prices', { type: 'WATCHING' });
});
// Handle slash commands
client.on('interactionCreate', async interaction => {
if (!interaction.isChatInputCommand()) return;
const command = client.commands.get(interaction.commandName);
if (!command) return;
try {
await command.execute(interaction, pokemonAPI);
} catch (error) {
console.error(`Error executing ${interaction.commandName}:`, error);
await interaction.reply({
content: 'β There was an error executing this command!',
ephemeral: true
});
}
});
// Error handling
client.on('error', error => {
console.error('Discord client error:', error);
});
process.on('unhandledRejection', error => {
console.error('Unhandled promise rejection:', error);
});
// Login
client.login(process.env.DISCORD_TOKEN);
Step 7: Deploy Commands
Create deploy-commands.js:
require('dotenv').config();
const { REST, Routes } = require('discord.js');
const fs = require('fs');
const commands = [];
const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js'));
for (const file of commandFiles) {
const command = require(`./commands/${file}`);
commands.push(command.data.toJSON());
}
const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN);
(async () => {
try {
console.log(`Started refreshing ${commands.length} application (/) commands.`);
const data = await rest.put(
Routes.applicationGuildCommands(process.env.CLIENT_ID, process.env.GUILD_ID),
{ body: commands },
);
console.log(`β
Successfully reloaded ${data.length} application (/) commands.`);
} catch (error) {
console.error(error);
}
})();
Run it:
node deploy-commands.js
Step 8: Run Your Bot
node index.js
Test in Discord:
/price Charizard VMAX/grading Umbreon VMAX Alt Art/compare Pikachu VMAX, Raichu VMAX
Step 9: Deploy to Production
Option 1: Heroku (Free Tier)
# Install Heroku CLI
npm install -g heroku
# Login and create app
heroku login
heroku create pokemon-price-bot
# Set environment variables
heroku config:set DISCORD_TOKEN=your_token
heroku config:set POKEMON_API_KEY=your_api_key
heroku config:set CLIENT_ID=your_client_id
# Create Procfile
echo "worker: node index.js" > Procfile
# Deploy
git init
git add .
git commit -m "Initial commit"
git push heroku main
# Scale worker
heroku ps:scale worker=1
Option 2: Railway.app (Recommended)
- Go to Railway.app
- Click "New Project" β "Deploy from GitHub"
- Connect your repository
- Add environment variables in dashboard
- Deploy automatically
Option 3: VPS (DigitalOcean, Linode)
# On your VPS
git clone your-repo
cd pokemon-price-bot
npm install
# Use PM2 for process management
npm install -g pm2
pm2 start index.js --name pokemon-bot
pm2 save
pm2 startup
Advanced Features
Rate Limiting
Add cooldowns to prevent spam:
const cooldowns = new Collection();
// In command execution
const { cooldowns } = interaction.client;
if (!cooldowns.has(command.data.name)) {
cooldowns.set(command.data.name, new Collection());
}
const now = Date.now();
const timestamps = cooldowns.get(command.data.name);
const cooldownAmount = (command.cooldown || 3) * 1000;
if (timestamps.has(interaction.user.id)) {
const expirationTime = timestamps.get(interaction.user.id) + cooldownAmount;
if (now < expirationTime) {
const timeLeft = (expirationTime - now) / 1000;
return interaction.reply({
content: `Please wait ${timeLeft.toFixed(1)} more seconds before using \`${command.data.name}\` again.`,
ephemeral: true
});
}
}
timestamps.set(interaction.user.id, now);
setTimeout(() => timestamps.delete(interaction.user.id), cooldownAmount);
Button Interactions
Add buttons for pagination:
const { ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');
const row = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setCustomId('previous')
.setLabel('Previous')
.setStyle(ButtonStyle.Primary),
new ButtonBuilder()
.setCustomId('next')
.setLabel('Next')
.setStyle(ButtonStyle.Primary)
);
await interaction.editReply({
embeds: [embed],
components: [row]
});
Troubleshooting
Bot doesn't respond to commands:
- Verify bot has proper permissions in server
- Check if commands are registered (
deploy-commands.js) - Ensure intents are enabled in Discord Developer Portal
API errors:
- Verify API key is valid
- Check rate limits (100 calls/day on free tier)
- Confirm API endpoints are correct
Deployment issues:
- Check environment variables are set
- Verify Node.js version (18+ required)
- Review logs for error messages
Cost Breakdown
Free Setup:
- Discord bot: Free
- PokemonPriceTracker API: Free (100 credits/day)
- Railway/Heroku hosting: Free tier
- Total: $0/month
Production Setup:
- Discord bot: Free
- PokemonPriceTracker API: $9.99/month (20K credits/day)
- Railway Pro: $5/month
- Total: $14.99/month
What's Next?
Enhance your bot with:
- Price history charts with Chart.js
- Automatic price alerts
- Collection tracking per user
- Set EV calculations
- Market trend notifications
Related Tutorials
- ChatGPT Custom GPT for Pokemon Cards
- Python Pokemon Price Automation
- Telegram Pokemon Price Alert Bot
Get Your API Key
Ready to build your bot? Get your free API key:
Need help? Join our Discord community or check the full source code on GitHub.

PokemonPriceTracker Team
Discord Bot Specialists
Related Articles

How to Get Real-Time PokΓ©mon Card Prices for Your App
Want to display real-time PokΓ©mon card prices in your application? This technical guide shows you how to use our real-time API to power your project.
PokemonPriceTracker Team

Pokemon Card API: Complete Developer's Guide to Card Data APIs (2025)
Complete Pokemon card API guide for developers. Learn about available APIs, integration methods, pricing data access, and how to build Pokemon card applications.
PokemonPriceTracker Team
Pokemon Card Price API: Complete Developer Guide to Real-Time TCG Data 2025
Integrate Pokemon card pricing into your applications with our comprehensive API guide. Learn endpoints, authentication, rate limits, and best practices for building Pokemon TCG applications with real-time market data.

API Development Team
Stay Updated
Subscribe to our newsletter for the latest Pokemon card market trends, investment opportunities, and exclusive insights delivered straight to your inbox.