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
pkmnpriceste 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:
DISCORD_TOKEN=your-bot-tokenPKMNPRICES_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.
mkdir price-bot && cd price-botpython -m venv .venv && source .venv/bin/activatepip 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.
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() 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.
# 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)
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.
@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}")
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.
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.
@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() 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.
@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:
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:
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:
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
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.

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.