PkmnPrices10 min de lectura

Cómo crear un bot de Discord con precios de Pokémon

Crea un bot con el comando /price en unas 50 líneas de Python con el SDK oficial pkmnprices: un cliente asíncrono, cartas tipadas y precios de mercado en directo en discord.py v2.

En tu Discord de intercambios se repite la misma pregunta todo el día: «¿cuánto vale esta carta?». En este tutorial vas a crear un bot que la responde. Escribes /price charizard y te devuelve el precio de mercado en directo en un embed limpio, sacado de la API de PkmnPrices a través de su SDK oficial de Python. Son unas 50 líneas de Python y, al terminar, lo tendrás funcionando en local.

Qué vas a aprender

  • El patrón de buscar y luego obtener que usa el SDK (los resultados del listado no llevan precios)
  • Cómo registrar un comando de barra con autocompletado en discord.py v2
  • Cómo el SDK pkmnprices te da sin esfuerzo un cliente asíncrono, modelos tipados y reintentos

Qué vas a crear

Un único comando de barra. El usuario escribe /price <card name> y el bot responde con la expansión de la carta, su estado y su precio de mercado actual en USD. Sin base de datos, sin scraping y sin HTTP escrito a mano: solo el SDK y un embed de Discord.

Por dentro son dos pasos, porque la API separa la búsqueda de los precios. client.cards.list() encuentra la carta y devuelve su ID. client.cards.get() devuelve esa carta con sus precios. Conviene decirlo claro, porque confunde a mucha gente: la llamada de listado nunca devuelve datos de precios. Para tener una cifra siempre hay que pedir la carta concreta. Y como un nombre como charizard coincide con decenas de impresiones de distintas expansiones y variantes, dejaremos que el usuario elija la exacta antes de pedir su precio.

Antes de empezar

Necesitas tres cosas. Primero, Python 3.10 o posterior. Segundo, una aplicación de Discord con un token de bot del Discord Developer Portal. Tercero, una clave de API de PkmnPrices, que puedes conseguir gratis en el panel.

No metas los dos secretos en el código. Van en un archivo .env:

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

Prepara el proyecto

Crea una carpeta, un entorno virtual e instala las dependencias. El SDK pkmnprices trae su propio cliente HTTP, reintentos y modelos tipados, así que no te queda fontanería de API por escribir.

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

Conoce el SDK

El SDK se encarga del HTTP, los reintentos y los tipos, así que no tienes que escribir ninguna clase cliente. discord.py es asíncrono, y el SDK incluye un cliente asíncrono hecho justo para esto: AsyncPkmnPrices. Una llamada busca y otra obtiene.

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() devuelve una página con .data y .pagination; cada carta es un objeto tipado, así que usas card.name y card.set.name, no claves de diccionario. get() añade la lista prices, en la que cada entrada tiene condition, variant y market_price. Esa separación es justo la razón por la que hacemos dos llamadas.

Crea el bot

Vamos a montar bot.py en cuatro piezas pequeñas: el esqueleto, el arranque, una función auxiliar para el embed y el propio comando. Empieza por el esqueleto: los imports, un bot con los intents por defecto y nada más por ahora.

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)

Los comandos de barra no necesitan el intent de contenido de mensajes, así que con los intents por defecto basta. commands.Bot exige un command_prefix, pero aquí no se usa: vamos a trabajar solo con comandos de barra.

Registra el comando al arrancar

Hay dos cosas que tienen que pasar una sola vez, cuando el bot se inicia: crear un cliente del SDK que pueda reutilizar en cada solicitud y avisar a Discord de que el comando existe. Las dos van en setup_hook, que discord.py ejecuta antes de que el bot se conecte.

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() es lo que hace que aparezca /price. Las sincronizaciones globales tardan unos minutos en propagarse, así que mientras desarrollas sincroniza con un solo servidor para ver los cambios al instante con bot.tree.sync(guild=discord.Object(id=YOUR_GUILD_ID)).

Convierte una carta en un embed

Saca la presentación del manejador del comando. Esta función recibe una carta y devuelve un embed de Discord con el verde de la marca en el lateral. Como el SDK devuelve modelos tipados, todo es acceso a atributos. Prefiere el precio Near Mint y, si no lo hay, usa el primero que encuentre.

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

Define el comando /price

Aquí está la pega de buscar por nombre: charizard coincide con decenas de cartas, una por cada expansión, reimpresión y variante. Quedarse con el primer resultado devolvería una cualquiera. La solución es el autocompletado de Discord (el siguiente paso): el usuario elige una coincidencia real mientras escribe, y el valor que hay detrás de su elección es el ID de la carta. Así el comando recibe un ID y simplemente lo pide, con una alternativa para el texto libre escrito sin elegir nada.

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() va primero: dos idas y vueltas pueden superar el margen de tres segundos que da Discord para responder, y aplazar la respuesta te da hasta quince minutos. (handle_error llega en la siguiente sección.)

Deja que el usuario elija la carta correcta

La función de autocompletado se ejecuta mientras el usuario escribe y devuelve hasta 25 opciones, el máximo de Discord. Al juntar el nombre de cada carta con su expansión, un «Charizard» ambiguo se convierte en una lista precisa: «Charizard · Base Set», «Charizard · Base Set 2», etc. El valor detrás de cada opción es el ID que recibe el comando.

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
]

La condición len(current) < 3 evita buscar hasta que la consulta merezca la pena, lo cual importa porque esto se ejecuta con cada pulsación. Apóyate en la caché de la siguiente sección para que los prefijos repetidos no lleguen cada uno a la API.

Por último, la línea que lo pone todo en marcha. Ponla al final del archivo:

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

Gestiona los errores y los límites

Un bot que se rompe con la primera entrada mala no aguanta ni un día en un servidor con movimiento. El SDK lanza excepciones tipadas, así que gestionas justo los casos que te interesan. Los dos que vale la pena capturar corresponden a la autenticación y a los límites:

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

El SDK ya reintenta los fallos transitorios con espera progresiva (puedes ajustarlo con AsyncPkmnPrices(key, max_retries=2)), así que cuando un RateLimitError llega a tu manejador, el límite es real. El plan gratis permite 60 solicitudes por minuto y 100 créditos al día, y los endpoints de listado cobran 1 crédito por elemento devuelto (documentación sobre límites de solicitudes). El coste se esconde en el autocompletado: cada búsqueda de sugerencias devuelve hasta diez cartas y se lanza mientras el usuario escribe, así que en una sola consulta puedes gastar más en sugerencias que en el propio precio.

Dos protecciones lo mantienen asequible. La condición len(current) < 3 recorta las consultas más cortas; la otra es una caché, para que los prefijos repetidos reutilicen una respuesta en lugar de pagarla de nuevo. Los precios se actualizan a diario, así que una caché corta en memoria no te cuesta nada en precisión:

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

Llama a search_cached(bot.api, current) desde la función de autocompletado en lugar de a bot.api.cards.list, y un usuario que vaya escribiendo «char», «chari», «chariz» pagará una búsqueda, no tres.

Despliégalo

En local, estás a un comando: python bot.py. Invita al bot a un servidor con el generador de URL de OAuth2 del Developer Portal y el scope applications.commands, y /price aparecerá en la lista de comandos.

Para tenerlo siempre encendido, vale cualquier sitio que ejecute Python: un VPS pequeño, una plataforma de contenedores o un hosting para proyectos personales. La única regla estricta es la del principio: el token y la clave de API viven en variables de entorno, nunca en el repositorio. Si la clave de API se filtra alguna vez, rótala desde el panel.

Preguntas frecuentes

¿Por qué usar el SDK en lugar de llamar directamente a la API?
El SDK pkmnprices incluye un cliente asíncrono (AsyncPkmnPrices) que encaja con discord.py, además de modelos tipados, reintentos integrados con espera progresiva y excepciones tipadas como UnauthorizedError y RateLimitError. Tú escribes la lógica del bot, no la fontanería HTTP.
¿Funciona con el plan gratis?
Sí, con dos límites que conviene conocer. El plan gratis solo sirve cartas en inglés y permite 100 créditos al día a 60 solicitudes por minuto. Un comando /price con caché cabe de sobra ahí para un servidor pequeño o mediano; si el tráfico es alto, es el momento de subir de plan.
¿Cuántos créditos cuesta un /price?
La consulta es barata: 1 crédito cuando el usuario elige una sugerencia (solo se pide esa carta), o 2 si se usa la alternativa de texto libre. El autocompletado cuesta más, porque cada cards.list devuelve hasta 10 cartas a 1 crédito cada una. El mínimo de 3 caracteres y la caché lo mantienen a raya.
¿Puede el bot mostrar el historial de precios o gráficos?
Sí. El endpoint de historial de precios devuelve puntos diarios para 7d, 30d, 90d o 365d. Conviértelos en una imagen y adjúntala al embed en un comando /history, la siguiente función lógica una vez que /price funcione.

Para el endpoint de historial y todas las demás rutas, consulta la referencia completa de la API.

Para terminar

Ya tienes un bot de precios que funciona: el SDK oficial, un comando sincronizado con autocompletado y una respuesta en embed, con gestión de errores tipada y una caché para respetar los límites. El mismo patrón en dos pasos, buscar un ID y luego pedir los datos, sirve también para productos sellados, ventas de eBay y ofertas de Cardmarket.

La referencia completa de la API cubre todos los endpoints, y una clave gratis del panel es todo lo que necesitas para empezar a construir.

PkmnPrices

Escrito por

PkmnPrices

PkmnPrices es una API para desarrolladores con datos del JCC Pokémon: precios diarios de TCGPlayer de más de 80.000 cartas, más de 800 expansiones y productos sellados.