COMPARISON · WORDLISTS

You already tried
the wordlist.

bad-words is a great 20-minute solution — until real users arrive. Then it censors "classic" and waves through "@ss" in the same afternoon. Filter8 is the deterministic version that catches the dodge and leaves the innocent word alone.

Try to beat the playground → Read the docs
100 free calls · no credit card · paste an evasion and watch it decide.
It censors "Scunthorpe"
A substring match flags classic, assassin, analysis, cockpit. Each one is a support ticket. Filter8 vetoes them with an explicit whitelist.
A kid beats it in seconds
@ss, a s s, sh1t, f🖕ck, zero-width splits — a literal wordlist sees none of it. Filter8 folds 26 evasion classes back before matching.
It can't see contact info
Users swap "six-eight-six… at gmail dot com" to go off-platform. A profanity list has no idea. Filter8's classifiers catch phone, email, and URLs — even spelled-out.

A wordlist vs an engine

CAPABILITY bad-words Filter8
ApproachStatic substring wordlistNormalize → match → score → decide
Evasion (@ss · a s s · sh1t · zero-width · 𝓯𝓾𝓬𝓴)26 classes
False positives (classic · Scunthorpe · assassin)Censors themWhitelist veto
Contact / PII classifiersPhone · email · URL (incl. spelled-out)
Rating-aware (E–M · minors)
OutputCleaned string / isProfaneVerdict + clean_text + via + score
Severity / contextBooleanConfidence + severity → action
Explainable / replayable✓ explain trace
MaintainedCommunity listCurated versioned bundle + your overlay
Cost & fitFree · offline · DIYFree tier → managed API
"bad-words" refers to the popular open-source JavaScript wordlist library and its cousins (better-profanity, leo-profanity, and similar), which share the same static-list behavior.
THE SWAP

Keep your function signature. Change what's behind it.

Wrap Filter8 behind the same call you already make. clean(text) becomes one request that returns a verdict and cleaned text — the rest of your app doesn't move.

BEFORE · BAD-WORDS
import Filter from bad-words;
const filter = new Filter();
const clean = filter.clean(userText);
// "classic" → "*******" · "@ss" → "@ss"
AFTER · FILTER8
const { action, clean_text } =
await (await fetch(https://api.filter8.dev/v1/filter, {
method: POST,
headers: { Authorization: Bearer f8_live_… },
body: JSON.stringify({ text: userText, rating: E10 })
})).json();
// "@ss" → mask · "you are an ***" · "classic" → allow
WHEN BAD-WORDS IS FINE

We're not going to pretend a wordlist is useless

It's free, offline, zero-dependency, and adds no latency. For a prototype, an internal tool, or a place where a slipped word or a false-flag is a shrug, it's genuinely the right call — ship it. Filter8 is for the moment "good enough" stops being good enough: when a false positive is a support ticket, an evasion is a screenshot, and contact-info leaks are a business problem.

Questions from wordlist refugees

What's actually wrong with bad-words?
Nothing, until users show up. Then: false-flags on classic/Scunthorpe/assassin, misses on @ss/a s s/sh1t/zero-width, no PII, no severity, no age ratings.
Isn't an API call slower than a local list?
By one hop. Worth it for correctness + PII + ratings; batch and WebSocket keep it fast.
Can I migrate incrementally?
Yes — wrap Filter8 behind your existing clean(text) signature so nothing else in the app changes.
I'm on better-profanity in Python. Same?
Same failure modes. Filter8 is a language-agnostic HTTP API — call it from Python, Go, Ruby, anything.
COMPARE MORE

Start where you'll end up.

Every team graduates off the wordlist eventually. Skip the support tickets — 100 free calls, no card.

Get an API key Open the playground