Running an Adult Chatbot Product Responsibly: A Builder's Checklist
An adult chatbot is a real product, and real products carry responsibilities. The API gives you a model that does not refuse lawful adult content, which means your app, not the model, is where age checks, house rules, reporting and abuse controls live. This checklist walks through each layer with working code, so you can launch with confidence.
Updated
Key points
- The platform is for adults only; verify age at your door and keep the check server-side.
- Write your own content policy and a report path, because the model's openness does not replace your rules.
- Sexual content involving minors is always blocked with a 403; design a clear, calm UX for it.
- Limit usage per user and keep the API key on your server; one key is shared by all your traffic.
Who is responsible for what
Think of the stack in two layers. The API serves lawful adult content, fiction and controversial topics without refusing, for users aged 18 and over. Everything around the model, the people who can reach it, the rules they agree to and what happens when something goes wrong, is yours. That split is freeing, but only if you take your half seriously.
Here is a short responsibilities table to keep on the wall.
| Concern | Platform | Your app |
|---|---|---|
| Sexual content involving minors | Always blocked (403) | Explain it, end the scene, prevent workarounds |
| Age of your users | Adults only by terms | Gate and verify at your entrance |
| House rules beyond the law | Not applied | Write, publish and enforce them |
| Per-user abuse | 300 requests per minute per key | Per-user and per-day limits |
| Secret key | Issues it | Keep it on your server |
This is engineering guidance, not legal advice; rules differ by place, so check the requirements where your users live.
Age gating that actually gates
A banner with an "I am 18" button is the minimum, and a determined minor can click it. It still matters, because it records an affirmative statement and keeps your policy honest. The key is where you enforce it. If the check only hides the chat in the browser, anyone can call your endpoint directly. Make the server refuse to talk until a signed marker exists.
import crypto from "node:crypto";
// Minimal 18+ gate: the user must confirm before the chat routes work.
// Replace the confirmation with a real verification step when your market requires it.
const SECRET = process.env.GATE_SECRET; // random string kept on the server
function sign(value) {
return crypto.createHmac("sha256", SECRET).update(value).digest("hex");
}
export function issueAdultCookie(res, userId) {
const stamp = String(Date.now());
const payload = `${userId}.${stamp}`;
res.cookie("adult_ok", `${payload}.${sign(payload)}`, {
httpOnly: true, sameSite: "strict", secure: true, maxAge: 30 * 24 * 3600 * 1000,
});
}
export function requireAdult(req, res, next) {
const raw = req.cookies?.adult_ok || "";
const [userId, stamp, sig] = raw.split(".");
if (!sig || sign(`${userId}.${stamp}`) !== sig) {
return res.status(403).json({ state: "age_required" });
}
req.userId = userId;
next();
}Wire requireAdult in front of every chat route. Add stronger verification when your market or risk level calls for it, such as a third-party age check, and keep the marker short-lived enough that you can re-prompt. Never target students or schools in your marketing, and place the gate before any adult content is shown, including previews and sample conversations.
Writing your own content policy
The model does not refuse lawful adult material, so you decide what your product allows. A one-page policy is plenty. Cover what is welcome, what is off-limits in your community even if lawful (for example harassment of real people or doxxing), how users can report problems, and what actions you take: warnings, temporary mutes, bans.
- Publish it where users sign up, in plain language.
- Mirror it in your system prompt. A line such as "All characters are adults; end any scene that becomes non-consensual or involves real, named people" keeps the bot aligned with your rules.
- Make enforcement boring. Decide thresholds in advance so moderators do not improvise.
- Review the policy every quarter as your product evolves.
A reporting path users can find
Give every message a small report control, and make it work without a login detour. When someone reports a reply, capture the minimum you need to act: the user id, the message id, a short reason and a timestamp. Route it into a queue your moderators watch.
app.post("/api/report", requireAdult, express.json(), (req, res) => {
const { messageId, reason } = req.body;
// Store only what you need to act on: who, which message, why, when.
queueForModerator({ userId: req.userId, messageId, reason: String(reason).slice(0, 500), at: Date.now() });
res.json({ ok: true });
});Keep your promises narrow. Tell users exactly what a report triggers, and only store what your own policy and applicable rules require. Review how long you keep reports, and delete them on a schedule. Acknowledge each report to the person who filed it, even with a one-line thank-you, because users stop reporting when nothing seems to happen. Track how quickly your moderators close items, and set a target you can actually meet.
Designing for the 403 minors block
Sexual content involving minors is always blocked, in fiction and roleplay too, and the API answers with a 403 and the code content_blocked. Do not retry it and do not try to rephrase on the user's behalf. Instead, treat it as a product moment that deserves a clear design.
- Translate it on the server into a state your UI knows, so raw upstream text never reaches the screen.
- Say what happened in one calm sentence and name the rule.
- End or reset the scene. Offering "continue anyway" defeats the point.
- Log the event with the user id so repeat attempts can trigger your own enforcement.
- Do not guess at intent. Some blocks come from ambiguous wording; the neutral message works for both cases.
// server: translate upstream outcomes into states your UI understands
if (upstream.status === 403) {
return res.status(403).json({
state: "content_blocked",
message: "That request breaks our rules, so we can't continue this scene.",
});
}
if (upstream.status === 402) return res.status(503).json({ state: "paused" });
if (upstream.status === 503) return res.status(503).json({ state: "busy" });// browser: show a calm, specific message for each state
const MESSAGES = {
content_blocked: "This scene can't continue. Sexual content involving minors is never allowed. Start a new chat any time.",
age_required: "Please confirm you are 18 or older to use the chat.",
slow_down: "You're sending messages quickly. Give it a few seconds.",
daily_limit: "You've reached today's message allowance. Come back tomorrow.",
paused: "Chat is briefly unavailable. We're on it.",
busy: "The service is busy. Try again in a moment.",
};
async function send(messages) {
const res = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ messages }),
});
if (!res.ok) {
const info = await res.json().catch(() => ({}));
showBanner(MESSAGES[info.state] || "Something went wrong.");
if (info.state === "content_blocked") endCurrentScene();
return null;
}
return res;
}
Per-user abuse and rate controls
Your single API key allows 300 requests per minute in total, shared by all users. One scripted account can starve everyone else, so enforce fair-use limits yourself, well below that ceiling. The example caps each user at 12 messages a minute and gives each a daily token budget measured from the usage figure in the final stream chunk.
const WINDOW_MS = 60_000;
const PER_USER_PER_MIN = 12; // your product rule, far below the key's 300
const DAILY_TOKEN_BUDGET = 60_000; // per user, tracked from the usage chunk
const hits = new Map(); // userId -> array of timestamps
const spent = new Map(); // userId -> tokens used today
export function userLimit(req, res, next) {
const now = Date.now();
const recent = (hits.get(req.userId) || []).filter((t) => now - t < WINDOW_MS);
if (recent.length >= PER_USER_PER_MIN) {
return res.status(429).json({ state: "slow_down" });
}
if ((spent.get(req.userId) || 0) >= DAILY_TOKEN_BUDGET) {
return res.status(429).json({ state: "daily_limit" });
}
recent.push(now);
hits.set(req.userId, recent);
next();
}
export function recordUsage(userId, totalTokens) {
spent.set(userId, (spent.get(userId) || 0) + totalTokens);
}For production, store counters in a shared store instead of process memory so limits survive restarts and work across several servers. Add a modest max_tokens on every request to bound the worst case, and watch for 402 on your own balance. A free trial of $0.50 for 7 days is a nice way to test these controls before real traffic arrives.
Keep the key on the server
Never ship the key in front-end bundles, mobile apps or public repositories. Store it as an environment variable or in a secret manager, load it only in your backend and exclude it from logs and error reports. If you suspect exposure, regenerate it from your account; the old key stops working immediately, so have a deploy ready. Remember there is one key per account, which makes a clean proxy layer even more valuable. The web chat tutorial shows the pattern end to end, and the docs list the error codes your wrapper should map.
Pre-launch checklist
Run through this list the day before you open the doors. Each item takes minutes and prevents a class of problems that is painful to fix after users arrive.
- Try to break the gate. Call your chat route with curl and no cookie. It must answer 403 with
age_required, never a model reply. - Trigger every state. Stub the upstream to return 402, 403, 429 and 503 in turn, and confirm each shows the right banner and never leaks raw error text.
- Read your own policy aloud. If a moderator cannot decide a case from it in a minute, tighten the wording.
- Test the report button from a phone screen. A control that is hard to reach does not count as a reporting path.
- Search your repository for the key. Look in front-end folders, build output, sample configs and commit history. Regenerate it if you find it anywhere public.
- Set alerts. Page yourself on a 402, on a spike of 403 blocks from a single user and on a jump in daily token use.
- Prepare the pause switch. A single flag that turns chat off gracefully is invaluable during an incident.
Safety is never finished, and neither is the list; add items every time an incident teaches you something new, and share them with whoever joins your team next. Revisit the list when you add features such as image uploads in your own UI, group rooms or public character sharing, because each one opens new paths for abuse. For help shaping the characters themselves, read the persona design guide, and keep the basics of your integration notes in the docs handy as your team grows.
Questions and answers
Is a checkbox enough for age verification?
It is a minimum and records a statement, but it is easy to bypass. Enforce it on the server and add stronger verification where your risk or local rules call for it.
What should my app do on a 403 content_blocked?
Do not retry. Translate it into a calm in-app message, end or reset the scene and log the event for your own enforcement.
Can I let users share my API key's capacity fairly?
Yes, by adding per-user rate and daily token limits on your server. The key itself is limited to 300 requests per minute in total.
Does the model enforce my house rules?
No. It does not refuse lawful adult content, so your policy, system prompt and moderation flow have to do that work.
Your key is one form away
Create an account, copy the key, change the base URL. That is the whole setup.