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

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.