PkmnPrices10 min de lecture

Créer un bot Discord de prix Pokémon

Créez un bot avec une commande slash /price en une cinquantaine de lignes de Python grâce au SDK officiel pkmnprices : un client async, des cartes typées et les prix du marché en direct avec discord.py v2.

Sur votre Discord d’échange, la même question revient toute la journée : « elle vaut combien, cette carte ? » Ce tutoriel construit un bot qui y répond. Tapez /price charizard et vous obtenez le prix du marché en direct dans un embed propre, tiré de l’API PkmnPrices via son SDK Python officiel. Cela tient en une cinquantaine de lignes de Python, et le bot tournera en local d’ici la fin.

Ce que vous allez apprendre

  • Le schéma « rechercher puis récupérer » qu’utilise le SDK (les résultats de liste ne contiennent aucun prix)
  • Comment enregistrer une commande slash avec autocomplétion dans discord.py v2
  • Comment le SDK pkmnprices vous donne d’office un client async, des modèles typés et des relances automatiques

Ce que vous allez construire

Une seule commande slash. Un utilisateur tape /price <card name>, et le bot répond avec l’extension de la carte, son état et son prix du marché actuel en USD. Pas de base de données, pas de scraping, pas de HTTP écrit à la main : juste le SDK et un embed Discord.

En coulisses, cela se fait en deux étapes, parce que l’API sépare la recherche des prix. client.cards.list() trouve la carte et renvoie son ID. client.cards.get() renvoie cette carte avec ses prix. Mieux vaut le dire clairement, car c’est un piège courant : l’appel de liste ne renvoie jamais de prix. Pour obtenir un chiffre, il faut toujours récupérer la carte elle-même. Et comme un nom tel que charizard correspond à des dizaines d’impressions réparties entre extensions et variantes, nous laisserons l’utilisateur choisir la bonne avant d’aller chercher son prix.

Avant de commencer

Il vous faut trois choses. D’abord, Python 3.10 ou plus récent. Ensuite, une application Discord avec un token de bot, à créer sur le Discord Developer Portal. Enfin, une clé API PkmnPrices, que vous pouvez obtenir gratuitement depuis le tableau de bord.

Gardez ces deux secrets hors de votre code. Ils vont dans un fichier .env :

bash
DISCORD_TOKEN=your-bot-token
PKMNPRICES_API_KEY=pk_your-api-key

Préparer le projet

Créez un dossier, un environnement virtuel, puis installez les dépendances. Le SDK pkmnprices apporte son propre client HTTP, ses relances et ses modèles typés : il ne reste aucune plomberie d’API à écrire.

bash
mkdir price-bot && cd price-bot
python -m venv .venv && source .venv/bin/activate
pip install pkmnprices discord.py python-dotenv

Découvrir le SDK

Le SDK s’occupe du HTTP, des relances et du typage, donc pas de classe client à écrire. discord.py est asynchrone, et le SDK fournit un client async conçu exactement pour ça : AsyncPkmnPrices. Un appel pour chercher, un appel pour récupérer.

python
from pkmnprices import AsyncPkmnPrices
async with AsyncPkmnPrices("pk_your-api-key") as client:
page = await client.cards.list(name="charizard", per_page=5) # no prices
card = await client.cards.get(page.data[0].id, currency="usd") # with prices
print(card.prices[0].market_price) # 285.0

list() renvoie une page avec .data et .pagination ; chaque carte est un objet typé, donc vous écrivez card.name et card.set.name, pas des clés de dictionnaire. get() ajoute la liste prices, dont chaque entrée a un condition, un variant et un market_price. C’est toute la raison des deux appels.

Construire le bot

Nous allons écrire bot.py en quatre petits morceaux : le squelette, le démarrage, une fonction d’aide pour l’embed, puis la commande elle-même. Commençons par le squelette : les imports, un bot avec les intents par défaut, et rien d’autre pour l’instant.

python
# bot.py
import os
import discord
from discord import app_commands
from discord.ext import commands
from dotenv import load_dotenv
from pkmnprices import AsyncPkmnPrices, UnauthorizedError, RateLimitError, CreditLimitError
load_dotenv()
intents = discord.Intents.default()
bot = commands.Bot(command_prefix="!", intents=intents)

Les commandes slash n’ont pas besoin de l’intent message-content, les valeurs par défaut suffisent donc. commands.Bot exige un command_prefix, mais il ne sert pas ici : nous misons tout sur les commandes slash.

Enregistrer la commande au démarrage

Deux choses doivent se faire une seule fois, au lancement du bot : créer un client SDK réutilisable pour chaque requête, et signaler à Discord que la commande existe. Les deux vont dans setup_hook, que discord.py exécute avant que le bot se connecte.

python
@bot.event
async def setup_hook():
bot.api = AsyncPkmnPrices(os.environ["PKMNPRICES_API_KEY"])
await bot.tree.sync()
@bot.event
async def on_ready():
print(f"Logged in as {bot.user}")

C’est bot.tree.sync() qui fait apparaître /price. Une synchronisation globale se propage en quelques minutes ; pendant le développement, synchronisez plutôt sur un seul serveur pour des mises à jour immédiates avec bot.tree.sync(guild=discord.Object(id=YOUR_GUILD_ID)).

Transformer une carte en embed

Gardez la présentation hors du gestionnaire de commande. Cette fonction prend une carte et renvoie un embed Discord, avec le vert de la marque sur le côté. Comme le SDK renvoie des modèles typés, tout passe par des attributs. Elle privilégie le prix Near Mint et se rabat sur le premier prix disponible sinon.

python
def build_embed(card):
price = next(
(p for p in card.prices if p.condition == "Near Mint"),
card.prices[0] if card.prices else None,
)
embed = discord.Embed(title=card.name, color=0x00E87B)
if card.image_url:
embed.set_thumbnail(url=card.image_url)
embed.add_field(name="Set", value=card.set.name, inline=True)
embed.add_field(name="Number", value=f"{card.number}/{card.total_set_number}", inline=True)
embed.add_field(name="Condition", value=price.condition if price else "—", inline=True)
embed.add_field(
name="Market price",
value=f"${price.market_price:.2f}" if price else "No data",
inline=True,
)
embed.set_footer(text="Data from PkmnPrices")
return embed

Définir la commande /price

Voici le problème d’une recherche par nom : charizard correspond à des dizaines de cartes, une par extension, réimpression et variante. Prendre le premier résultat en renverrait une au hasard. La solution, c’est l’autocomplétion de Discord (étape suivante) : l’utilisateur choisit une vraie correspondance pendant qu’il tape, et la valeur derrière son choix est l’ID de la carte. La commande reçoit donc un ID et se contente de le récupérer, avec un repli pour le texte libre tapé sans rien choisir.

python
@bot.tree.command(name="price", description="Look up the market price of a Pokémon card")
@app_commands.describe(card="Start typing a card name, then pick one from the list")
async def price(interaction: discord.Interaction, card: str):
await interaction.response.defer()
try:
if card.isdigit():
data = await bot.api.cards.get(int(card), currency="usd")
else:
page = await bot.api.cards.list(name=card, per_page=1)
if not page.data:
await interaction.followup.send(f"No card found for **{card}**.")
return
data = await bot.api.cards.get(page.data[0].id, currency="usd")
await interaction.followup.send(embed=build_embed(data))
except Exception as err:
await handle_error(interaction, err)

await interaction.response.defer() s’exécute en premier : deux allers-retours peuvent dépasser le délai de réponse de trois secondes imposé par Discord, et différer la réponse vous laisse jusqu’à quinze minutes. (handle_error arrive dans la section suivante.)

Laisser l’utilisateur choisir la bonne carte

Le callback d’autocomplétion se déclenche pendant la saisie et renvoie jusqu’à 25 choix, le maximum de Discord. Associer chaque nom de carte à son extension transforme un vague « Charizard » en liste précise : « Charizard · Base Set », « Charizard · Base Set 2 », et ainsi de suite. La valeur derrière chaque choix est l’ID que reçoit la commande.

python
@price.autocomplete("card")
async def card_autocomplete(interaction: discord.Interaction, current: str):
if len(current) < 3:
return []
page = await bot.api.cards.list(name=current, per_page=10)
return [
app_commands.Choice(name=f"{c.name} · {c.set.name}"[:100], value=str(c.id))
for c in page.data
]

Le garde-fou len(current) < 3 attend que la requête vaille la peine d’être lancée, ce qui compte puisque ce callback se déclenche à chaque frappe. Appuyez-vous sur le cache de la section suivante pour que les préfixes répétés n’appellent pas l’API à chaque fois.

Enfin, la ligne qui lance le tout. Placez-la en bas du fichier :

python
bot.run(os.environ["DISCORD_TOKEN"])

Gérer les erreurs et les limites de requêtes

Un bot qui plante à la première saisie invalide ne tiendra pas une journée sur un serveur actif. Le SDK lève des exceptions typées, vous traitez donc exactement les cas qui vous intéressent. Les deux qui valent la peine d’être interceptées correspondent à l’authentification et aux limites :

python
async def handle_error(interaction, err):
if isinstance(err, UnauthorizedError):
await interaction.followup.send("API key is missing or invalid.")
elif isinstance(err, (RateLimitError, CreditLimitError)):
await interaction.followup.send("Rate or credit limit hit. Try again shortly.")
else:
print(err)
await interaction.followup.send("Something went wrong. Try again in a moment.")

Le SDK relance déjà les erreurs passagères avec un délai croissant (réglable via AsyncPkmnPrices(key, max_retries=2)) : quand RateLimitError arrive jusqu’à votre gestionnaire, la limite est bien réelle. L’offre gratuite autorise 60 requêtes par minute et 100 crédits par jour, et les endpoints de liste facturent 1 crédit par élément renvoyé (documentation sur les limites de requêtes). C’est dans l’autocomplétion que le coût se cache : chaque recherche de suggestions renvoie jusqu’à dix cartes et se déclenche pendant la saisie, si bien qu’une seule consultation peut coûter plus en suggestions qu’en prix.

Deux garde-fous gardent la facture raisonnable. Le seuil len(current) < 3 écarte les requêtes les plus courtes ; l’autre est un cache, pour que les préfixes répétés réutilisent une même réponse au lieu de la payer à nouveau. Les prix sont mis à jour une fois par jour, donc un petit cache en mémoire ne vous coûte rien en précision :

python
import time
_cache = {}
TTL = 10 * 60 # seconds
async def search_cached(api, name):
key = name.lower()
hit = _cache.get(key)
if hit and time.monotonic() - hit["at"] < TTL:
return hit["data"]
page = await api.cards.list(name=name, per_page=10)
_cache[key] = {"data": page.data, "at": time.monotonic()}
return page.data

Appelez search_cached(bot.api, current) depuis le callback d’autocomplétion au lieu de bot.api.cards.list, et un utilisateur qui tape « char », « chari », « chariz » paie une seule recherche, pas trois.

Le déployer

En local, il ne reste qu’une commande : python bot.py. Invitez le bot sur un serveur via le générateur d’URL OAuth2 du Developer Portal avec le scope applications.commands, et /price apparaît dans la liste des commandes.

Pour un hébergement permanent, tout ce qui fait tourner du Python convient : un petit VPS, une plateforme de conteneurs ou un hébergeur pour projets perso. La seule règle stricte est celle du début : votre token et votre clé API vivent dans des variables d’environnement, jamais dans le dépôt. Si la clé API fuite un jour, renouvelez-la depuis le tableau de bord.

Questions fréquentes

Pourquoi utiliser le SDK plutôt que d’appeler l’API directement ?
Le SDK pkmnprices fournit un client async (AsyncPkmnPrices) adapté à discord.py, ainsi que des modèles typés, des relances intégrées avec délai croissant et des exceptions typées comme UnauthorizedError et RateLimitError. Vous écrivez la logique du bot, pas la plomberie HTTP.
Est-ce que ça fonctionne avec l’offre gratuite ?
Oui, avec deux limites à connaître. L’offre gratuite ne sert que les cartes anglaises et autorise 100 crédits par jour à 60 requêtes par minute. Une commande /price avec cache y tient largement pour un serveur petit ou moyen ; un trafic important est le signe qu’il faut passer à une offre supérieure.
Combien de crédits coûte un /price ?
La consultation coûte peu : 1 crédit quand l’utilisateur choisit une suggestion (seule la carte est récupérée), ou 2 avec le repli en texte libre. L’autocomplétion coûte davantage, puisque chaque cards.list renvoie jusqu’à 10 cartes à 1 crédit chacune. Le seuil de 3 caractères et le cache gardent cela sous contrôle.
Le bot peut-il afficher l’historique des prix ou des graphiques ?
Oui. L’endpoint d’historique des prix renvoie des points quotidiens sur 7d, 30d, 90d ou 365d. Dessinez-les dans une image et joignez-la à l’embed pour une commande /history, une suite naturelle une fois /price en place.

Pour l’endpoint d’historique et toutes les autres routes, consultez la référence complète de l’API.

Pour conclure

Voilà un bot de prix fonctionnel : le SDK officiel, une commande synchronisée avec autocomplétion et une réponse en embed, avec une gestion d’erreurs typée et un cache pour respecter les limites. Le même schéma en deux étapes, chercher un ID puis récupérer les données, s’applique aussi aux produits scellés, aux ventes eBay et aux annonces Cardmarket.

La référence complète de l’API couvre chaque endpoint, et une clé gratuite depuis le tableau de bord suffit pour commencer.

PkmnPrices

Écrit par

PkmnPrices

PkmnPrices est une API pour développeurs consacrée aux données du JCC Pokémon : prix TCGPlayer quotidiens pour plus de 80 000 cartes, plus de 800 extensions et les produits scellés.