A plain-language guide to the method behind ai-text-watermark / @claude-flow/watermark.
Live playground & animated explainer: https://ruvnet.github.io/ai-text-watermark/ · Source: https://github.com/ruvnet/ai-text-watermark
As of August 2026 the EU AI Act asks AI providers to mark AI-generated text so it can be recognised later. But a paragraph is just words — there's nowhere to stamp a logo without wrecking the sentence. The mark has to hide inside the choices the model was already making: invisible to a reader, unmistakable to anyone with a secret key.
A language model writes one word at a time. At most positions, several next words are equally good:
"The weather today is cold and overcast / grey…"
A reader won't notice or care which one lands. (It would never pick "sugary" — that was never in the running.) The watermark only ever plays inside that tie. It never reaches for a strange word the model wouldn't have used anyway — so the writing quality is untouched.
Normally the model breaks a tie with a private roll of the dice — pure chance. Watermarking swaps the dice for something that looks just as random but isn't: a secret key plus the words that just came before decide which tied word to pick.
The Monopoly analogy. Imagine a game where, instead of rolling, every player reads the next digit of π for their move. The moves are still effectively random — the game plays out identically — but if you knew π, you could look back and prove the game had followed it. Watermarked text is that game.
Crucially, it isn't biased toward any one word. "overcast" might win here, "grey" the next time — it depends entirely on the key and the preceding words.
Later, someone with the same key re-walks the text and asks at every tie: is this the word the key would have chosen?
- In ordinary human writing: "sometimes, by chance."
- In watermarked text: "yes — far more often than luck allows."
Stack up enough of those little agreements and the odds of coincidence collapse. That's the whole detector — a confidence score that climbs as the text gets longer.
See it happen: the live playground generates a watermarked stream in your browser (real WebAssembly, nothing leaves the page) and shows the confidence land around a one-in-a-billion z-score — while a wrong key sits at zero.
- Short or factual text carries almost no mark. "Newton's masterwork was the Principia…" has exactly one right next word — no tie to hide in. Same for most code, and for a light proofread. Fewer choices → fainter signature.
- A wrong key sees nothing. The mark is specific to whoever made it, and it carries no information about you — not your identity, account, or chat. It answers one question only: was a keyed model likely involved? Not who, not when.
- It is not a way to launder AI text into looking human. This toolkit only writes and checks marks. If you legitimately need un-marked output of your own model, you simply don't apply the mark — you never strip a finished text.
| scheme | trade-off |
|---|---|
gumbel |
Provably changes the model's word odds by zero. The gentlest signature. |
tournament |
The SynthID method — a stronger mark for a whisper of extra bias. |
tournament_nd |
The middle path: unbiased on average, still firm. |
Plus two specialist detectors: one that survives editing and rearranging, and one that squeezes confidence out of very short passages.
The mark can carry more than one bit. Split a short payload — a model id, an author handle, a run tag — into bits, and encode each bit by which of two key-derived streams watermarks a block of tokens. To read it back, run the detector on each block under both keys and take the stronger; the blocks spell the message out. It needs only the key and block size — never the original text.
The project dogfoods this: the playground embeds and recovers the provenance payload ruvnet end-to-end, 100% of the bits. Same honest boundary as everything else — the payload is provenance (who/what/which run), never end-user identity, and it's applied at generation, not stamped onto finished prose.
For real serving, StreamProxy drops into a decode loop between the model and the emitted token. Give it a step's raw logits (what a serving stack produces) or a truncated top-k (ids, logprobs) set (what an OpenAI-compatible API returns); it applies temperature + top-k/top-p to match your sampler, watermarks the candidate set, and returns the token id to emit. Scratch buffers are reused, so the per-token cost is fixed and allocation-free after warmup — the mark rides the sampling you already do.
const { StreamProxy, detect } = require('ai-text-watermark');
const proxy = new StreamProxy({ key: 'my-secret', scheme: 'gumbel', temperature: 0.9, topK: 40, topP: 0.95 });
const out = [];
for (const logits of decodeSteps) out.push(proxy.pushLogits(logits)); // logits → watermarked token id
detect(Uint32Array.from(out), { key: 'my-secret', scheme: 'gumbel' }).isWatermarked(1e-6); // trueIt ships as a WebAssembly module. Node or browser, nothing to compile.
npm install ai-text-watermark # standalone name
# or: npm install @claude-flow/watermark (same package, canonical name)
# or, in Rust: cargo add ruflo-watermarkconst { Watermarker, detect } = require('ai-text-watermark');
// The watermarker sits between the model and the sampled token: you give it the
// model's candidate token ids + their probabilities each step; it returns which
// candidate to emit.
const wm = new Watermarker({ key: 'my-secret', scheme: 'gumbel' });
const out = new Uint32Array(600);
for (let i = 0; i < out.length; i++) out[i] = candidates[wm.step(candidates, probs)];
wm.free();
// Later, with the same key:
const r = detect(out, { key: 'my-secret', scheme: 'gumbel' });
console.log(r.zScore, r.isWatermarked(1e-6)); // strong signal, trueIn a browser it's the same API after one async init:
import { init, Watermarker, detect } from 'ai-text-watermark/web';
await init(); // auto-fetches the wasmDetectors return { zScore, pValue, log10P, scoredPositions, isWatermarked(alpha) }. Confidence grows with the number of low-stakes word choices — so give the detector length.
| what | where |
|---|---|
| Live playground + animated explainer | https://ruvnet.github.io/ai-text-watermark/ |
| Source, README, ADRs | https://github.com/ruvnet/ai-text-watermark |
| npm (standalone) | ai-text-watermark |
| npm (canonical) | @claude-flow/watermark |
| Rust crate | ruflo-watermark (crates.io) |
Design decisions are recorded as ADRs in the repo: the watermarking component (383), the bounded-evolution detector tuner (384), the ultra-low-latency proxy (385), multi-bit secret message (386), and the proposed gateway service + OAuth-gated generation (387/388).
Method: SynthID-Text (Dathathri et al., Nature 2024) and the Aaronson (2022) / Kuditipudi et al. (2024) distortion-free family. Provenance, not surveillance — a mark that says "a machine helped here," and nothing else.