PkmnPrices9 min di lettura

Creare un bot Discord per i prezzi Pokémon

Crea un bot con il comando slash /price in circa 50 righe di Python usando l'SDK ufficiale pkmnprices: un client asincrono, carte tipizzate e prezzi di mercato aggiornati in discord.py v2.

Nel tuo server Discord di scambi la domanda è sempre la stessa, tutto il giorno: «quanto vale questa carta?». In questo tutorial costruiamo un bot che risponde. Scrivi /price charizard e ricevi il prezzo di mercato aggiornato in un embed ordinato, preso dall'API di PkmnPrices tramite il suo SDK Python ufficiale. Sono circa 50 righe di Python e alla fine lo avrai in funzione in locale.

Cosa imparerai

  • Lo schema «prima cerca, poi recupera» usato dall'SDK (i risultati dell'elenco non contengono prezzi)
  • Come registrare un comando slash con autocompletamento in discord.py v2
  • Come l'SDK pkmnprices ti dà già pronti un client asincrono, modelli tipizzati e i tentativi automatici

Cosa costruirai

Un solo comando slash. Un utente scrive /price <card name> e il bot risponde con l'espansione della carta, la condizione e l'attuale prezzo di mercato in USD. Niente database, niente scraping, niente HTTP scritto a mano: solo l'SDK e un embed di Discord.

Dietro le quinte i passaggi sono due, perché l'API separa la ricerca dai prezzi. client.cards.list() trova la carta e ne restituisce l'ID. client.cards.get() restituisce quella carta con i prezzi allegati. Vale la pena dirlo chiaramente, perché confonde molti: la chiamata di elenco non restituisce mai dati sui prezzi. Per avere un numero devi sempre recuperare la singola carta. E siccome un nome come charizard corrisponde a decine di stampe tra espansioni e varianti, lasceremo all'utente la scelta di quella esatta prima di recuperarne il prezzo.

Prima di iniziare

Ti servono tre cose. Primo, Python 3.10 o successivo. Secondo, un'applicazione Discord con un token del bot dal Discord Developer Portal. Terzo, una chiave API di PkmnPrices, che puoi prendere gratis dalla dashboard.

Tieni i due segreti fuori dal codice. Vanno in un file .env:

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

Prepara il progetto

Crea una cartella, un ambiente virtuale e installa le dipendenze. L'SDK pkmnprices include il proprio client HTTP, i tentativi automatici e i modelli tipizzati, quindi non resta nessuna infrastruttura dell'API da scrivere.

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

Conosci l'SDK

L'SDK si occupa al posto tuo di HTTP, tentativi automatici e tipi, quindi non c'è nessuna classe client da scrivere. discord.py è asincrono, e l'SDK include un client asincrono pensato proprio per questo: AsyncPkmnPrices. Una chiamata cerca, una recupera.

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() restituisce una pagina con .data e .pagination; ogni carta è un oggetto tipizzato, quindi usi card.name e card.set.name, non chiavi di dizionario. get() aggiunge la lista prices, in cui ogni voce ha condition, variant e market_price. Questa separazione è l'unico motivo per cui facciamo due chiamate.

Costruisci il bot

Costruiremo bot.py in quattro piccoli pezzi: lo scheletro, l'avvio, una funzione di supporto per l'embed e il comando vero e proprio. Inizia dallo scheletro: gli import, un bot con gli intent predefiniti e nient'altro per ora.

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)

I comandi slash non hanno bisogno dell'intent message-content, quindi quelli predefiniti bastano. command_prefix è obbligatorio per commands.Bot ma qui non viene usato: puntiamo tutto sui comandi slash.

Registra il comando all'avvio

Due cose devono succedere una volta sola, quando il bot si avvia: creare un client dell'SDK da riutilizzare per ogni richiesta e comunicare a Discord che il comando esiste. Entrambe vanno in setup_hook, che discord.py esegue prima che il bot si connetta.

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}")

È bot.tree.sync() a far comparire /price. Le sincronizzazioni globali si propagano in qualche minuto, quindi durante lo sviluppo sincronizza su un solo server per avere aggiornamenti immediati con bot.tree.sync(guild=discord.Object(id=YOUR_GUILD_ID)).

Trasforma una carta in un embed

Tieni la presentazione fuori dal gestore del comando. Questa funzione di supporto prende una carta e restituisce un embed di Discord, con il verde del marchio sul lato. Dato che l'SDK restituisce modelli tipizzati, è tutto accesso ad attributi. Preferisce il prezzo Near Mint e, se manca, ripiega sul primo disponibile.

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

Definisci il comando /price

Ecco il problema della ricerca per nome: charizard corrisponde a decine di carte, una per ogni espansione, ristampa e variante. Prendere il primo risultato ne restituirebbe una a caso. La soluzione è l'autocompletamento di Discord (nel prossimo passaggio): l'utente sceglie una corrispondenza reale mentre scrive, e il valore dietro la sua scelta è l'ID della carta. Così il comando riceve un ID e si limita a recuperarlo, con un ripiego per il testo libero scritto senza scegliere.

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() viene eseguito per primo: due chiamate di andata e ritorno possono superare la finestra di tre secondi che Discord concede per rispondere, e il defer ti fa guadagnare fino a quindici minuti. (handle_error arriva nella prossima sezione.)

Lascia scegliere all'utente la carta giusta

La callback di autocompletamento scatta mentre l'utente scrive e restituisce fino a 25 opzioni, il massimo consentito da Discord. Abbinare il nome di ogni carta alla sua espansione trasforma un vago «Charizard» in una lista precisa: «Charizard · Base Set», «Charizard · Base Set 2» e così via. Il valore dietro ogni opzione è l'ID che il comando riceve.

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
]

Il controllo len(current) < 3 salta la ricerca finché la query non vale la pena di essere eseguita, cosa importante perché questa funzione scatta a ogni tasto premuto. Appoggiati alla cache della prossima sezione, così i prefissi ripetuti non colpiscono l'API ogni volta.

Infine, la riga che fa partire tutto. Mettila in fondo al file:

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

Gestisci errori e limiti di richieste

Un bot che si rompe al primo input sbagliato non dura un giorno in un server affollato. L'SDK solleva eccezioni tipizzate, quindi gestisci esattamente i casi che ti interessano. I due che vale la pena intercettare riguardano l'autenticazione e i limiti:

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.")

L'SDK ritenta già gli errori temporanei con backoff (puoi regolarlo con AsyncPkmnPrices(key, max_retries=2)), quindi quando RateLimitError arriva al tuo gestore il limite è reale. Il piano gratuito consente 60 richieste al minuto e 100 crediti al giorno, e gli endpoint di elenco costano 1 credito per ogni elemento restituito (documentazione sui limiti di richieste). È nell'autocompletamento che si nasconde il costo: ogni ricerca di suggerimenti restituisce fino a dieci carte e scatta mentre l'utente scrive, quindi una singola ricerca può spendere più in suggerimenti che per il prezzo vero e proprio.

Due accorgimenti tengono i costi sotto controllo. Il controllo len(current) < 3 taglia le query più corte; l'altro è una cache, così i prefissi ripetuti riusano una sola risposta invece di pagarla di nuovo. I prezzi si aggiornano ogni giorno, quindi una breve cache in memoria non ti costa nulla in precisione:

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

Chiama search_cached(bot.api, current) dalla callback di autocompletamento al posto di bot.api.cards.list, e un utente che scorre tra «char», «chari», «chariz» paga una ricerca sola, non tre.

Mettilo online

In locale ti basta un comando: python bot.py. Invita il bot in un server con il generatore di URL OAuth2 del Developer Portal e lo scope applications.commands, e /price comparirà nella lista dei comandi.

Per tenerlo sempre acceso va bene qualsiasi posto che esegua Python: un piccolo VPS, una piattaforma di container o un hosting per progetti amatoriali. L'unica regola rigida è quella dell'inizio: il token e la chiave API stanno nelle variabili d'ambiente, mai nel repository. Se la chiave API dovesse trapelare, rigenerala dalla dashboard.

Domande frequenti

Perché usare l'SDK invece di chiamare direttamente l'API?
L'SDK pkmnprices include un client asincrono (AsyncPkmnPrices) adatto a discord.py, oltre a modelli tipizzati, tentativi automatici con backoff ed eccezioni tipizzate come UnauthorizedError e RateLimitError. Tu scrivi la logica del bot, non l'infrastruttura HTTP.
Funziona con il piano gratuito?
Sì, con due limiti da conoscere. Il piano gratuito offre solo carte in inglese e consente 100 crediti al giorno a 60 richieste al minuto. Un comando /price con cache ci sta comodamente per un server piccolo o medio; un traffico intenso è il segnale per passare a un piano superiore.
Quanti crediti costa un /price?
La ricerca costa poco: 1 credito quando l'utente sceglie un suggerimento (solo il recupero della singola carta), oppure 2 con il ripiego sul testo libero. L'autocompletamento costa di più, perché ogni cards.list restituisce fino a 10 carte a 1 credito ciascuna. Il controllo dei 3 caratteri e la cache tengono la spesa sotto controllo.
Il bot può mostrare lo storico dei prezzi o dei grafici?
Sì. L'endpoint dello storico dei prezzi restituisce punti giornalieri su 7d, 30d, 90d o 365d. Trasformali in un'immagine e allegala all'embed per un comando /history, il passo successivo naturale quando /price funziona.

Per l'endpoint dello storico e ogni altra route, consulta il riferimento completo dell'API.

Per concludere

Ecco un bot dei prezzi funzionante: l'SDK ufficiale, un comando sincronizzato con autocompletamento e una risposta in embed, con gestione degli errori tipizzata e una cache per rispettare i limiti. Lo stesso schema in due passaggi, cercare un ID e poi recuperare i dati, funziona anche per i prodotti sigillati, le vendite su eBay e le offerte su Cardmarket.

Il riferimento completo dell'API copre ogni endpoint, e per iniziare a costruire ti basta una chiave gratuita dalla dashboard.

PkmnPrices

Scritto da

PkmnPrices

PkmnPrices è un’API per sviluppatori con i dati del GCC Pokémon: prezzi TCGPlayer giornalieri per oltre 80.000 carte, più di 800 espansioni e prodotti sigillati.