Methodology
Sample selection
Each cycle tests humanizers against writing prompts spanning six categories:
academic_essay: argumentative essays, lit-review introsapplication_essay: personal statements, cover lettersblog_post: listicles, how-to guidesmarketing_copy: product descriptions, landing pagesdiscussion_board: discussion-board responsesnews_article: news article writing
The full template set, with target word counts and the placeholder tokens
each contains, is published per cycle as templates.json.
Source-LLM outputs (the texts each humanizer is asked to rewrite) are
published as samples.json, and per-cycle sample counts are
recorded in cycle.json and on leaderboard.json.
Each template contains [BRACKETED] placeholder tokens
([TOPIC], [ROLE], [FIELD], etc.).
At cycle creation we fill every token from a curated value bank using a
deterministic hash seeded by a per-cycle secret nonce. The value banks are
versioned in the benchmark source and frozen into every cycle as
banks.json; using banks rather than free-typed values
eliminates founder discretion in what gets tested, and the seeded hash
means anyone with the published banks and algorithm can re-derive the
exact prompts after the cycle closes.
Transparency & verification
Because cycle names are predictable (e.g. January 2026, then
February 2026), seeding placeholder selection on the cycle name
would let a humanizer pre-compute next month's prompts and tune against
them. We use a commit-reveal scheme instead:
- At cycle creation, we generate a random 32-byte
nonceand publish only its SHA-256 to the public repo asdata/cycles/<cycle>/commit.json. The nonce itself stays private. - At cycle close, after all humanizers have been scored,
we publish the nonce as
nonce.txtalongside frozen snapshots of the templates (templates.json), the value banks (banks.json), the selection algorithm (select-placeholders.js), and the resolved prompts (prompts.json).
The scripts/verify-cycle.ts script in the public repo
re-runs the frozen algorithm against the published nonce, banks, and
templates, then asserts that the resulting prompts match
prompts.json exactly and that sha256(nonce)
matches the committed hash from cycle start. It also runs the full
score replay described below. Run it locally:
git clone https://github.com/HumanizerBench/humanizerbench
cd humanizerbench
npm install
npm run verify # verify every cycle
npm run verify -- "January 2026" # verify one cycle (quote names with spaces) Tested humanizers
The active humanizer set is listed on the leaderboard. Each humanizer has a dedicated detail page under
/humanizers/[slug]. WriteHuman is included on the
leaderboard and is held to exactly the same methodology, scoring, and
penalty rules as every other humanizer.
How we configure each humanizer
Humanizers are not standardized: they expose different modes, models, and strength settings, and there is no single configuration that maps cleanly across tools. To keep the comparison fair and give each tool its best shot, we apply the same selection policy to all of them:
- Most-undetectable setting. Where a tool offers a choice of modes or models, we pick whatever the vendor advertises as its most human / most undetectable option. We want to measure each humanizer at its strongest, not catch it on a weak default.
- Base monthly plan. We buy each tool's entry-level monthly paid plan and test on that. We do not purchase annual or top-tier plans to chase a better score.
- Upgrades for credits only. If a tool runs out of credits before the cycle's runs are finished, we upgrade only to obtain enough credits to complete the run. Even then we use only the base plan's features — if a more powerful model or mode is exclusive to the higher tier, we do not use it. We consume the extra credits and nothing more, so the result still reflects the base plan's capabilities.
The plan tier and the exact settings we used for each humanizer are
recorded per cycle in leaderboard.json under
humanizers[i].plan_tier_used and
humanizers[i].settings_used, so the configuration behind
every score is auditable.
Detectors
We test every humanized output against five independent commercial AI
detectors: GPTZero, Originality.ai, Copyleaks, Winston AI, and ZeroGPT.
We do not operate, fine-tune, or coordinate with the detectors. For each
humanizer output we submit the same text to all five and record each
detector's own human-likelihood score on a common [0, 1]
scale, where 1 means "human" (the humanizer evaded the
detector) and 0 means "AI" (the detector caught it).
Detectors that natively report an AI-probability are inverted
(human = 1 − ai) and detectors that report a 0–100 score are
divided by 100; we do not otherwise calibrate, re-scale, or weight the
detectors against one another — each detector's score enters the
per-test median exactly as the vendor returned it. The full per-test
verdicts are published per cycle as detector-scores.json,
where this value is the raw_score field.
Scoring formula
The composite score for each humanizer is computed from four sub-scores,
each in [0, 1]. The base composite is bounded
[0, 100]; per-test penalties (defined below) are then
subtracted and the result clamped to a final
composite ∈ [0, 100]:
composite_raw =
42 * bypass_rate
+ 32 * meaning_preservation
+ 16 * readability
+ 10 * consistency_across_categories
composite = max(0, composite_raw - sum_of_penalty_deltas)
Weights and penalty rules are inlined verbatim into the published
scoring.js file every cycle, so the methodology version on a
given leaderboard.json pins the exact constants that ran. The
composite weights above (42 / 32 / 16 / 10) have been stable
since launch; the current methodology is
methodology_version 1.2.0 — readability moved to a
language-model quality rating, and each per-penalty cap was raised to
10. Every version's rationale is recorded in the published
change log, and the exact constants are documented inline in the scoring
code.
Each sub-score has a precise algorithmic definition, given below.
Aggregate per humanizer always means "over the humanizer's set
of tests in this cycle with status = "complete""; flagged
and failed tests feed only the penalty rules. Tests reference the
published row shapes in tests.json and
detector-scores.json.
bypass_rate
For each test, compute the median of all
raw_score values across the five detectors in
detector-scores.json (median, not mean, because some
detectors binarize their output near 0 or 1, and a single outlier would
otherwise swing the mean by 0.25). The humanizer's
bypass_rate is the mean of those per-test
medians.
meaning_preservation
For each test, the cosine similarity of the OpenAI
text-embedding-3-large embeddings of
input_text and output_text, clamped to
[0, 1]. This raw cosine is the per-test value published in
tests.json as tests[i].meaning_preservation, and
it is what the severe_meaning_drift penalty checks.
Cosine between any two on-topic English passages empirically floors around
0.85, so faithful rewrites cluster in a narrow high band and
the raw value barely separates humanizers — even though meaning carries
32% of the composite. When forming the humanizer aggregate we
therefore rescale each per-test value from [0.75, 1.0] onto
[0, 1] (clamped) and take the mean, so genuine differences in
meaning preservation register in the score. The rescale is monotonic — it
never reorders humanizers, only spreads them — and the published
scoring.js applies the identical transform, so an auditor
re-derives the same aggregate from the raw per-test values.
readability
A language model reads each output and rates its writing quality — clarity,
fluency, and naturalness — returning a value in [0, 1] where
higher is better. Each output is rated once (the rating is cached by text
hash, so it is stable across re-runs) and the per-test value is published
in tests[i].readability; the humanizer aggregate is the mean.
The published scoring.js simply averages these published
per-test ratings and never calls a model, so an auditor reproduces the
leaderboard from the raw files via npm run verify without any
model access.
Before methodology_version 1.1.0, readability was a grammar-
and spelling-error rate measured with LanguageTool rather than a
model quality rating. That heuristic rated almost every output as
flawless and barely separated humanizers, so it was replaced with the
direct writing-quality rating described above; detector-bypass and
meaning-preservation scoring were unaffected.
consistency_across_categories
For each writing category present in the humanizer's tests, compute the
mean of per-test bypass medians within that category. The humanizer's
consistency score is
max(0, 1 - stddev(category_means)). Consistency needs enough
categories to be meaningful, so a cycle that tested
fewer than three categories scores a neutral
0.5 rather than a free 1.0 — otherwise a tool
tested in a single category would top the consistency term over one tested
broadly with mild variance. With three or more categories, a humanizer
with identical category means scores 1; one that crushes blog
posts but flunks academic essays sees its consistency score dragged down.
Penalties
A humanizer's composite score is reduced by one or more penalties when
its output exhibits known quality issues. Penalties deduct from
composite_raw; the final composite is clamped to
[0, 100]. Per-humanizer penalty deltas are published in
leaderboard.json under
humanizers[i].penalties_applied, so every deduction is
auditable.
Each penalty has a fixed deduction per occurrence and a cap on its
total impact, so no single category can dominate the final score. The
maximum possible total penalty across all codes is −50.0 (out of 100).
| Penalty | Trigger | Per / Cap |
|---|---|---|
Meaning drift
(severe_meaning_drift)
The output's meaning drifted significantly from the original input. |
Per test where
tests[i].meaning_preservation < 0.85.
| −1.0 / −10.0 |
Length inflation
(length_inflation)
The output ran much longer than the input, a common trick that pads text to dilute the AI signal. |
Per test where
word_count(output_text) / word_count(input_text) > 1.4.
| −1.0 / −10.0 |
Length deflation
(length_deflation)
The output came back much shorter than the input. The rewrite dropped content instead of paraphrasing it. |
Per test where
word_count(output_text) / word_count(input_text) < 0.6.
| −1.0 / −10.0 |
Identical to input
(identical_to_input)
The tool returned the input mostly unchanged. No real humanization happened. |
Per flagged test with
failure_reason_code = "identical_to_input".
| −2.0 / −10.0 |
Refusal
(refusal_in_output)
The tool refused to humanize the input, often due to a content-policy block. |
Per flagged test with
failure_reason_code ∈ {"refusal_in_output", "refused_input"}.
| −1.0 / −10.0 |
Unavailable tools are excluded, not penalized. If at least
half of a humanizer's attempted tests are flagged as site or access
failures (site down, paywall, captcha, no output generated, or a ToS block),
the tool could not be meaningfully exercised that cycle. Rather than rank it
on the handful of runs that happened to go through — or deduct points for it
being offline — we drop it from the ranking and record it separately under
leaderboard.json's unavailable_humanizers, with its
attempt counts. Output-quality failures (identical-to-input, refusal,
truncation) are treated differently: the tool ran, so they keep the
per-occurrence penalties above.
Reproducing the leaderboard from raw data
Anyone can re-derive every score on the leaderboard from the published cycle bundle. The shortest path:
-
git clone https://github.com/HumanizerBench/humanizerbenchandcdin. npm install(only dev deps are needed).-
npm run verify -- <cycle>. Under the hood this loadsscoring.jsfrom the cycle bundle, runs it againstsamples.json,tests.json, anddetector-scores.json, and asserts that the per-humanizer results matchleaderboard.jsonwithin1e-4.
The published scoring.js is a self-contained ESM module
with no external dependencies. To verify by hand without our script,
import it and pass the three JSON arrays:
import { computeLeaderboard } from "./data/cycles/January 2026/scoring.js";
import samples from "./data/cycles/January 2026/samples.json" with { type: "json" };
import tests from "./data/cycles/January 2026/tests.json" with { type: "json" };
import detectorScores from "./data/cycles/January 2026/detector-scores.json" with { type: "json" };
const result = computeLeaderboard({ samples, tests, detectorScores });
console.log(result.humanizers[0]); // top-ranked humanizer for the cycle
Re-deriving a single sub-score by hand also works. To check one
humanizer's bypass_rate:
-
From
tests.json, keep rows wherehumanizer_slug = handstatus = "complete". Call this listT_h. -
For each test in
T_h, gather everyraw_scoreindetector-scores.jsonwith thattest_id. Take the median. -
The humanizer's
bypass_rateis the arithmetic mean of those medians. -
Compare to
leaderboard.json.humanizers[?].scores.bypass_rate.
Floating-point comparisons should use an absolute tolerance of
1e-4; verify-cycle.ts uses the same.
Versioning
Every cycle's leaderboard.json records three version stamps:
| Stamp | Bumped when |
|---|---|
methodology_version | Anything on this page changes: formula weights, sub-score definitions, penalty rules. |
scoring_version | The scoring script changes (typically alongside methodology_version). |
prompt_set_version | A template is added/removed/edited, OR a value bank is changed, OR the placeholder-selection algorithm changes. |
Adding or removing a humanizer or detector does not bump a stamp: each
cycle's leaderboard.json and
detector-scores.json already record exactly which humanizers
and detectors were tested, so that part of the audit trail comes from the
data itself rather than a separate version number.
Past cycles are immutable. Each cycle bundle (templates, banks, algorithm, scoring code, raw data, leaderboard) is frozen at publish time, so a methodology change today never alters a cycle that ran yesterday; it just changes the version stamps on the next cycle.
Methodology integrity
WriteHuman does not manually adjust rankings. Rankings are computed
mechanically from the published raw data using the published
scoring.js. The integrity argument is structural rather
than reputational: if a ranking didn't come from the published
algorithm running on the published data, npm run verify
would catch it, and anyone in the world can run that script.
How to contribute or dispute
If you've found an error or want to dispute a score, see our fairness & corrections policy for the contact address and the response-time commitment.