TutorialsJune 23, 20259 min read
How to Build a Discord AI Bot with a Free LLM API (Python + discord.py)
Build a fully working Discord AI bot with conversation memory, slash commands, and model switching — using a free LLM API key and discord.py.
What You Will Build
A Discord bot that:
- Responds when mentioned or in a specific channel
- Maintains per-channel conversation context (remembers the thread)
- Supports slash commands:
/ask,/reset,/model,/summarize - Works with GPT-4o, Claude, DeepSeek, and Gemini — switchable per server
Total cost: $0. You need a Discord developer account (free), Python 3.10+, and a FreeLLMKeys API key.
Step 1 — Create a Discord Application
- Go to discord.com/developers/applications
- Click New Application → give it a name
- Go to Bot → click Add Bot
- Under Token, click Reset Token and copy the token
- Enable Message Content Intent under Privileged Gateway Intents
- Go to OAuth2 → URL Generator: select
bot+applications.commandsscopes, andSend Messages+Read Message Historypermissions - Copy the generated URL and open it in your browser to add the bot to your server
Step 2 — Install Dependencies
pip install "discord.py[voice]" openai
Step 3 — Write the Bot
import discord
from discord import app_commands
from openai import OpenAI
from collections import defaultdict
DISCORD_TOKEN = "your-discord-bot-token"
LLM_API_KEY = "sk-your-freellmkeys-key"
BASE_URL = "https://aiapiv2.pekpik.com/v1"
client = OpenAI(base_url=BASE_URL, api_key=LLM_API_KEY)
# Per-channel conversation history and model
channel_history: dict[int, list] = defaultdict(list)
channel_model: dict[int, str] = defaultdict(lambda: "gpt-4o")
SYSTEM_PROMPT = "You are a helpful AI assistant in a Discord server. Be concise and friendly. Format responses for Discord (use **bold** and bullet points where appropriate)."
# ── Discord setup ─────────────────────────────────────────
intents = discord.Intents.default()
intents.message_content = True
bot = discord.Client(intents=intents)
tree = app_commands.CommandTree(bot)
@bot.event
async def on_ready():
await tree.sync()
print(f"Bot ready as {bot.user}")
# ── Slash commands ────────────────────────────────────────
@tree.command(name="ask", description="Ask the AI a question")
async def ask_cmd(interaction: discord.Interaction, question: str):
await interaction.response.defer()
cid = interaction.channel_id
history = channel_history[cid]
model = channel_model[cid]
history.append({"role": "user", "content": question})
if len(history) > 20:
history[:] = history[-20:]
try:
response = client.chat.completions.create(
model=model,
messages=[{"role": "system", "content": SYSTEM_PROMPT}] + history,
max_tokens=1024
)
reply = response.choices[0].message.content
history.append({"role": "assistant", "content": reply})
# Discord messages max 2000 chars — split if needed
if len(reply) > 1990:
chunks = [reply[i:i+1990] for i in range(0, len(reply), 1990)]
await interaction.followup.send(chunks[0])
for chunk in chunks[1:]:
await interaction.channel.send(chunk)
else:
await interaction.followup.send(reply)
except Exception as e:
await interaction.followup.send(f"⚠️ Error: {e}")
@tree.command(name="reset", description="Clear conversation history for this channel")
async def reset_cmd(interaction: discord.Interaction):
channel_history[interaction.channel_id].clear()
await interaction.response.send_message("✅ Conversation history cleared.")
@tree.command(name="model", description="Switch AI model for this channel")
@app_commands.describe(model_name="The model to use")
@app_commands.choices(model_name=[
app_commands.Choice(name="GPT-4o", value="gpt-4o"),
app_commands.Choice(name="Claude Opus 4", value="claude-opus-4-7"),
app_commands.Choice(name="DeepSeek V3", value="deepseek-chat"),
app_commands.Choice(name="Gemini 2.5 Flash", value="gemini-2.5-flash"),
app_commands.Choice(name="Grok-4", value="grok-4"),
])
async def model_cmd(interaction: discord.Interaction, model_name: app_commands.Choice[str]):
channel_model[interaction.channel_id] = model_name.value
await interaction.response.send_message(f"✅ Switched to **{model_name.name}** for this channel.")
@tree.command(name="summarize", description="Summarize the recent conversation")
async def summarize_cmd(interaction: discord.Interaction):
await interaction.response.defer()
history = channel_history[interaction.channel_id]
if not history:
await interaction.followup.send("No conversation to summarize yet.")
return
conv_text = "\n".join(f"{m['role'].upper()}: {m['content']}" for m in history)
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": f"Summarize this conversation in 3 bullet points:\n{conv_text}"}],
max_tokens=300
)
await interaction.followup.send(f"📋 **Summary:**\n{response.choices[0].message.content}")
# ── Reply when mentioned ──────────────────────────────────
@bot.event
async def on_message(message: discord.Message):
if message.author.bot:
return
if bot.user not in message.mentions:
return
text = message.content.replace(f"<@{bot.user.id}>", "").strip()
if not text:
await message.reply("Yes? Ask me anything or use /ask, /model, /reset, /summarize")
return
cid = message.channel.id
history = channel_history[cid]
model = channel_model[cid]
history.append({"role": "user", "content": text})
async with message.channel.typing():
try:
response = client.chat.completions.create(
model=model,
messages=[{"role": "system", "content": SYSTEM_PROMPT}] + history[-20:],
max_tokens=1024
)
reply = response.choices[0].message.content
history.append({"role": "assistant", "content": reply})
await message.reply(reply[:1990])
except Exception as e:
await message.reply(f"⚠️ {e}")
bot.run(DISCORD_TOKEN)
Step 4 — Run It
python discord_bot.py
Your bot will appear online in your Discord server. Test it with /ask or by mentioning it: @YourBot what is machine learning?
Keeping It Running 24/7
Deploy for free on Railway or Render (same steps as the Telegram bot guide). Add DISCORD_TOKEN and LLM_API_KEY as environment variables, and your Discord AI bot runs continuously at zero cost.
F
FreeLLMKeys Team
Building tools for the AI developer community