Profanity filter vs AI moderation: what each one catches
A word-list filter blocks the words on its list. AI moderation reads the whole message and says what kind of abuse it finds and how sure it is. Each misses things the other catches. Here is how they differ on spelling tricks, innocent words, context, languages and cost, and when to use both.
Profanity filter vs AI moderation comes down to how each reads a message. A profanity filter checks it against a list of words. AI moderation uses a trained model that reads the whole message and scores it for each kind of abuse. The filter is fast, free per check and easy to explain, and it knows only what is on its list. The model understands more, costs a check each time and makes mistakes of its own. Use a list for strings you always refuse, AI for the rest, and people for the cases in between.
Profanity filter vs AI moderation at a glance
Here, AI moderation means a trained model that reads the message, whether you run it or an API runs it for you. Facts about other tools come from their own pages, read on 17 October 2026. Where a cell describes an AI result, it describes Detectivision's: a flag with a confidence, for the whole message.
| Word-list profanity filter | AI moderation | |
|---|---|---|
| How it decides | Finds strings from a list in the text | A model reads the whole message and scores it for each kind of abuse |
| Misspellings and leetspeak | Finds the spellings on the list, or the ones its code undoes. Others pass | Can recognise spellings it has learned. Deliberate edits can still fool it |
| Harmless words that contain a listed string | Can block them: the Scunthorpe problem | Reads the word in its sentence. It can still flag harmless text |
| Context | None. A listed word is a match, aimed at anyone or no one | Uses the words around a word, with mistakes of its own |
| Abuse with no listed word | Passes | Can be found, such as an insult or hate speech |
| Languages | One list per language | Depends on the provider |
| Which word was found | The matched word, so you can mask it | A verdict on the whole message, with a confidence |
| Price per check | None for a library | With an API, a fee or credits per check |
| Where it runs | In your own code, with no network call | With an API, a network call per message |
| Upkeep | You add words, spellings and languages | With an API, the provider updates its models. You keep your lines |
Misspellings and leetspeak
People write around filters. They put digits for letters (sh1t), dots or spaces inside a word (s.h.i.t), repeated letters (shiiit) or lookalike letters from another alphabet. The glossary calls this filter evasion.
A list matches strings, and every spelling is a different string. Each variant must be on the list, or the code must undo it before matching. Good libraries do part of this. The better-profanity page says it "supports modified spellings", with examples such as p0rn and handj0b, and obscenity's README shows it matching a swear word written with repeated letters. Both state limits. better-profanity says its censor "could easily be bypassed by adding any character(s) to the word", and that words with separators other than spaces, such as "s & m", cannot be recognised. obscenity says: "Use its output as a heuristic, and not as the sole judge of whether some content is appropriate or not."
A model reads patterns, not exact strings, so it can recognise spellings it has learned. It is not immune. In February 2017, researchers at the University of Washington tried small edits on the demo of Google's Perspective API: a dot inside a word, spaces between letters, a repeated or swapped letter. "It's stupid and wrong" scored 89%. "It's stuipd and wrong" scored 17%. Models have changed since then, so read this as a test to run on your own tool, not as a measure of how any tool performs now. The paper's own suggestion was a spell check before scoring, which it says "may however increase the false alarm".
What to do: send the spellings your users really type through any tool you consider. Keep a list of what slips through, add it to your own list, and watch for the pattern. Hold the middle band for a person.
The Scunthorpe problem
A list matches letters, not words, so it can block harmless text that contains a listed string. The problem is named after the English town of Scunthorpe: in April 1996, AOL's profanity filter stopped people there from creating AOL accounts, Wikipedia says. The RISKS Digest of 25 April 1996 records the case. The resident got an account by typing a misspelled town name.
It happens in every language. Each of these everyday words contains a vulgar word of its own language:
| Everyday word | Language and meaning | What a list can do |
|---|---|---|
| ridículo | Spanish: ridiculous | It contains a four-letter vulgar word |
| computador | Portuguese: computer | It contains a four-letter vulgar word |
| Marsch | German: march | It contains a five-letter vulgar word |
| корабля | Russian: of the ship | It contains a three-letter vulgar word |
A model that scores the whole message does not match listed strings, so it does not make this mistake in the same way, but it makes mistakes of its own. The fixes for a list reduce the problem and do not remove it: match whole words, keep an allow-list of harmless words, lowercase text with the rules of its language, and hold uncertain cases for a person. The Scunthorpe problem page has each fix, its limits and code.
Context: the same word can be friendly or abusive
A list sees a word, not its sentence. "This update is damn slow" and "You damn idiot" contain the same listed word. A comment that quotes abuse to report it contains the abuse. And "I know where you live" contains no swear word at all.
A model reads the words around a word, so it can tell more cases apart. Detectivision's profanity flag finds swear words, including friendly swearing and swearing aimed at no one. Its insult flag finds an attack on one person. A message can raise both, and your rules can treat them apart: a gaming chat that allows swearing but not attacks can act on insult alone. The profanity flag is on the Basic plan, and GET /api/v1/flags lists the flags your account has (Flags).
Models still get context wrong. Detoxify's README warns that a comment with swearing or insults in it is likely to be classified as toxic "regardless of the tone or the intent of the author", and that this could bias results against vulnerable minority groups. A study at the 2019 meeting of the Association for Computational Linguistics found a related effect. Models trained on widely used hate speech datasets were up to two times more likely to label a tweet as offensive when it was written in African American English, or by a self-identified African American. The 2017 test of Perspective's demo found false alarms too: "It's not stupid and wrong" scored 83%. So test on your own users' writing, and give authors a way to appeal (appeals).
Other languages
A list is written for one language at a time. Words take endings in many languages, so a list needs each form (the Scunthorpe problem page explains why). AI moderation has a list of languages too.
- A list. obscenity's README shows an English preset, and leo-profanity names English, French and Russian. For any other language you find a list or write one.
- A model. Detoxify's multilingual model says it should only be tested on English, French, Spanish, Italian, Portuguese, Turkish or Russian.
- Detectivision. English, Spanish, Portuguese, French, German, Italian, Turkish, Arabic, Russian and Hindi. Text in other languages is checked, but the results are not reliable.
Whatever you choose, test comments in each language your community writes.
What each costs
| Word-list filter | AI moderation | |
|---|---|---|
| Price per check | None. bad-words, obscenity, leo-profanity and better-profanity are all MIT-licensed | A fee or credits per check |
| Setup | Install a package and choose a list | Create an API key and send one HTTPS request (Quickstart) |
| Upkeep | Your time, in every language | The provider updates its models. You keep your lines and a short list of your own |
| Failures | A list in your own code has no network to fail | A request can fail: retry 429, 500 and 503. Errors and partial results cost nothing |
| People | Moderators read what slips through | Moderators read the middle band |
On Detectivision, a message of up to 2,500 characters costs 1 credit when the result comes by webhook, and 2 for an answer given at once, which is the default. Naming fewer flags never costs more. Basic costs $49 a month, with 10,000 text credits that pay for that many messages checked by webhook, or half as many checked at once. Can you try it first? Yes. Basic is free for 2 months, with 10,000 text, 10,000 image and 300 video credits. The pricing page has the plans, and credits and billing has the rules.
When to use which, or both
| Your situation | Use | Why |
|---|---|---|
| A few strings you always refuse: brand names, spam links, reserved usernames | A list | An exact match is what you want, and it costs nothing |
| Users write abuse in their own words | AI moderation | A list cannot hold every insult, spelling and language |
| Several languages | AI moderation, then test each language | One list per language is a lot of upkeep |
| Chat that must answer at once | Your list first, then AI moderation for what passes | The list is instant. A narrow request, naming only the flags you act on, answers faster |
| Comments that can wait | AI moderation with "async": true | The result comes by webhook at half the price (Async requests) |
| A small community in one language | A list and a moderator may be enough | Few messages, and few spellings to learn |
A layered setup
- Your own list. Strings you always refuse, such as brand names and spam links, matched as whole words. It is free and instant, and a match needs no API call.
- An AI check. Everything that passes goes to the model. Name only the flags your rules use: only those are checked, and a narrow request answers faster (Flags).
- Your lines. Hide above a high line, hold the middle for a person, publish the rest.
- People. Moderators decide the middle band and the appeals.
- Feedback. Add the misses your users report to your list, and the false alarms to your test comments.
The function below runs steps 1 to 3. Its list holds your own strings, not swear words: the profanity and insult flags handle swearing and name-calling.
import os
import re
import requests
API_URL = "https://api.detectivision.ai/api/v1/moderation"
HIDE_AT = 0.8 # your own line: the API raises a flag from 0.5
# Step 1: strings of your own that you always refuse (names, spam links).
# Whole-word, any case. In Python 3, \w counts letters of every alphabet.
OWN_LIST = ["buy-followers.example", "free crypto giveaway"]
OWN_PATTERN = re.compile(
r"(?<!\w)(?:" + "|".join(re.escape(s) for s in OWN_LIST) + r")(?!\w)",
re.IGNORECASE,
)
def check_message(text, message_id):
"""Return "hide", "review", "publish" or "retry" for one message."""
if OWN_PATTERN.search(text):
return "hide" # free and instant: no API call
# Step 2: the AI check. Name only the flags your rules use.
r = requests.post(
API_URL,
headers={"Api-Key": os.environ["DETECTIVISION_API_KEY"]},
json={
"type": "text_moderation",
"input": text,
"mode": "detect",
"flags": ["profanity", "insult"], # drop "profanity" if swearing is fine
"metadata": {"messageId": message_id},
},
timeout=30,
)
if r.status_code == 429 or r.status_code >= 500:
return "retry" # busy or unavailable: nothing is charged
r.raise_for_status() # any other 4xx needs a changed request
result = r.json()
if result["unchecked"]:
return "retry" # partial: free; "no flags" is not "clean"
if not result["flagged"]:
return "publish"
# Step 3: your lines. From HIDE_AT up, hide the message.
# Between 0.5 and HIDE_AT, a person decides.
top = max(f["confidence"] for f in result["flags"])
return "hide" if top >= HIDE_AT else "review"
// Node.js 18 or later, on your server: never in a browser or an app.
const API_URL = "https://api.detectivision.ai/api/v1/moderation";
const HIDE_AT = 0.8; // your own line: the API raises a flag from 0.5
// Step 1: strings of your own that you always refuse (names, spam links).
// Whole-word, any case. \p{L} and \p{N} count letters and digits of every alphabet.
const OWN_LIST = ["buy-followers.example", "free crypto giveaway"];
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const OWN_PATTERN = new RegExp(
`(?<![\\p{L}\\p{N}_])(?:${OWN_LIST.map(escapeRegExp).join("|")})(?![\\p{L}\\p{N}_])`,
"iu",
);
async function checkMessage(text, messageId) {
if (OWN_PATTERN.test(text)) return "hide"; // free and instant: no API call
// Step 2: the AI check. Name only the flags your rules use.
const res = await fetch(API_URL, {
method: "POST",
headers: {
"Api-Key": process.env.DETECTIVISION_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
type: "text_moderation",
input: text,
mode: "detect",
flags: ["profanity", "insult"], // drop "profanity" if swearing is fine
metadata: { messageId },
}),
});
if (res.status === 429 || res.status >= 500) return "retry"; // nothing is charged
if (!res.ok) throw new Error(`Detectivision: ${res.status} ${await res.text()}`);
const result = await res.json();
if (result.unchecked.length > 0) return "retry"; // partial: free; "no flags" is not "clean"
if (!result.flagged) return "publish";
// Step 3: your lines. From HIDE_AT up, hide the message.
// Between 0.5 and HIDE_AT, a person decides.
const top = Math.max(...result.flags.map((f) => f.confidence));
return top >= HIDE_AT ? "hide" : "review";
}
Call it from your server, and keep the key there: a key in a web page or an app can be read by anyone (Authentication). The confidence of a flag is a score from 0 to 1, not a probability you can compare across flags, so set a line for each flag you act on (Flags).
Test your setup
Run these through your list and through any AI check you are considering. Decide what you want first, then compare.
| Test | Example | What you want |
|---|---|---|
| Harmless words with a listed string inside | Scunthorpe, class, ridículo | Allowed |
| The spellings your users type | sh1t, s.h.i.t, shiiit | Caught, or a known miss you review |
| Friendly swearing | "This update is damn slow." | What your rules say: allowed, or held |
| Abuse with no swear word | "You are a waste of space." | Caught as an insult |
| Each language you serve | One comment of each kind in each language | The same result as in English |
You can try them in the dashboard's playground, or send them from your server. The API's answer lists each flag raised with its confidence (Responses).
How the code was checked
On 17 October 2026 the Python function ran with Python 3.9.6 and requests 2.32.5, and the Node.js function with Node.js 22.23.2, against a local stand-in that answers in the documented format. Both returned hide, and sent no request, for messages that hold a string from the list, whatever its case. Neither matched a listed string inside a longer word. Both returned publish for an answer with no flags, review for a flag below the line, and hide for a flag on the line and above it. Both returned retry for a partial answer, a 429, a 500 and a 503. The stand-in also checked each request: the endpoint, the Api-Key header, the JSON body and the flag names. The tests ran against the stand-in, not the live API.
Next: see the profanity filter API, the toxicity detection API for insults, harassment and self-harm, and the hate speech detection API. For the whole check-and-review flow, read how to detect toxic comments. The text request is in the text docs.
Frequently asked
Is a profanity filter enough to moderate a community?
Only for swear words in the spellings on its list. It passes abuse that uses no listed word, such as a plain insult, and it can block harmless words that contain a listed one. Add an AI check for the rest, keep a short list of your own, and give moderators the middle.
Does AI moderation catch leetspeak and misspelled swear words?
Sometimes. A model reads patterns, not exact strings, so it can recognise spellings it has learned, but deliberate edits can fool it: in a 2017 test of Google's Perspective demo, misspelling one word cut a phrase's score from 89% to 17%. Send the spellings your users really type through any tool you consider.
How do I avoid the Scunthorpe problem?
Match whole words, keep an allow-list of harmless words, lowercase text with the rules of its language, and hold uncertain cases for a person. A model that scores the whole message does not match listed strings, but it makes mistakes of its own. The glossary page has each fix, its limits and code.
Can I use a profanity filter and AI moderation together?
Yes. Run a short list of your own first, because it is free and instant. Send what passes to the AI check, and let people decide the middle band. The function on this page runs the list, the AI check and your lines.
Which languages does AI moderation cover?
It depends on the provider, so read its list and test your own languages. Detectivision's text check is reliable in English, Spanish, Portuguese, French, German, Italian, Turkish, Arabic, Russian and Hindi. Text in other languages is checked, but the results are not reliable.
What does AI moderation cost compared with a free profanity filter?
A library has no price per check, and its upkeep is your time. An API charges per check: on Detectivision, a credit per started 2,500 characters when the result comes by webhook, and twice that for an answer at once. Errors and partial results are free.
Find swear words in chat, comments and usernames in ten languages with one API call: separate flags for profanity and insults, no word list to maintain.
The Scunthorpe problem is a filter blocking harmless text because a banned word hides inside it. Why it happens in every language, and the fixes.
How to detect toxic comments: what counts as toxic, the three approaches, what to do with a score, and a working Python and Node.js example.
Check comments and chat messages for insults, hate, harassment and self-harm in ten languages, with a flag for each kind of toxicity.
What OpenAI, Google, AWS, Azure, Sightengine and others give for free, the limits, and the catch, from each vendor’s own pages with the date read.
- Scunthorpe problem
- AOL censors British town's name! (Clive Feather, The RISKS Digest, volume 18, issue 07, 25 April 1996)
- obscenity (README: a profanity filter for Node.js, English preset, MIT licence)
- leo-profanity (README: languages en, fr and ru, MIT licence)
- better-profanity on PyPI (version 0.7.0 of 2 November 2020, MIT: modified spellings and limits)
- bad-words on the npm registry (version 4.1.5, MIT)
- Deceiving Google's Perspective API Built for Detecting Toxic Comments (Hosseini, Kannan, Zhang and Poovendran, arXiv:1702.08138, 27 February 2017)
- The Risk of Racial Bias in Hate Speech Detection (Sap, Card, Gabriel, Choi and Smith, ACL 2019)
- Detoxify: trained models and code to predict toxic comments (README: limitations and languages)
- Detectivision API status (GET /api/v1/status)
- Profanity filter API (what the profanity and insult flags cover)
- Toxicity detection API (the Basic plan has the profanity flag)
- Quickstart
- Authentication
- Text moderation
- Flags
- Responses
- Errors
- Async requests
- Credits and billing
- Detectivision pricing