A Discord bot on Claude or GPT: a minimal example
Twenty lines of code and one API key, and Claude or GPT is answering in a Discord channel. A working example plus the errors that show up under load.
4 min · · Updated · development
A Discord bot on your own key is really just one event handler and one HTTP call. The complexity isn't in the bot's code — it's that Discord expects a fast reply while a model can take seconds to think, and that a busy channel easily bumps into your own key's spend limit (the gateway itself has no general rate limit).
A minimal discord.js bot
// bot.js
import { Client, GatewayIntentBits, Events } from "discord.js";
const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent]
});
client.on(Events.MessageCreate, async (message) => {
if (message.author.bot || !message.content.startsWith("!ask ")) return;
const question = message.content.slice(5);
await message.channel.sendTyping();
try {Note message.channel.sendTyping() — the "is typing" indicator keeps the channel feeling alive while the model puts together an answer, which matters more on long answers or when the model reasons before replying (extended thinking), since the first token doesn't arrive instantly.
The Anthropic schema, if you specifically want Claude
Discord.js doesn't care about the request body's shape — calling /v1/messages is just as easy:
const r = await fetch("https://api.zukko.pro/v1/messages", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.ZUKKO_KEY}`,
"Content-Type": "application/json",
"anthropic-version": "2023-06-01"
},
body: JSON.stringify({
model: "claude-sonnet-5",
max_tokens: 1024,
messages: [{ role: "user", content: question }]
})
});
const data = await r.json();
const reply = data.content[0].text;The only difference is the body's shape and that the reply lives at content[0].text instead of choices[0].message.content.
Your key's own limit is the real problem on a busy server
On a large Discord server, a burst of !ask messages within a minute is routine, and it's easy to hit your own key's limit rather than any model-wide one: a 429 with key_daily_limit_reached or key_monthly_limit_reached means the daily (resets at 00:00 UTC) or monthly spend cap set on the key's dashboard page has been used up. The gateway has no rate limit of its own — other 429s come from the model's provider. Worth catching the key limit separately and showing a bot-specific message instead of "service unavailable":
if (r.status === 429) {
const err = await r.json();
if (err.error?.code === "key_daily_limit_reached") {
return message.reply("The bot hit its daily request limit, try again tomorrow.");
}
return message.reply("The model is temporarily overloaded at the provider, give it a moment.");
}402 means the balance ran out, not that the bot broke
{"error": {"message": "Insufficient balance", "code": "insufficient_balance"}}The balance check happens before the request ever reaches a model, so a failed attempt costs nothing. For a bot, that means it's safe to show users an honest balance message instead of a generic error — it's not a code bug, it's a signal to top up at https://zukko.pro/app.
Don't repeat identical requests — use the cache
If several people in a channel ask the same frequent question (server rules, FAQ), it's worth turning on response caching with the X-Zukko-Cache: true header (temperature: 0 works too, but only for models that accept it — not Claude). Requests from the same user with an exactly matching body get cached for up to 7 days for chat, the repeat answer comes back instantly and free, and the response carries an X-Zukko-Cache: hit header — worth logging so you can see how many replies the bot isn't even paying for.
An Idempotency-Key for duplicate events
Discord occasionally delivers the same event twice because of network delays on its side. If the bot reacts to events rather than only direct user calls, it's worth adding an Idempotency-Key based on the message id — that way an accidental duplicate event doesn't get billed twice:
headers: {
"Authorization": `Bearer ${process.env.ZUKKO_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": `discord-${message.id}`
}Different models per server on the same bot
If one bot runs across several servers, it's better not to hardcode the model but keep it per-server in config and pull the current list of valid ids without maintaining your own cache:
const models = await fetch("https://api.zukko.pro/v1/models").then(r => r.json());The endpoint is public and needs no key, so it's fine to call it on bot startup and validate each server's config — if an admin typed in a stale or wrong model id, the bot catches it before the first user message instead of returning model_not_found mid-conversation.
Restricting access by role and by key at the same time
The key-level limit (daily and monthly dollar spend, allowed-model list) is set on the key's dashboard page and protects against overspend across the whole bot. Inside the bot itself, it's worth additionally restricting the !ask command by Discord roles — that doesn't replace the key limit, it works alongside it: the key limit is a ceiling on money spent, roles decide who can even reach it. If one bot runs across several servers on a single key, it's reasonable to issue a separate key per large server, so one community's burst of activity doesn't lock out the rest.
If the bot doesn't respond at all
Before digging through the code, it's worth testing the same request the bot makes, but directly via curl with the same key. If that goes through, the bug is in the bot itself (wrong intent, a truncated token from .env, a wrong path); if it doesn't, the server's response names the cause: bad key, empty balance, or a limit hit. Also worth checking the public GET https://api.zukko.pro/api/status separately — it returns JSON with model status; if the model you need is down, the problem has nothing to do with the bot.
zukko
Pay 25×
less
for Claude Code & ChatGPT
One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.
Related articles
- Prompt Injection: What It Is and How to Protect a Bot and an AgentHow prompt injection works through user input and third-party documents, and which measures limit the damage: data isolation, tool permissions, key limits.
- Your Own Neural Network Proxy on Node.js: Keep the API Key Off the FrontendA small Express server between your frontend and the model API: the key stays on the server, and models, request size and rate are capped.
- DeepSeek API in Python: Requests, Streaming, JSON and Error HandlingReady code for DeepSeek V4.1 Flash and V4 Pro on the OpenAI SDK: a request, streaming, JSON parsing and careful error handling.
- Prompt Engineering Examples: 12 Ready-Made Templates for WorkTwelve prompt templates for everyday tasks: what each one says, why it is built that way, and what a single API call costs.