Profanity filtering users can't bypass.
Profanity scored on its own, every disguise named, and a free key in a minute.
Azure AI Content Safety: not run on our benchmark, so no figure for it here.
Head to head, 2026-09-28; the Scunthorpe Test, 2026-10-20. How it was measured
Choose Unprofane for
Each line on Azure AI Content Safety cites its own pages, one click away.
| Question | Unprofane | Azure AI Content Safety |
|---|---|---|
| Accuracy, published | 89.0% of toxic text caught at 7.0% clean flagged, on a private test set; 0.0% of the Scunthorpe Test’s innocent words flagged. See it | Not run on our benchmark: no figure here. |
| What it moderates | Profanity, slurs, insults, threats, sexual language, self-harm and reserved names, in 17 languages, with 17 disguises undone and named. Text only. See it | |
| What an answer shows | The decision and why: each word, where it was, how it was disguised, its severity, a censored version and the engine version. See it | |
| Deployment and upkeep | Nothing to install or run: one HTTPS call, answered at Cloudflare’s edge. Engine versions are dated and never change once published. See it | An Azure subscription, and a resource in a supported region. [1] |
| Review by people | Allow, review or block, set by audience (kids, teen, standard, strict), so only what needs a person reaches your team. See it | A Studio to set up a moderation workflow and watch it. [1] |
| Integrations | JavaScript and Python SDKs, an MCP server, a Perspective-compatible endpoint, and recipes for Supabase, Clerk, Firebase, Auth0, Better Auth, Unity and Godot. See it | A REST API, client libraries and the Studio. [4] |
| Pricing | From $9 a month for 25,000 checks, with a published overage rate. Free test keys, 100 checks a day, no card. See it | A free tier and a paid one, F0 and S0. [1] |
| Processing and retention | Kept 90 days when the model reads it, then deleted; Growth, Scale and Enterprise can turn that off. Never logged or cached. See it | Inputs not stored or used for training; blocklists are kept. [4] |
The live engine. Type a disguised word and watch it read; type an innocent one and watch it pass.
False positives on innocent words are counted and published, and run again for every engine version.
0.0%
false positives on 1,470 innocent words and phrases, the Scunthorpe Test of 2026-10-20
One request in the language you use. The answer carries the evidence.
curl -X POST https://api.unprofane.com/v1/check \
-H "Authorization: Bearer up_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"text":"xXsh1tXx","context":"username","policy":"teen"}'const res = await fetch('https://api.unprofane.com/v1/check', {
method: 'POST',
headers: { Authorization: 'Bearer up_test_YOUR_KEY', 'Content-Type': 'application/json' },
body: JSON.stringify({
"text": "xXsh1tXx",
"context": "username",
"policy": "teen"
}),
});
const result = await res.json();import httpx
r = httpx.request(
"POST",
"https://api.unprofane.com/v1/check",
headers={"Authorization": "Bearer up_test_YOUR_KEY"},
json={
"text": "xXsh1tXx",
"context": "username",
"policy": "teen",
},
timeout=5.0,
)
result = r.json()req, _ := http.NewRequest("POST", "https://api.unprofane.com/v1/check",
strings.NewReader(`{"text":"xXsh1tXx","context":"username","policy":"teen"}`))
req.Header.Set("Authorization", "Bearer up_test_YOUR_KEY")
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)$res = file_get_contents('https://api.unprofane.com/v1/check', false, stream_context_create(['http' => [
'method' => 'POST',
'header' => "Authorization: Bearer up_test_YOUR_KEY\r\nContent-Type: application/json\r\n",
'content' => '{"text":"xXsh1tXx","context":"username","policy":"teen"}',
]]));
$result = json_decode($res, true);require 'net/http'; require 'json'
uri = URI('https://api.unprofane.com/v1/check')
req = Net::HTTP::Post.new(uri, 'Authorization' => 'Bearer up_test_YOUR_KEY', 'Content-Type' => 'application/json')
req.body = '{"text":"xXsh1tXx","context":"username","policy":"teen"}'
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
result = JSON.parse(res.body)using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "up_test_YOUR_KEY");
var req = new HttpRequestMessage(HttpMethod.Post, "https://api.unprofane.com/v1/check");
req.Content = new StringContent("{\"text\":\"xXsh1tXx\",\"context\":\"username\",\"policy\":\"teen\"}", Encoding.UTF8, "application/json");
var res = await http.SendAsync(req);
var result = await res.Content.ReadFromJsonAsync<JsonElement>();var http := HTTPRequest.new()
add_child(http)
http.request_completed.connect(func(_r, _code, _h, body):
var result = JSON.parse_string(body.get_string_from_utf8())
print(result))
http.request("https://api.unprofane.com/v1/check",
PackedStringArray(["Authorization: Bearer up_test_YOUR_KEY", "Content-Type: application/json"]),
HTTPClient.METHOD_POST, "{\"text\":\"xXsh1tXx\",\"context\":\"username\",\"policy\":\"teen\"}")