Start now

Build an Uncensored AI App on a Text API

To build an uncensored ai app you need four things: a key, a base URL, a loop that remembers the conversation, and a plan for when the context fills up. This walkthrough builds all four in one Python file.

Last updated

Summary

  • The app uses the standard OpenAI Python SDK with a custom base URL and the model uncensored.
  • History lives in your code; the API itself keeps no conversation for you.
  • Trim old turns so each request stays well inside the 100,000-token window.
  • Handle HTTP 429 with a short wait; the API sends Retry-After: 5 on overload.
A staircase of glowing chat bubbles rising toward a terminal, with a ribbon of light linking the turns

What you are building

The goal is a terminal chat app for fiction and roleplay-style writing that keeps context across turns, prints text as it streams, survives rate limits and saves the session when you quit. The same skeleton works behind a web UI or a Discord bot later. It is text only: this API does not generate images, audio or video.

If you want to build an uncensored ai app for other adults, remember the rule that applies everywhere: the model answers without content refusals for lawful adult use, and sexual content involving minors is off the table, full stop. Put that into your app's own terms as well.

Setup

Get a key from the API key page (new users can claim $0.50 of trial credit there), then:

python -m venv .venv
source .venv/bin/activate
pip install openai
export API_KEY="paste-your-key-here"
# base URL: https://api.unsensoredai.com/v1   model: uncensored

Any OpenAI SDK works because the API follows the same request and response shapes. You change two things, the base URL and the key, and keep everything else you know.

The complete app

import json
import os
import time

from openai import OpenAI, RateLimitError

client = OpenAI(
    base_url="https://api.unsensoredai.com/v1",
    api_key=os.environ["API_KEY"],
)

SYSTEM = "You are a story co-writer. Stay in character and keep replies under 200 words."
MAX_HISTORY_CHARS = 200_000   # about 50k tokens at 4 characters per token
history = []


def window():
    """Return the system prompt plus as many recent turns as fit."""
    kept, size = [], 0
    for message in reversed(history):
        size += len(message["content"])
        if size > MAX_HISTORY_CHARS:
            break
        kept.append(message)
    return [{"role": "system", "content": SYSTEM}] + kept[::-1]


def ask(text):
    history.append({"role": "user", "content": text})
    for attempt in range(4):
        try:
            stream = client.chat.completions.create(
                model="uncensored",
                messages=window(),
                stream=True,
                temperature=0.9,
            )
            reply = ""
            for chunk in stream:
                if chunk.choices and chunk.choices[0].delta.content:
                    piece = chunk.choices[0].delta.content
                    reply += piece
                    print(piece, end="", flush=True)
            print()
            history.append({"role": "assistant", "content": reply})
            return
        except RateLimitError:
            time.sleep(5)
    history.pop()
    print("Still busy. Try again in a moment.")


if __name__ == "__main__":
    while True:
        line = input("you> ")
        if line.strip() in ("/quit", "/exit"):
            break
        ask(line)
    with open("session.json", "w") as f:
        json.dump(history, f, ensure_ascii=False, indent=2)

Why each part is there

History is your job

The chat completions method is stateless. Every call must carry the messages you want the model to see, which is why ask appends both sides of every exchange. The cost consequence is that input tokens grow with the conversation, since earlier turns are billed again each time they are sent.

Trimming by size

The context window is 100,000 tokens, prompt plus completion. Counting exact tokens needs a tokenizer; counting characters is cheaper and good enough here. At roughly four characters per English token, the 200,000-character cap is about 50,000 tokens, which leaves plenty of room for the reply. If your story needs long memory, replace the cutoff with a summary: ask the model to condense the oldest turns into a paragraph and keep that paragraph at the top.

Streaming

With stream=True text appears as it is produced. A streamed request occupies its concurrency slot until the stream finishes, so if you run several chats at once, a full key allows 8 at a time and a trial key allows 2.

Saving and resuming

The last lines of the script write history to session.json. To resume tomorrow, load that file into history at startup with json.load. Because the file holds the full transcript, it also doubles as a log you can search later. Keep it out of version control, since it contains everything you typed.

Retries

The SDK already retries a couple of times by default, but a loop of your own makes the behavior visible. A 429 means either a limit (rate_limited or concurrency) or temporary overload, which comes with Retry-After: 5. Waiting five seconds is the right move in all three cases.

What a session costs

Assume a system prompt of 100 tokens, user messages of 120 tokens and replies of 250 tokens, over 40 turns. Input at turn n is 100 + 370 x (n - 1) + 120 tokens, since each earlier exchange adds 370. Summed over 40 turns that is 40 x 220 + 370 x (0 + 1 + ... + 39) = 8,800 + 370 x 780 = 297,400 input tokens. Output is 40 x 250 = 10,000 tokens.

  • Input: 297,400 x $0.15 / 1,000,000 = $0.044610
  • Output: 10,000 x $0.50 / 1,000,000 = $0.005
  • Total: about $0.0496, roughly five cents for a long story

The trial credit covers about ten sessions like that. Notice that the input side dominates: trimming or summarizing old turns is the main lever for cost, more than anything you do to the output. The pricing page lists the same rates and the top-up bonuses.

Try it and watch the numbers

Run python app.py and have a short conversation. Print the balance header once to see what a few turns cost. With the SDK you can read raw headers like this:

raw = client.chat.completions.with_raw_response.create(
    model="uncensored",
    messages=[{"role": "user", "content": "Say hi in four words."}],
)
print(raw.headers.get("X-Balance-USD"))
completion = raw.parse()
print(completion.choices[0].message.content)
# client base URL: https://api.unsensoredai.com/v1

Call it before and after a long turn and subtract. If the drop is larger than your arithmetic predicts, your history is bigger than you think. Print len(window()) to see how many messages are being sent.

Troubleshooting

  • KeyError for API_KEY. The variable is not set in this shell. Export it again or load it from a .env file that stays out of version control.
  • A not found error. The base URL must end at /v1. The SDK adds /chat/completions itself.
  • Replies drift out of character. The system prompt is sent on every request, but a long history can overpower it. Shorten the history window or restate the key rule in the latest user message.
  • Replies get cut off. You may have set max_tokens too low. The parameter is optional, so remove it and let the completion use the room left in the window.
  • Two chats at once fail. A trial key allows 2 concurrent requests. Queue the rest or top up to get a full key's 8.

Next steps

Three upgrades fit this skeleton without a rewrite.

  • Structured output. Pass response_format={"type": "json_object"} and mention JSON in the prompt when you need fields such as a scene title and summary to store alongside the text.
  • Tool calling. The API supports tools, tool_choice, streamed tool_calls and results sent back as role: "tool", so the model can call a dice roller or a lookup function you define.
  • Sampling controls. Temperature, top_p, stop, seed and the penalties are passed through, which helps when a character keeps repeating itself.

The documentation covers each of these, and our integration guide shows the same ideas from a different angle. Keep the key in an environment variable, and if it ever leaks, regenerate it on the key page: the old one stops working right away.

Frequently asked questions

Do I need a special SDK?

No. The official OpenAI SDKs work once you set the base URL to https://api.unsensoredai.com/v1 and use your key with the model uncensored.

Does the API remember my conversation?

No. Each request must include the messages you want the model to see, so your app stores and resends the history.

How much text can the app send at once?

The context window is 100,000 tokens for prompt plus completion, and the maximum request body is 8 MB.

Can the app generate images?

No. The service is text only, with no image, audio or video generation.

Generate your API key

Sign up, copy your key, and update the base URL. Setup takes seconds.

Start now