Open Source Discord Bot With an Uncensored LLM
An open source discord bot that talks through an LLM is about eighty lines of Python. This guide gives you the code, the limits you must respect, and the cost per reply, using an OpenAI-compatible API with a model that does not refuse lawful adult requests.

Key points
- discord.py plus the OpenAI SDK is enough; point the SDK at https://api.openrouternsfw.com/v1 and use the model uncensored.
- Turn on the Message Content intent in the Discord developer portal or the bot sees empty messages.
- Use a semaphore below 8 to stay inside the concurrency limit, and keep adult content in channels marked age-restricted.
- A reply with 1,500 input and 300 output tokens costs $0.000375, so about $3.75 per 10,000 replies.
No card needed. After sign-up, claim $0.50 of free test credit with one click (once per person).
Get API keyWhat you are building
The bot below answers when someone mentions it. It keeps a short history per channel, sends it to the chat completions endpoint, and posts the reply. It is plain Python, MIT-friendly, and has no database. You can read all of it in one sitting, which is the point of an open source project: someone else can fork it and understand it.
The model behind it is a single open-weight text model, run by an independent provider and tuned to answer without content refusals for lawful adult use. That suits Discord communities built around fiction, roleplay or adult conversation. Two rules apply from the start. First, sexual content involving minors is not allowed by the API and must not be part of your bot's design. Second, Discord has its own rules and age-restricted channel settings, and you are responsible for following them. Keep adult content to channels that are marked as such and to servers where members are adults.
Set up Discord and your keys
- Open the Discord developer portal and create an application, then add a bot to it.
- Under the bot settings, enable the Message Content Intent. Without it, the bot receives messages with empty text.
- Copy the bot token. Treat it like a password.
- Use the OAuth2 URL generator to invite the bot with permission to read and send messages in the channels you choose.
- Get an API key from the key page. New users can claim $0.50 of trial credit with one click, no payment details needed, valid for 7 days.
Store both secrets as environment variables, never in the repository. For an open source project this matters more than usual, because the code is public.
pip install discord.py openai python-dotenv
printf 'DISCORD_TOKEN=your-discord-token
API_KEY=your-api-key
' > .env
echo ".env" >> .gitignore
The bot code
Save this as bot.py. The comments explain the decisions that are not obvious.
import asyncio
import os
from collections import defaultdict, deque
import discord
from dotenv import load_dotenv
from openai import AsyncOpenAI
load_dotenv()
ai = AsyncOpenAI(
base_url="https://api.openrouternsfw.com/v1",
api_key=os.environ["API_KEY"],
max_retries=4, # the SDK retries 429 and honors Retry-After
)
SYSTEM = (
"You are Vex, a sharp-tongued storyteller in a Discord server for adults. "
"Stay in character, keep replies under 150 words, and never write "
"anything sexual involving minors."
)
intents = discord.Intents.default()
intents.message_content = True
bot = discord.Client(intents=intents)
slots = asyncio.Semaphore(6) # stay under 8 concurrent
history = defaultdict(lambda: deque(maxlen=12)) # last 12 turns per channel
async def ask(channel_id, user_text):
turns = history[channel_id]
turns.append({"role": "user", "content": user_text})
messages = [{"role": "system", "content": SYSTEM}] + list(turns)
async with slots:
resp = await ai.chat.completions.create(
model="uncensored",
messages=messages,
temperature=0.9,
max_tokens=400,
)
reply = resp.choices[0].message.content or ""
turns.append({"role": "assistant", "content": reply})
return reply
@bot.event
async def on_ready():
print(f"Logged in as {bot.user}")
@bot.event
async def on_message(message):
if message.author.bot or bot.user not in message.mentions:
return
text = message.clean_content.replace(f"@{bot.user.display_name}", "").strip()
if not text:
return
async with message.channel.typing():
try:
reply = await ask(message.channel.id, f"{message.author.display_name}: {text}")
except Exception as exc:
print("API error:", exc)
await message.reply("The model is busy. Try again in a few seconds.")
return
for i in range(0, len(reply), 1900): # Discord caps messages at 2000 chars
await message.reply(reply[i:i + 1900])
bot.run(os.environ["DISCORD_TOKEN"])
Run it with python bot.py, then mention the bot in a channel it can read. The first reply should arrive in a few seconds.
If nothing happens, check three things in order. The bot must be online in the member list, so the token is valid. It must have permission to read and send messages in that channel. And the mention must be a real Discord mention, picked from the autocomplete list, because plain text with the bot name does not appear in message.mentions. The terminal prints API errors, so a wrong key shows up there as an authentication failure.
Why the code is shaped this way
History is small on purpose
The context window is 100,000 tokens, so you could send a lot. But every input token is billed on every call, and Discord chat is mostly short lines. Twelve turns keep replies coherent without turning each message into a large bill. Raise maxlen for story channels where continuity matters.
The semaphore protects your key
A full key allows 8 concurrent requests and 300 requests per minute. A trial key allows only 2 and 60. If ten people mention the bot at once and you fire ten requests, the extra ones get HTTP 429 with the concurrency error code. The semaphore queues them in your own process instead. Set it to 2 while you are on the trial key. If you ever turn on streaming, remember that a streamed request holds its slot until the stream finishes.
Retries come from the SDK
The OpenAI Python SDK retries on 429 and respects the Retry-After header, which the service sends as Retry-After: 5 on temporary overload. Setting max_retries to 4 covers short spikes. Failed or refused requests are not charged, so retries do not cost you tokens when they fail.
One shared persona
The system prompt sits first on every call. Per-channel personas are a small extension: keep a dictionary from channel ID to prompt and look it up in ask.
Make it a good open source project
The code works, but a public repository needs a few extras before strangers can use it.
- A license. MIT or Apache 2.0 are the usual choices for small bots. Add the file at the repository root.
- A .env.example. List
DISCORD_TOKENandAPI_KEYwith fake values so people know what to set. - A configurable base URL. Read
BASE_URLandMODELfrom the environment, defaulting to the values above. Anyone who forks it can then use any OpenAI-compatible server. - A Dockerfile. A short file based on a Python slim image, copying
bot.py, installing the three packages and running the script, lets people host it anywhere. - A README with the intents warning. The missing Message Content intent is the top support question for every Discord bot.
Keep your own token and key out of Git history. If one leaks, revoke it in the Discord portal or on the API key page and issue a new one.
Hosting and what it costs
The bot is a single long-running process with light CPU use. A small VPS, a spare machine or a container platform is enough. There is no web server to expose, because the bot connects out to Discord.
The API cost is easy to compute. A typical reply here uses 1,500 input tokens (system prompt plus twelve short turns) and 300 output tokens.
- Input: 1,500 x $0.15 / 1M = $0.000225
- Output: 300 x $0.50 / 1M = $0.00015
- Per reply: $0.000375
That makes 1,000 replies $0.375 and 10,000 replies $3.75. The $0.50 trial covers about 1,333 replies. A $20 top-up covers more than 53,000. Top-ups run from $20 to $500, with +5% from $50, +10% from $100 and +15% from $200. Credit never expires and there is no subscription, so a quiet server costs almost nothing. See the pricing page for the details. Payment is crypto only, using USDT on TRC20 or USDC on Base.
Where to go next
Three upgrades are worth the effort. Add streaming by editing a single message as chunks arrive, but mind Discord's edit rate limits. Add a slash command that resets the channel history. Add tool calling so the bot can roll dice or look up a character sheet; the API supports tools, tool_choice and role: "tool" results in the OpenAI format.
The API docs list every supported parameter, and the Hermes API article covers how uncensored models behave inside a project. Prompts sent to the API are not used for training, which is worth saying in your server's rules if your members ask.
More guides
Questions and answers
Does the bot need the Message Content intent?
Yes. Enable it in the Discord developer portal and in the code with intents.message_content set to true. Otherwise the bot sees empty messages.
How many replies can the bot handle at once?
A full key allows 8 concurrent requests and 300 per minute; a trial key allows 2 and 60. A semaphore in the bot keeps you below the limit.
Can I publish the bot's code?
Yes, as long as the Discord token and API key stay out of the repository. Read them from environment variables and ship a .env.example.
Is it allowed to use adult themes?
Only for lawful adult use, in line with Discord's rules and its age-restricted channel settings. Sexual content involving minors is never allowed.
What does it cost to run?
A reply with 1,500 input and 300 output tokens costs $0.000375, so about $3.75 per 10,000 replies, plus the cost of hosting the process.
Your key is one form away
Create an account, copy the key, change the base URL. That is the whole setup.
Get API key