Build an uncensored Discord bot in one Python file

An uncensored Discord bot you run yourself: one discord.py file, the OpenAI SDK on this API, three env vars, and replies only in age-restricted channels.

Steps checked October 25, 2026 · prices as of October 27, 2026

This uncensored Discord bot is one Python file you run yourself. It answers @mentions with a model from this API through the OpenAI SDK, reads three environment variables, and sends prompts to the model only from age-restricted channels, which is where Discord's rules put adult content. The code was checked against the discord.py and OpenAI SDK docs on 25 October 2026.

If builds with the refusals edited out are new to you, start with AI with no restrictions, then come back here to put one in a channel.

Why discord.py for this bot

discord.py and the OpenAI Python SDK both run on asyncio. The SDK's AsyncOpenAI client waits for the model on the same event loop as the bot, so one slow reply does not hold up the next message. The whole bot is one file and two pip packages, with no build step.

discord.js with the OpenAI Node SDK can do the same job. Python wins here on length: the file below is short enough to read in one pass. Versions checked on 25 October 2026: discord.py 2.7.1 and openai 3.37.0 on PyPI. The openai package needs Python 3.10 or newer.

The three environment variables

DISCORD_TOKEN
your-bot-token
From the Bot page of your Discord app
ULLM_API_KEY
sk-ullm-live-…
Created in the console, shown once
ULLM_MODEL
glm-5.3-flash-uncensored
Optional; any id from the models page

The base URL, https://api.uncensoredllm.ai/v1, is written into the file. Create the key in the console; it starts with sk-ullm- and spends a prepaid balance that starts at $10.

Create the Discord app and its token

The labels below are from Discord's getting started guide, read on 25 October 2026.

  1. In the Developer Portal, create an app: enter a name and press Create.
  2. On the Bot page, under Token, press Reset Token and copy it into DISCORD_TOKEN. Discord shows it once.
  3. On the Installation page, choose the Discord Provided Link. Under Default Install Settings, add the bot scope for Guild Install.
  4. In the permissions menu that appears, pick Send Messages and Read Message History. Discord's message docs require Read Message History for a reply to another message, and the bot replies to the message that mentioned it.
  5. Open the install link, choose Add to server, and make sure the bot can see the age-restricted channel.

No privileged intent is needed. The bot reads only messages that mention it, and Discord's gateway docs list "content in which the app is mentioned" as an exception to the Message Content intent.

The uncensored Discord bot file

Save this as bot.py. Not as discord.py: the discord.py quickstart warns that the name conflicts with the library.

bot.py
1# bot.py: answers @mentions in age-restricted channels with an uncensored model.
2import os
3import re
4
5import discord
6from openai import APIStatusError, AsyncOpenAI
7
8MODEL = os.environ.get("ULLM_MODEL", "glm-5.3-flash-uncensored")
9SYSTEM = (
10 "You are a blunt, helpful member of this Discord server. "
11 "Answer in plain text, under 1,500 characters."
12)
13
14llm = AsyncOpenAI(base_url="https://api.uncensoredllm.ai/v1", api_key=os.environ["ULLM_API_KEY"])
15client = discord.Client(intents=discord.Intents.default())
16
17
18def age_restricted(channel) -> bool:
19 # Guild text channels and threads have is_nsfw(); DMs do not, so the bot stays quiet there.
20 is_nsfw = getattr(channel, "is_nsfw", None)
21 return bool(is_nsfw and is_nsfw())
22
23
24@client.event
25async def on_ready():
26 print(f"Logged in as {client.user}")
27
28
29@client.event
30async def on_message(message: discord.Message):
31 if message.author.bot or client.user not in message.mentions:
32 return
33 if not age_restricted(message.channel):
34 await message.reply("I only answer in age-restricted channels.")
35 return
36 prompt = re.sub(rf"<@!?{client.user.id}>", "", message.content).strip()
37 if not prompt:
38 return
39 try:
40 async with message.channel.typing():
41 reply = await llm.chat.completions.create(
42 model=MODEL,
43 messages=[
44 {"role": "system", "content": SYSTEM},
45 {"role": "user", "content": prompt},
46 ],
47 max_tokens=500,
48 )
49 except APIStatusError as err:
50 await message.reply(f"The model API answered {err.status_code}.")
51 return
52 text = reply.choices[0].message.content or "(empty reply)"
53 await message.reply(text[:2000])
54
55
56client.run(os.environ["DISCORD_TOKEN"])

It answers an @mention with one request to the API and replies in the same channel. A reply is cut at 2,000 characters, the limit Discord's message docs give for message content. Each mention is a fresh request with no history, which keeps every call small.

Discord's age-restricted channel rule

Discord's Community Guidelines (effective 29 September 2025, read on 25 October 2026) cover this in rule 9: "Do not make sexually explicit content available to anyone under the age of 18." The same rule says users must be 18 or older to view adult sexual content, and server owners must apply an age-restricted label to channels that contain it.

An uncensored model can write what a filtered one will not, so the bot checks the channel before it answers. Discord's API calls the flag nsfw and describes it as "whether the channel is age-restricted". discord.py exposes it as is_nsfw() on channels and threads. Direct messages carry no such flag, so a mention there, or in any unlabeled channel, gets a one-line notice and never reaches the model.

Discord also publishes a Developer Policy for apps; read it before you add the bot to servers you do not run. Use of this API also falls under our acceptable use policy.

Run the bot

shell
1python3 -m pip install discord.py openai
2export DISCORD_TOKEN="your-bot-token"
3export ULLM_API_KEY="sk-ullm-live-…"
4export ULLM_MODEL="glm-5.3-flash-uncensored" # optional
5python3 bot.py

"Logged in as" in the terminal means Discord accepted the token. In the age-restricted channel, mention the bot with a request, for example a villain's monologue for your campaign. A typing indicator shows while the model answers. The bot runs as long as the script does, on your machine or any small server.

Which uncensored build to use

Three builds, with the figures the catalog gave when this page rendered and today's verdict from the daily real-model check, which sends a fixed prompt to every model each day:

Uncensored models and their prices per million tokens
Wire formats
GLM-5.3 Flash Uncensoredglm-5.3-flash-uncensoredUncensored$0.1086$0.3802openai
GLM-5.3 Uncensoredglm-5.3-uncensoredUncensored$0.282$0.846openai

Prices per million tokens, read from the live catalog on 2026-10-27.

Model idDaily checkContext window
glm-5.3-flash-uncensoredSteady since Oct 12not published
glm-5.3-uncensoredSteady since Oct 12not published

Windows from the live catalog on 2026-10-27; verdicts from /proof at render.

The flash build is the default for a busy channel. For a character bot, set ULLM_MODEL to roleplay-uncensored and write the character into SYSTEM. More character builds are listed with the Character AI alternatives.

What a busy server costs

The worked example: 300 replies a day for 30 days. Each request sends about 200 tokens (the system prompt and the message) and gets about 300 back.

Model9,000 replies a month
GLM-5.3 Flash Uncensored$1.22
GLM-5.3 Uncensored$2.79

Computed at render from the catalog prices on 2026-10-27: replies × 200 × input price, plus replies × 300 × output price.

Adding memory changes this most. Send the last ten messages as history and every request grows by their length. The max_tokens=500 line caps the reply side. Every per-token rate is on the pricing page.

Check the key before the bot

This lists the model ids the key can call. It is free and takes a second.

curl
1curl https://api.uncensoredllm.ai/v1/models \
2 -H "Authorization: Bearer sk-ullm-live-…"

A 200 with a list of ids means the key works and ULLM_MODEL only has to match one of them. A 401 says which way the key is wrong:

http
1HTTP 401
2{"error":{"message":"No API key was sent.","type":"authentication_error","param":null,"code":"invalid_api_key"}}
http
1HTTP 401
2{"error":{"message":"The API key is not valid.","type":"authentication_error","param":null,"code":"invalid_api_key"}}

Both bodies as the API returned them on 25 October 2026.

Troubleshooting the Discord bot

The bot replies "The model API answered 401"

ULLM_API_KEY is empty, wrong or revoked. Run the curl above with the same key.

"The model API answered 404"

ULLM_MODEL is not an id in the catalog:

http
1HTTP 404
2{"error":{"message":"No such model: glm-5.3-flash-uncensord.","type":"not_found_error","param":null,"code":"model_not_found"}}

"The model API answered 402"

The prepaid balance is at zero. Top up in the console; a refused request is not charged.

http
1HTTP 402
2{"error":{"message":"The prepaid balance does not cover this request. Top up at … to continue.","type":"insufficient_quota","param":null,"code":"insufficient_balance"}}

PrivilegedIntentsRequired on start

The code asks for an intent the Developer Portal has not switched on; Discord closes the connection with code 4014. The file above asks for none. If you add message_content, enable it on the Bot page as well.

Forbidden when the bot replies

discord.py raises Forbidden when the bot lacks a permission. Check Send Messages and Read Message History on that channel. Base URL, key and model id for other clients are on the docs page.

Questions

Bot directories such as top.gg and discord.bots.gg list hosted ones. Running your own, as here, means you pick the model and pay per reply on your own key.

Discord’s Community Guidelines (rule 9) require an age-restricted label on channels with sexually explicit adult content. This bot sends prompts to the model only from channels that carry the label; anywhere else it replies with a one-line notice.

No. It reads only messages that mention it, and Discord delivers those without the intent. Asking for the intent in code without enabling it in the Developer Portal makes discord.py raise PrivilegedIntentsRequired.

Yes. Use the OpenAI Node SDK with baseURL https://api.uncensoredllm.ai/v1 and your key; the logic is the same. The docs page has the Node snippet. See the docs page.

Any id that takes the OpenAI wire format works. A flash build keeps a busy channel cheap; a roleplay build suits a character bot. See the uncensored model list.

Only the tokens: the table above prices a month of replies at the live catalog rates. There is no subscription; the balance is prepaid from $10. See per-token prices.

Get a keyPrepaid from $10, no subscription.

New models and price changes, once a month.

New uncensored builds with their prices, price moves, and what the daily model check found.