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
pkmnpricesvous 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 :
DISCORD_TOKEN=your-bot-tokenPKMNPRICES_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.
mkdir price-bot && cd price-botpython -m venv .venv && source .venv/bin/activatepip 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.
from pkmnprices import AsyncPkmnPricesasync with AsyncPkmnPrices("pk_your-api-key") as client:page = await client.cards.list(name="charizard", per_page=5) # no pricescard = await client.cards.get(page.data[0].id, currency="usd") # with pricesprint(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.
# bot.pyimport osimport discordfrom discord import app_commandsfrom discord.ext import commandsfrom dotenv import load_dotenvfrom pkmnprices import AsyncPkmnPrices, UnauthorizedError, RateLimitError, CreditLimitErrorload_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.
@bot.eventasync def setup_hook():bot.api = AsyncPkmnPrices(os.environ["PKMNPRICES_API_KEY"])await bot.tree.sync()@bot.eventasync 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.
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.
@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}**.")returndata = 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.
@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 :
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 :
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 :
import time_cache = {}TTL = 10 * 60 # secondsasync 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.

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