Skip to content

Latest commit

 

History

24 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🔏 AI Text Watermark

Invisible, key-verifiable watermarks for LLM-generated text — in Rust + WebAssembly.

Generate a watermark that rides the model's own word choices, then detect it later with a secret key. SynthID-Text-style provenance for AI content — no extra tokens, no quality loss, Node and browser.

npm crates.io license wasm

▶ Live demo & playground · How it works · npm · crate


AI text watermarking marks machine-generated writing so it can be recognised later — a requirement of the EU AI Act (in force Aug 2026) that major providers, including Anthropic (SynthID-Text), are adopting. This library implements that method as a fast, embeddable WASM module and Rust crate, with an interactive playground.

Story + playground

Why watermark AI text?

You can't stamp a logo on a sentence. AI text watermarking hides an invisible, statistical signature in the low-stakes word choices a model already makes — undetectable to a reader, but provable to anyone holding the key. It answers one question: was a keyed model likely involved? It carries no user information and is not a way to hide that AI was used.

  • No quality loss, no extra tokens, no added latency — the mark lives in ties between equally-plausible words.
  • Key-verifiable — a wrong key sees nothing; the mark identifies the model/provider, never a person.
  • Node + browser via WebAssembly — nothing to compile.
  • Honest about limits — short/factual/code text carries little mark; confidence grows with length.
  • 🚫 No removal / laundering tool — this library only writes and checks marks.

Install

JavaScript / TypeScript (npm):

npm install @claude-flow/watermark
# or the standalone name (thin wrapper, same API):
npm install ai-text-watermark

Rust (crates.io):

cargo add ruflo-watermark

Quick start

Node

const { Watermarker, detect } = require('@claude-flow/watermark');

// You supply the model's candidate token ids + probabilities each step;
// the watermarker 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();

const r = detect(out, { key: 'my-secret', scheme: 'gumbel' });
console.log(r.zScore, r.isWatermarked(1e-6)); // strong signal, true

Browser / Deno / bundlers

import { init, Watermarker, detect } from '@claude-flow/watermark/web';
await init();                       // auto-fetches the wasm
const wm = new Watermarker({ key: 'my-secret', scheme: 'gumbel' });

Rust

use ruflo_watermark::{Watermarker, WatermarkConfig, WatermarkKey, Scheme, detect_gumbel};

let cfg = WatermarkConfig::new(WatermarkKey::from_bytes(b"my-secret"));
let mut wm = Watermarker::new(cfg, Scheme::Gumbel);
let idx = wm.step(&candidate_ids, &probs);         // emit one token
// … collect a sequence, then:
let r = detect_gumbel(&tokens, cfg);
assert!(r.is_watermarked(1e-6));

Ultra-low-latency proxy — watermark a live LLM stream

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 per-token cost is fixed and allocation-free after warmup — the mark rides the sampling you already do.

const { StreamProxy, detect } = require('@claude-flow/watermark');

// In front of a local serving stack (vLLM / llama.cpp / Candle): full-vocab logits.
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
proxy.free();

// …or in front of an OpenAI-compatible API that returns top_logprobs per token:
//   proxy.pushTopK(tokenIds, logprobs)   // watermark the returned candidate set

detect(Uint32Array.from(out), { key: 'my-secret', scheme: 'gumbel' }).isWatermarked(1e-6); // true

Same class in Rust (ruflo_watermark::StreamProxy) and in the browser (import { StreamProxy } from '@claude-flow/watermark/web'). It's a gateway front-end, not a new scheme — a proxy stream detects identically to a Watermarker stream.

Inflight analysis — MidStream

MidStream fuses generation and detection into one streaming pass: each token is watermarked and analyzed, so you know the watermark's strength while the text is still generating — no second detection scan. It's the online form of the same statistic (the live z-score converges exactly to a batch detect), plus redundancy and backpressure signals for a serving loop.

const { MidStream, detect } = require('@claude-flow/watermark');

const ms = new MidStream({ key: 'my-secret', scheme: 'gumbel', capacity: 64 });
for (const logits of decodeSteps) {
  const ev = ms.pushLogits(logits);   // { token, zScore, scored, log10P, novel, backpressure }
  emit(ev.token);
  if (ev.backpressure) throttle();     // consumer is behind
  if (ev.zScore > 6) markProvenanceConfirmed();  // enough watermark signal, live
  ms.ack(1);                           // consumer drained one
}

Inspired by ruvnet/midstream ("real-time LLM streaming with inflight analysis"); the WASM-safe analysis primitives are reimplemented in-core (design: ADR-389), while real QUIC transport belongs in the gateway tier (ADR-387).

How it works (60 seconds)

A model writes one word at a time, and at most steps several next words are equally good ("cold and overcast / grey"). Normally a tie is broken by a private dice roll. Watermarking keeps the odds identical but changes the source of that randomness: a secret key + the preceding words decide the winner — like playing a game with the digits of π instead of dice. Later, anyone with the key re-walks the text and checks how often it matched what the key would have chosen. Enough matches, and coincidence is ruled out.

▶ See the animated explainer & try it live →

Live playground

Schemes & detectors

scheme property
gumbel (default) Provably distortion-free — the model's word distribution is unchanged.
tournament SynthID-Text tournament — a stronger mark for a whisper of bias.
tournament_nd Non-distortionary on average (key-averaged, measured < 0.3% drift).
detector for
detect the scheme's standard detector
detectSelfSync indel-robust — survives edits, deletions, rearrangement
detectExact short text — exact p-values where the normal approximation misleads

Every detection returns { zScore, pValue, log10P, scoredPositions, isWatermarked(alpha) }.

Provenance & secret message (multi-bit)

Beyond the single "is this watermarked?" bit, you can embed a short provenance payload — a model id, a run tag, an author handle — inside the same invisible mark. Each message bit is carried by which of two key-derived streams watermarks a block of tokens; extraction detects each block under both keys and takes the stronger, spelling the payload back. It needs only the key and block size — never the original text or candidate sets.

This project dogfoods it: the live playground embeds and recovers the provenance payload ruvnet entirely in your browser (100% recovery on the demo). Capacity trades against robustness — budget ~90 tokens per bit for a clean read, and layer error-correction for editing resilience.

Honest boundary: provenance is applied at generation, not stamped onto finished prose. You watermark a token stream your model produces; you never post-hoc mark someone's existing text. The payload carries provenance (a model / author / run tag), never end-user identity. Design: ADR-386.

Use cases

  • EU AI Act / transparency compliance — mark model output so it's later recognisable as AI-assisted.
  • Content provenance & platform trust & safety — flag likely-AI passages at scale, cheaply (detection scans at ~10M tokens/sec).
  • Dataset hygiene — detect and exclude your own model's output from training data to avoid model collapse.
  • Leak / policy attribution — check whether a passage rode your key (a wrong key sees nothing).
  • Watermark research & red-teaming — a built-in robustness harness measures how detection degrades under editing (Rust crate).
  • Authorized un-marked generation — a governed, audited path to not apply the mark for exempt content (Rust crate; never a post-hoc stripper).

Benefits

  • Fast — single-digit microseconds per token to generate; ~10M tokens/sec to detect.
  • 🪶 Tiny & portable — a 37 KB WebAssembly module, dependency-free core.
  • 🔬 Honest & tested — 46 Rust tests; distortion-freeness, key-specificity, length/entropy scaling and robustness all measured, not asserted.
  • 🧩 Model-agnostic — logits in, watermarked token out; wire it to any tokenizer/model.

Packages

what where
npm (Node + browser WASM) @claude-flow/watermark · packages/watermark
Rust crate ruflo-watermark · crates/ruflo-watermark
Live site (story + playground) docs/ → GitHub Pages

Design decisions (ADRs)

Method & prior art

SynthID-Text (Dathathri et al., Nature 2024), and the distortion-free family of Aaronson (2022) and Kuditipudi et al. (2024). See ADR-383 for the full comparison.

License

MIT © rUv


Keywords: AI text watermark · LLM watermarking · SynthID · SynthID-Text · AI content detection · AI provenance · EU AI Act · text watermark · WASM · Rust · distortion-free watermark · Gumbel watermark · content credentials

About

Invisible, key-verifiable watermarks for AI/LLM-generated text — SynthID-Text-style, in Rust + WebAssembly (Node + browser). Live playground included.

Topics

Resources

Stars

27 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages