Discord Bot Coding AI: Build One with an LLM API
A Discord bot coding AI answers programming questions inside your server, using a language model behind an API. This tutorial builds one in about 70 lines of Python and covers the parts that usually break.
Updated

Key points
- The bot needs discord.py, the openai package, a bot token and an LLM API key.
- Enable the Message Content intent in the Discord developer portal or the bot will see empty messages.
- Keep a short per-channel history and trim it, since the model remembers nothing between calls.
- Split replies at 2,000 characters and limit parallel calls so you stay under the 8-request concurrency cap.
What the bot will do
The goal is a Discord bot coding AI that reacts when someone mentions it. A user writes "@helper why does my list comprehension return None?", the bot sends the message to the model, and the model's answer comes back as a reply in the same channel. The bot also remembers the last few turns of each channel, so follow-up questions work.
The design has three pieces. discord.py handles the connection to Discord. The OpenAI Python SDK talks to the LLM API, which works here because the API is OpenAI-compatible. A small dictionary holds conversation history. You do not need a database to start.
You will need Python 3.9 or newer, a Discord account that can create an application, and an API key from the API key page. New accounts can claim $0.50 of trial credit with one click, which is enough to build and test this bot.
Step 1: create the Discord application
- Open the Discord developer portal and create a new application.
- In the Bot section, create the bot user and copy its token. Treat it like a password.
- On the same page, switch on Message Content Intent under Privileged Gateway Intents. Without it, the bot receives messages with empty text, and you will spend an hour wondering why it never answers.
- In OAuth2, generate an invite link with the
botscope and the permissions to read messages and send messages. Open the link and add the bot to a test server.
Install the libraries and set two environment variables.
pip install discord.py openai
export DISCORD_TOKEN="your-discord-bot-token"
export LLM_API_KEY="your-llm-api-key"
# quick check that the LLM key works, using model uncensored
curl -s https://api.codingllmapi.com/v1/chat/completions \
-H "Authorization: Bearer $LLM_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"uncensored","messages":[{"role":"user","content":"ping"}],"max_tokens":5}'Step 2: the bot code
Save this as bot.py. It answers when mentioned, keeps twelve messages of history per channel, shows the typing indicator while it waits, and splits long answers.
import asyncio
import os
import discord
from openai import AsyncOpenAI, RateLimitError, APIError
llm = AsyncOpenAI(
base_url="https://api.codingllmapi.com/v1",
api_key=os.environ["LLM_API_KEY"],
)
SYSTEM = (
"You are a helpful programming assistant in a Discord server. "
"Answer briefly. Put code in fenced blocks. Say so when you are unsure."
)
MAX_TURNS = 12
history = {} # channel id -> list of messages
slots = asyncio.Semaphore(6) # stay under the 8 concurrent request cap
intents = discord.Intents.default()
intents.message_content = True
bot = discord.Client(intents=intents)
def split_reply(text, limit=1900):
parts = []
while len(text) > limit:
cut = text.rfind("\n", 0, limit)
if cut < limit // 2:
cut = limit
parts.append(text[:cut])
text = text[cut:].lstrip("\n")
parts.append(text)
return parts
@bot.event
async def on_message(message):
if message.author.bot or bot.user not in message.mentions:
return
question = message.clean_content.replace(f"@{bot.user.display_name}", "").strip()
if not question:
await message.reply("Ask me a coding question after the mention.")
return
log = history.setdefault(message.channel.id, [])
log.append({"role": "user", "content": question})
del log[:-MAX_TURNS]
async with message.channel.typing():
try:
async with slots:
resp = await llm.chat.completions.create(
model="uncensored",
messages=[{"role": "system", "content": SYSTEM}] + log,
temperature=0.4,
max_tokens=900,
)
except RateLimitError:
await message.reply("The model is busy. Try again in a few seconds.")
log.pop()
return
except APIError:
await message.reply("The model call failed. Try again shortly.")
log.pop()
return
answer = resp.choices[0].message.content or "(empty reply)"
log.append({"role": "assistant", "content": answer})
for part in split_reply(answer):
await message.reply(part)
bot.run(os.environ["DISCORD_TOKEN"])
Run it with python bot.py, then mention the bot in your test server. The first answer should arrive within a few seconds.
Why the code is written this way
History is trimmed on purpose
The model has no memory, so each call sends the recent turns. Without del log[:-MAX_TURNS] the list would grow forever. Eventually it would hit the 100,000-token window and your costs would climb with every message. Twelve entries is enough for a conversation about one bug. For a server with long threads, raise it or summarize older turns.
Discord's 2,000-character limit
Discord rejects messages longer than 2,000 characters, and code answers hit that quickly. The split_reply helper cuts at a newline near the limit. It does not know about code fences, so a block can be cut in the middle. If that bothers you, add logic that closes a fence at the end of one part and reopens it at the start of the next.
A semaphore for concurrency
A full key allows 8 concurrent requests, and a trial key allows 2. A busy server can produce several questions at once. The semaphore queues extra calls inside your process instead of sending them and receiving a 429 with the error code concurrency. If you are still on the trial key, change Semaphore(6) to Semaphore(2).
Errors are not charged
Failed or refused requests are not billed, so a retry after an error does not double the cost. The code pops the user turn off the history on failure, so a retry does not leave a dangling question that the model has already seen twice.
Making answers feel better
Once the basic loop works, a few small changes improve it a lot.
- Stream the answer. Pass
stream=Trueand edit the bot's message every second or so with the text so far. Do not edit on every token, because Discord applies its own rate limits to message edits. A streamed request holds its concurrency slot until it ends, so keep the semaphore around the whole stream. - Use slash commands. A command like
/askavoids the mention parsing and does not need the Message Content intent at all, which also makes the bot easier to verify later if it grows past a small number of servers. - Per-user history. Key the history by channel and user if several people ask unrelated questions in one channel.
- Tune the system prompt. Name the languages your community uses, ask for short answers, and tell the model to ask a clarifying question when the problem is vague. Prompt changes are free and often beat model changes.
- Lower the temperature. For code, values between 0.2 and 0.4 give steadier results than the default.
Troubleshooting the usual failures
When a new bot stays silent, the cause is almost always one of the items below. Check them in this order.
- The bot is online but never replies. The Message Content intent is off in the developer portal, or
intents.message_content = Trueis missing from the code. Both must be set. - It replies in some channels only. The bot lacks permission to read or send messages in the others. Check the channel's permission overrides, not only the server-wide role.
- Every reply is a "busy" message. You are probably hitting a 429. Print the exception body once. If the code says
rate_limited, you passed the per-minute cap of 300 requests on a full key or 60 on a trial key. If it saysconcurrency, lower the semaphore size. - An authentication error appears at the first call. The LLM key is missing from the environment of the process that runs the bot. A key exported in one terminal tab is not visible to a service started elsewhere.
- The answer is cut off. The
max_tokens=900in the sample stops long replies. Raise it, or remove it, sincemax_tokensis optional and the completion may use the rest of the window. - The bot answers its own messages. That loop happens if you drop the
message.author.botcheck. Keep it.
For debugging, log the X-Request-Id value for each call. With the OpenAI SDK you can read it from the raw response by calling llm.chat.completions.with_raw_response.create(...) and then reading .headers. If you contact support about a specific failure, that ID lets them find the exact request.
What this costs to run
Pricing is $0.15 per 1M input tokens and $0.50 per 1M output tokens, with no subscription. Take a typical exchange: the system prompt plus twelve history entries make about 2,500 input tokens, and the answer is about 400 tokens.
Input: 2,500 x $0.15 / 1,000,000 = $0.000375. Output: 400 x $0.50 / 1,000,000 = $0.0002. One answer costs about $0.000575. A community that asks 500 questions in a day would spend 500 x $0.000575 = $0.2875, a little under thirty cents. Your numbers will differ with longer code pastes, so log resp.usage for a week and read the real averages. The pricing page shows the top-up options: any whole amount from $20 to $500, with +5% from $50, +10% from $100 and +15% from $200, paid by card or crypto.
To limit abuse, add a per-user cooldown and a cap on question length. Those two checks protect your balance better than anything else you can add.
Hosting and safety notes
The bot is a long-running process. A small virtual machine or a container host is enough, since the heavy work happens on the API side. Run it under a process manager so it restarts after a crash, and keep both tokens in environment variables or a secrets store, never in the repository.
Think about what your server allows. The model is tuned to answer without content refusals for lawful adult use, so it will discuss security testing or edgy fiction that a stricter assistant might refuse. If your community includes younger members, set rules in the system prompt, restrict the bot to specific channels, and remember that Discord's own terms apply to your bot too. Content sexualizing minors is forbidden on this API without exception.
Full parameter details, such as stop, seed and tool calling, are in the API docs. If you want the bot to run code or read repositories, tool calling is the route, and you decide which functions the model may request. For programming-focused use of this model, see the page about the uncensored AI model for coding.
More guides
Questions and answers
Do I need the Message Content intent?
Yes, if the bot reads normal messages and mentions. If you use slash commands only, you do not need it.
Can the bot stream replies?
Yes. Set stream=True and edit the Discord message at a calm interval, such as once per second, so you stay within Discord's own limits.
What happens if too many people ask at once?
Past 8 concurrent requests on a full key, the API returns HTTP 429 with the code concurrency. A semaphore in your code queues the extra questions instead.
Will the bot remember conversations after a restart?
Not with the dictionary used here. Store history in SQLite or Redis if you need it to survive restarts.
Is the API key safe in a Discord bot?
Yes if it stays on your server in an environment variable. Never post it in a channel or commit it to a public repository.
Your key is one form away
Create an account, copy the key, change the base URL. That is the whole setup.
Get API key