PkmnPrices9 Min. Lesezeit

Einen Discord-Bot für Pokémon-Preise bauen

Bau einen Bot mit /price-Slash-Befehl in rund 50 Zeilen Python mit dem offiziellen pkmnprices-SDK: ein asynchroner Client, typisierte Karten und aktuelle Marktpreise in discord.py v2.

Auf deinem Tausch-Discord fällt den ganzen Tag dieselbe Frage: „Was ist die Karte wert?“ In dieser Anleitung baust du einen Bot, der sie beantwortet. Tipp /price charizard, und du bekommst den aktuellen Marktpreis als übersichtliches Embed zurück, abgerufen von der PkmnPrices API über ihr offizielles Python-SDK. Das sind ungefähr 50 Zeilen Python, und am Ende läuft der Bot bei dir lokal.

Was du hier lernst

  • Das Muster „erst suchen, dann abrufen“, das das SDK nutzt (Listenergebnisse enthalten keine Preise)
  • Wie du in discord.py v2 einen Slash-Befehl mit Autovervollständigung registrierst
  • Wie dir das pkmnprices-SDK einen asynchronen Client, typisierte Modelle und Wiederholungsversuche ohne Mehraufwand liefert

Was du baust

Einen einzigen Slash-Befehl. Jemand tippt /price <card name>, und der Bot antwortet mit dem Set der Karte, dem Zustand und dem aktuellen Marktpreis in USD. Keine Datenbank, kein Scraping, kein handgeschriebenes HTTP, nur das SDK und ein Discord-Embed.

Intern sind es zwei Schritte, weil die API Suche und Preise trennt. client.cards.list() findet die Karte und gibt ihre ID zurück. client.cards.get() liefert genau diese Karte mit ihren Preisen. Das sagen wir lieber deutlich, weil viele darüber stolpern: Der Listenaufruf liefert nie Preisdaten. Für eine Zahl rufst du immer die einzelne Karte ab. Und weil ein Name wie charizard auf Dutzende Drucke in verschiedenen Sets und Varianten passt, lassen wir die Person die genaue Karte auswählen, bevor wir ihren Preis abrufen.

Bevor du loslegst

Du brauchst drei Dinge. Erstens Python 3.10 oder neuer. Zweitens eine Discord-Anwendung mit Bot-Token aus dem Discord Developer Portal. Drittens einen PkmnPrices-API-Schlüssel, den du kostenlos im Dashboard bekommst.

Die beiden Geheimnisse gehören nicht in deinen Code, sondern in eine .env-Datei:

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

Das Projekt einrichten

Leg einen Ordner an, erstell eine virtuelle Umgebung und installier die Abhängigkeiten. Das pkmnprices-SDK bringt seinen eigenen HTTP-Client, Wiederholungsversuche und typisierte Modelle mit, du musst also keinen API-Unterbau mehr schreiben.

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

Das SDK kennenlernen

Das SDK übernimmt HTTP, Wiederholungsversuche und Typisierung, du schreibst also keine eigene Client-Klasse. discord.py ist asynchron, und das SDK hat genau dafür einen asynchronen Client: AsyncPkmnPrices. Ein Aufruf sucht, einer ruft ab.

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() gibt eine Seite mit .data und .pagination zurück. Jede Karte ist ein typisiertes Objekt, du greifst also auf card.name und card.set.name zu statt auf Dictionary-Schlüssel. get() ergänzt die Liste prices, in der jeder Eintrag condition, variant und market_price hat. Diese Trennung ist der einzige Grund für zwei Aufrufe.

Den Bot bauen

Wir bauen bot.py in vier kleinen Teilen: das Grundgerüst, den Start, eine Hilfsfunktion für das Embed und den Befehl selbst. Los geht's mit dem Grundgerüst: die Imports, ein Bot mit den Standard-Intents, sonst noch nichts.

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)

Slash-Befehle brauchen den Message-Content-Intent nicht, die Standardwerte reichen also. command_prefix verlangt commands.Bot zwar, hier wird es aber nicht genutzt: Wir setzen komplett auf Slash-Befehle.

Den Befehl beim Start registrieren

Beim Hochfahren des Bots muss zweierlei einmal passieren: ein SDK-Client entstehen, den er für jede Anfrage wiederverwendet, und Discord erfahren, dass es den Befehl gibt. Beides gehört in setup_hook, das discord.py ausführt, bevor sich der Bot verbindet.

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() sorgt dafür, dass /price auftaucht. Globale Syncs brauchen ein paar Minuten, bis sie überall ankommen. Während der Entwicklung synchronisierst du deshalb mit bot.tree.sync(guild=discord.Object(id=YOUR_GUILD_ID)) auf einen einzelnen Server, dann sind Änderungen sofort da.

Aus einer Karte ein Embed machen

Halt die Darstellung aus dem Befehls-Handler heraus. Diese Hilfsfunktion nimmt eine Karte und gibt ein Discord-Embed zurück, mit dem Markengrün am Rand. Weil das SDK typisierte Modelle liefert, ist alles reiner Attributzugriff. Sie nimmt bevorzugt den Preis für Near Mint und sonst den ersten vorhandenen.

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

Den /price-Befehl definieren

Der Haken bei einer Namenssuche: charizard passt auf Dutzende Karten, eine pro Set, Reprint und Variante. Das erste Ergebnis zu nehmen, würde eine beliebige davon liefern. Die Lösung ist die Autovervollständigung von Discord (nächster Schritt): Beim Tippen wählt die Person einen echten Treffer aus, und hinter ihrer Auswahl steht die ID der Karte. Der Befehl bekommt also eine ID und ruft sie nur noch ab, mit einem Fallback für freien Text, bei dem nichts ausgewählt wurde.

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() läuft zuerst: Zwei Roundtrips können Discords Antwortfenster von drei Sekunden sprengen, und mit dem Aufschieben verschaffst du dir bis zu fünfzehn Minuten. (handle_error kommt im nächsten Abschnitt.)

Die richtige Karte auswählen lassen

Der Autocomplete-Callback feuert beim Tippen und gibt bis zu 25 Vorschläge zurück, das Maximum bei Discord. Wenn du jeden Kartennamen mit seinem Set kombinierst, wird aus einem vagen „Charizard“ eine genaue Liste: „Charizard · Base Set“, „Charizard · Base Set 2“ und so weiter. Hinter jedem Vorschlag steht die ID, die der Befehl bekommt.

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
]

Die Bedingung len(current) < 3 überspringt die Suche, bis sich die Anfrage lohnt. Das zählt, weil der Callback bei jedem Tastendruck feuert. Nutz außerdem den Cache aus dem nächsten Abschnitt, damit wiederholte Anfänge nicht jedes Mal die API treffen.

Zum Schluss die Zeile, die alles startet. Sie kommt ans Ende der Datei:

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

Fehler und Rate Limits abfangen

Ein Bot, der bei der ersten falschen Eingabe abstürzt, hält auf einem belebten Server keinen Tag durch. Das SDK wirft typisierte Exceptions, du behandelst also genau die Fälle, die dir wichtig sind. Die beiden, die sich zu fangen lohnen, betreffen Authentifizierung und Limits:

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

Das SDK wiederholt vorübergehende Fehler bereits mit Backoff (einstellbar mit AsyncPkmnPrices(key, max_retries=2)). Wenn also ein RateLimitError bei deinem Handler ankommt, ist das Limit wirklich erreicht. Der kostenlose Tarif erlaubt 60 Anfragen pro Minute und 100 Credits pro Tag, und Listen-Endpoints berechnen 1 Credit pro zurückgegebenem Eintrag (Dokumentation zum Rate Limiting). Bei der Autovervollständigung verstecken sich die Kosten: Jede Vorschlagssuche liefert bis zu zehn Karten und feuert beim Tippen. Eine einzelne Abfrage kann so für die Vorschläge mehr verbrauchen als für den Preis selbst.

Zwei Schutzmaßnahmen halten das bezahlbar. Die Bedingung len(current) < 3 streicht die kürzesten Anfragen. Die andere ist ein Cache, damit wiederholte Anfänge eine Antwort wiederverwenden, statt erneut dafür zu zahlen. Preise werden täglich aktualisiert, ein kurzer Cache im Speicher kostet dich also nichts an Genauigkeit:

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

Ruf im Autocomplete-Callback search_cached(bot.api, current) statt bot.api.cards.list auf, und wer sich durch „char“, „chari“, „chariz“ tippt, zahlt für eine Suche statt für drei.

Den Bot online bringen

Lokal fehlt nur noch ein Befehl: python bot.py. Lade den Bot über den OAuth2-URL-Generator im Developer Portal mit dem Scope applications.commands auf einen Server ein, und /price erscheint in der Befehlsliste.

Für dauerhaften Betrieb eignet sich alles, was Python ausführt: ein kleiner VPS, eine Container-Plattform oder ein Hoster für Hobbyprojekte. Die einzige feste Regel ist die vom Anfang: Token und API-Schlüssel liegen in Umgebungsvariablen, nie im Repo. Sollte der API-Schlüssel jemals öffentlich werden, tausch ihn im Dashboard aus.

Häufige Fragen

Warum das SDK statt direkter API-Aufrufe?
Das pkmnprices-SDK bringt einen asynchronen Client (AsyncPkmnPrices) mit, der zu discord.py passt, dazu typisierte Modelle, eingebaute Wiederholungsversuche mit Backoff und typisierte Exceptions wie UnauthorizedError und RateLimitError. Du schreibst Bot-Logik statt HTTP-Klempnerei.
Funktioniert das mit dem kostenlosen Tarif?
Ja, mit zwei Grenzen, die du kennen solltest. Der kostenlose Tarif liefert nur englische Karten und erlaubt 100 Credits pro Tag bei 60 Anfragen pro Minute. Ein /price-Befehl mit Cache passt für einen kleinen bis mittleren Server bequem hinein; bei viel Verkehr ist es Zeit für einen größeren Tarif.
Wie viele Credits kostet ein /price?
Die Abfrage selbst ist günstig: 1 Credit, wenn die Person einen Vorschlag auswählt (nur der Abruf der einzelnen Karte), oder 2 beim Fallback mit freiem Text. Die Autovervollständigung kostet mehr, weil jedes cards.list bis zu 10 Karten zu je 1 Credit liefert. Die Grenze von 3 Zeichen und der Cache halten das im Rahmen.
Kann der Bot Preisverlauf oder Diagramme anzeigen?
Ja. Der Endpoint für den Preisverlauf liefert tägliche Werte über 7, 30, 90 oder 365 Tage. Mach daraus ein Bild und häng es für einen /history-Befehl an das Embed an, ein naheliegendes nächstes Feature, sobald /price läuft.

Den Endpoint für den Preisverlauf und alle anderen Routen findest du in der vollständigen API-Referenz.

Fazit

Das ist ein funktionierender Preis-Bot: das offizielle SDK, ein synchronisierter Befehl mit Autovervollständigung und eine Antwort als Embed, mit typisierter Fehlerbehandlung und einem Cache, der die Limits respektiert. Dasselbe Muster aus zwei Schritten, erst nach einer ID suchen, dann die Daten abrufen, funktioniert auch für Sealed-Produkte, eBay-Verkäufe und Cardmarket-Angebote.

Die vollständige API-Referenz deckt jeden Endpoint ab, und ein kostenloser Schlüssel aus dem Dashboard ist alles, was du zum Loslegen brauchst.

PkmnPrices

Geschrieben von

PkmnPrices

PkmnPrices ist eine Entwickler-API für Daten zum Pokémon TCG: tägliche TCGPlayer-Preise für über 80.000 Karten, über 800 Sets und Sealed-Produkte.