Back to Blog

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

PokemonPriceTracker Team

PokemonPriceTracker Team

β€’12 min read
Build a Discord Bot for Pokemon Card Prices - Complete 2025 Guide

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 VMAX and instantly get current market prices
  • Ask /compare Umbreon Alt Art, Espeon Alt Art to see side-by-side analysis
  • Check /grading Pikachu VMAX for PSA ROI calculations
  • Track /trending to 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

  1. Go to Discord Developer Portal
  2. Click "New Application"
  3. Name it "Pokemon Price Bot"
  4. Go to "Bot" section β†’ "Add Bot"
  5. 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)

  1. Go to Railway.app
  2. Click "New Project" β†’ "Deploy from GitHub"
  3. Connect your repository
  4. Add environment variables in dashboard
  5. 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

Get Your API Key

Ready to build your bot? Get your free API key:

Get Free API Key β†’


Need help? Join our Discord community or check the full source code on GitHub.

PokemonPriceTracker Team

PokemonPriceTracker Team

Discord Bot Specialists

Related Articles

How to Get Real-Time PokΓ©mon Card Prices for Your App
API
August 11, 2025

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)
API & Development
August 8, 2025

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
API Documentation
July 15, 2025

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

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.