Back to Blog
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

  1. Go to discord.com/developers/applications
  2. Click New Application → give it a name
  3. Go to Bot → click Add Bot
  4. Under Token, click Reset Token and copy the token
  5. Enable Message Content Intent under Privileged Gateway Intents
  6. Go to OAuth2 → URL Generator: select bot + applications.commands scopes, and Send Messages + Read Message History permissions
  7. 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