Skip to content
Friendly disclaimer: flozi00 TechHub is a solo side-project next to a full-time job β€” personal learning notes, no official statements. Verify critical steps yourself.

Gumbel Watermarking Finally Shipped β€” and the Compliance Asymmetry Nobody Priced

A source-based comparison of Anthropic SynthID-Text and vLLM Gumbel watermarking, their statistical limits, and the provider marking duty under Article 50(2) of the EU AI Act.

16 min readflozi00
aimachine-learningllmwatermarkingsecuritycomplianceregulation
Skip to the full article
Watch Β· explore Β· understand

A secret key connects sampling to a statistical test

See where a keyed Gumbel watermark enters generation and what its detector can conclude.

AI-assisted learning visualization Β· schematic article explanation

Step 1 / 4Start with a distribution

Ordinary Gumbel-max sampling adds independent noise to log probabilities and selects the largest result. Under its assumptions, this samples the original categorical distribution.

AlgorithmCurrent stepSelect a block to explain it
Next-token scores
The model assigns probabilities to possible continuations.
Keyed Gumbel selection
Combine scores and noise to choose a token.
  1. Next-token scores Keyed Gumbel selection
    model scores
What to remember

The key makes a statistical signal testable; it does not make every text reliably attributable.

This follows the article’s keyed Gumbel path, not SynthID’s different tournament algorithm. Its toy detection rates are not production guarantees or an Article 50 determination.

Terms and the complete walkthrough
Gumbel-max
Sampling by adding Gumbel noise to log probabilities and selecting the maximum.
Pseudorandom function
A keyed procedure that produces reproducible random-looking values.
False positive
A detector reports a mark in an unmarked sample.
  1. Start with a distribution

    Ordinary Gumbel-max sampling adds independent noise to log probabilities and selects the largest result. Under its assumptions, this samples the original categorical distribution.

  2. Make the randomness reproducible

    The keyed construction derives pseudorandom noise from a key and context. Claims about preserving distributions need its stated randomness and repeated-context assumptions.

  3. Look for agreement with the key

    A detector recreates the expected signal from tokenized text and the matching key. Short or heavily rewritten passages can carry less usable evidence.

  4. Read a verdict with its limits

    The threshold trades missed signals against false alarms. A negative test does not establish human origin, and a positive test does not prove authorship or legal compliance.

All connections

  • Next-token scores β†’ Keyed Gumbel selection: model scores
  • Key and recent context β†’ Keyed Gumbel selection: sampling noise
  • Keyed Gumbel selection β†’ Generated text: chosen token
  • Generated text β†’ Recompute the key signal: token sequence
  • Key and recent context β†’ Recompute the key signal: matching key
  • Recompute the key signal β†’ Statistical evidence: score and threshold
Set your own pace. Pause at any time or select a step directly.

Text watermarking for LLMs spent 2022 to 2024 as a research topic with a famous gap: the inventor could not get it deployed (Scott Aaronson prototyped his scheme at OpenAI in fall 2022, and OpenAI leadership declined to ship it). In August 2026 that gap closed from two directions at once. On the closed side, Anthropic announced that Claude models generate watermarked text, using "a version of the SynthID-Text approach published by Google DeepMind in a Nature paper in 2024"1 β€” the Dathathri et al. paper that is, to date, the only generative text watermark with a production deployment at scale (Gemini, and now Claude)2. On the open-engine side, vLLM ships a keyed Gumbel-max watermarking mode with a detector that runs without model weights3. Two distinct keyed text-watermark constructions, landing inside the same legal window: Article 50(2) of the EU AI Act, which has applied since 2 August 2026 and requires providers of generative systems to mark outputs in a machine-readable format45. Anthropic says the quiet part plainly: "we're applying watermarking globally at launch because we don't yet have a durable way to scope it by region"1. The watermark exists because of a European statute, and it runs everywhere.

This guide is not about the policy debate. It checks the math and the silicon path it runs on: what the Gumbel-max trick actually is and why it is an exact sampler, how a secret key turns sampling noise into a detectable signal without moving the output distribution, what detection power looks like measured rather than asserted β€” and how an operator can assess marking of outputs from a particular deployment. Open weights alone do not determine whether a given deployment meets Article 50(2). Every number below is either quoted from a primary source or measured in the cells and labeled as such.

Article 50(2) requires providers of covered generative AI systems to mark outputs in a machine-readable format and make them detectable as artificially generated or manipulated. Technical solutions must be effective, interoperable, robust and reliable as far as technically feasible, taking account of content limitations, implementation costs and the state of the art. The paragraph also includes exceptions for standard assistive editing, input that is not substantially altered, and certain legally authorized law-enforcement uses.4

Three details of the provision matter here. First, "machine-readable format and detectable" describes the required output property, not a mandate to use a particular keyed statistical watermark. Metadata and other technical methods may also be relevant under Article 50(2). Second, the qualifier "as far as this is technically feasible ... taking into account the specificities and limitations of various types of content" matters for low-entropy text, where statistical watermark detection can be weak. It is not a categorical legal exemption for all such text. Third, the standard-editing exemption depends on the system's function and whether it substantially alters input; it cannot be decided from a weak watermark signal alone.4

The timing: the AI Act entered into force on 1 August 2024 and, per Article 113, "shall apply from 2 August 2026"5. The Digital Omnibus, Regulation (EU) 2026/1744 of 8 July 2026, added a transitional period for exactly this duty: recital 38 describes the four-month transitional now codified as Article 111(4) β€” "providers who have already placed their systems on the market before the 2 August 2026" get until 2 December 20266 β€” that is, marking for pre-existing generative systems must be in place by 2 December 2026, while systems placed on the market after 2 August 2026 owed it immediately. Anthropic's rollout timeline follows this shape exactly: new models watermarked at launch, and "the EU law includes a transition period for Anthropic models launched before August 2, 2026, and we're working to add watermarking for those models as well"1. Anthropic, with "around 190 total signatories", signed the EU Code of Practice on Transparency of AI-Generated Content in July 20261 β€” the instrument the Commission's AI Office uses to operationalize detection-and-labelling duties under Article 50(7).

2. The Gumbel-max trick: exact sampling from additive noise

Every autoregressive LLM decoder ends a forward pass with a distribution over the vocabulary, and sampling is the step that turns that distribution into one token. The classical method is inverse-CDF sampling: draw a uniform random number and walk the cumulative distribution. The Gumbel-max trick is the less obvious equivalent: if you add independent Gumbel-distributed noise to each log-probability and take the argmax, the winner is exactly distributed as the categorical distribution. That is the mechanism of the Gumbel construction; SynthID-Text uses tournament sampling instead. Let us verify Gumbel-max rather than assert it. The Gumbel(0,1) distribution has CDF F(x) = exp(-exp(-x)); if G.i are iid Gumbel, then P(argmax of log p.i + G.i = k) = p.k, provable from the max-stability of the Gumbel family (the max of iid Gumbels shifted by log n is again Gumbel β€” the same property that makes the trick work).

python
# Cell 1: Gumbel-max trick vs naive CDF inversion, 200,000 draws
import math, random
 
V = 4                                  # tiny 4-token vocabulary
probs = [0.55, 0.25, 0.15, 0.05]       # the model's next-token distribution
cum = []
s = 0.0
for p in probs:
    s += p
    cum.append(s)
cum[-1] = 1.0                          # guard against float drift
 
def sample_cdf(r):                     # naive inversion: first i with r < cum[i]
    for i, c in enumerate(cum):
        if r < c:
            return i
    return V - 1
 
def gumbel(rng):                       # G_i ~ Gumbel(0,1) via inverse CDF:
    u = rng.random()                   # F(x) = exp(-exp(-x))  =>  x = -ln(-ln u)
    return -math.log(-math.log(u))
 
def sample_gumbel_max(rng):            # argmax_i ( log p_i + G_i )
    best, best_score = -1, -math.inf
    for i in range(V):
        score = math.log(probs[i]) + gumbel(rng)
        if score > best_score:
            best, best_score = i, score
    return best
 
N = 200000
rng = random.Random(2026)
c_cnt = [0] * V
g_cnt = [0] * V
for _ in range(N):
    c_cnt[sample_cdf(rng.random())] += 1
    g_cnt[sample_gumbel_max(rng)] += 1
 
print("Empirical frequencies over", N, "draws (analytic in last column)")
print("")
print(f"{'token':>6} {'analytic':>10} {'CDF inv':>10} {'Gumbel-max':>11}")
for i in range(V):
    print(f"{i:>6} {probs[i]:>10.4f} {c_cnt[i]/N:>10.4f} {g_cnt[i]/N:>11.4f}")
print("")
print("max |CDF - analytic| =", f"{max(abs(c_cnt[i]/N - probs[i]) for i in range(V)):.5f}")
print("max |Gum - analytic| =", f"{max(abs(g_cnt[i]/N - probs[i]) for i in range(V)):.5f}")
 
# Max-stability check: max of two iid Gumbels is Gumbel(0,1) again,
# up to the shift log(2):  F_max(x) = F(x)^2 = exp(-2 exp(-x))
#                                        = exp(-exp(-(x - ln 2)))
rng2 = random.Random(7)
m = [0] * 5                             # histogram bins over [-1, 3)
hits = 0
TOTAL = 100000
for _ in range(TOTAL):
    x = max(gumbel(rng2), gumbel(rng2))
    if 0.0 <= x < 1.0:
        hits += 1
b = hits / TOTAL
a = math.exp(-2 * math.exp(-1)) - math.exp(-2)  # F_max(1)-F_max(0) = F(1)^2 - F(0)^2
print("")
print(f"P(0 <= max of 2 Gumbels < 1): empirical {b:.4f} vs exact exp(-2e^-1)-exp(-2) = {a:.4f}")
text
Empirical frequencies over 200000 draws (analytic in last column)
 
 token   analytic    CDF inv  Gumbel-max
     0     0.5500     0.5506      0.5495
     1     0.2500     0.2507      0.2510
     2     0.1500     0.1490      0.1498
     3     0.0500     0.0497      0.0498
 
max |CDF - analytic| = 0.00101
max |Gum - analytic| = 0.00100
 
P(0 <= max of 2 Gumbels < 1): empirical 0.3446 vs exact exp(-2e^-1)-exp(-2) = 0.3438

(Our own measurement, /usr/bin/python3.) Both samplers land within 0.001 of the analytic distribution over 200,000 draws, and the max-stability interval matches its closed form to three decimals. "Exact" is doing honest work here: adding keyless Gumbel noise changes nothing about the induced token distribution. So where does a watermark come from? Change the source of the noise, not its distribution.

3. Keying the noise: Aaronson (2022), SynthID-Text (2024), vLLM (2026)

Aaronson's 2022 proposal β€” "as far as I know, the first LLM watermarking proposal", as he puts it looking back on Anthropic's deployment β€” is the minimal move: replace the iid Gumbel noise with pseudorandom noise derived from a secret key and the recent context. Then the "random" choices become deterministic given the key, a holder of the key can re-derive every noise value and test agreement, and an outsider sees an unchanged distribution. As Anthropic summarizes the family: "the watermark only changes the source of the randomness used to pick among words"1. Aaronson reports that Hendrik Kirchner built and tested the prototype at OpenAI, that OpenAI leadership decided against deploying it, and that Christ, Gunn and Zamir subsequently strengthened it toward cryptographic indistinguishability7. A parallel 2023 lineage, Kirchenbauer et al.'s green/red-list watermark (arXiv:2301.10226, ICML 2023), took a cruder but cheaper path β€” softly promoting a hash-selected "green list" β€” which does shift the distribution slightly8.

The production-grade descendants key the randomness in two different ways.

SynthID-Text (Dathathri et al., Nature 634, 2024) β€” Tournament sampling. The random seed generator uses the sliding-window method: "the random seed is a hash of the most recent H tokens ... along with the watermarking key", with H = 4. The seed feeds m independent pseudorandom watermarking functions which assign each candidate token a g-value (a 0 or 1, i.e. Bernoulli). Generation then runs a knockout tournament: sample M = 2^m candidate tokens from the model's own distribution, pair them up, and in each pair "the token with the higher score under g_1 is selected ... any ties are broken randomly"; winners are re-paired and scored under g_2, down through g_m; the final winner becomes the output token2. The paper's default is m = 30, so 2^30 candidates per step in principle (their integration with speculative sampling is what makes that affordable). Detection needs only the tokenized text and the key: recompute every seed from the sliding window, score the emitted tokens' g-values (a simple scoring function being the plain mean across steps and layers; the paper's default is its Bayesian scoring function), and compare against a threshold. The paper proves single-token non-distortion for a specified two-competitor tournament configuration; that result does not follow merely from drawing candidates from the model. Google also reported no statistically significant difference in user ratings across nearly 20 million live Gemini responses, a quality measure rather than a direct proof of identical token distributions.2

vLLM β€” keyed Gumbel-max. The open engine takes Aaronson's construction essentially directly. Engine startup accepts --watermark-config \{"algorithm":"gumbel","key":42\}; "Gumbel-max derives a deterministic pseudorandom value from the key, prior token context, and every candidate token, then uses the resulting Gumbel noise for categorical sampling" β€” a Philox PRF keyed by the secret and the previous context_width = 4 tokens39. Greedy requests (temperature 0) bypass watermarking entirely; watermarked generations honor repeated-context deduplication, with the docs citing section G.3 of the SynthID-Text supplementary materials for the single-sequence non-distortion argument β€” the same concern the Nature paper handles by falling back to ordinary sampling when a context window repeats3. Detection, like SynthID-Text's, is weights-free: GumbelWatermarkDetector(key=42, prf="philox").detect(token_ids) β€” a GumbelWatermarkDetector constructed with key 42 and the Philox PRF returns a p-value and verdict on the token-ID list, so a downstream party can verify a stream without knowing the model, only the key3.

These are different algorithms that share a keyed, distribution-preserving design goal. The exact categorical identity holds for independent random Gumbel variables; a fixed key makes pseudorandom choices deterministic for a fixed context, so claims about matching the unmarked distribution require the stated randomness and security assumptions. Now measure a toy detector.

4. Detection power, measured β€” and Anthropic's limits section, priced

The experiment below (our own measurement, /usr/bin/python3, fixed seeds) implements a minimal keyed-Gumbel watermark over a 32-token flat distribution: at each position, a keyed PRF (HMAC-SHA256 over the key, the four previous tokens, the position, and the candidate token) supplies the Gumbel noise, and the argmax wins. The detector recomputes the same noise and scores the emitted token against the position's noise distribution β€” cell 2 against the position mean, cell 3 against the position's maximum over the vocabulary β€” zero when the key's favorite won, and negative on average for unwatermarked text. The null mean is not mysterious: E[one Gumbel] is the Euler–Mascheroni constant Ξ³ β‰ˆ 0.5772, and E[max of n iid Gumbels] is Ξ³ + ln n by max-stability β€” so for a uniformly chosen token out of V = 32, the chosen-minus-max gap has expectation exactly -ln 32 β‰ˆ -3.4657, which is what the calibration below measures (-3.4621/-3.4582/-3.4663 at N = 50/200/500).

python
# Cell 2: keyed-PRF Gumbel sampling vs unwatermarked streams
import math, hmac, hashlib
 
KEY = b"secret-deployment-key"
CTX = 4
 
def gumbel_from(prf_value):            # uniform u in (0,1) -> Gumbel(0,1)
    u = (prf_value % 1000003 + 1) / 1000004.0
    return -math.log(-math.log(u))
 
def prf(items):                         # keyed PRF: HMAC-SHA256(key, ctx || token)
    msg = b"|".join(str(t).encode() for t in items)
    d = hmac.new(KEY, msg, hashlib.sha256).digest()
    return int.from_bytes(d[:8], "big")
 
V = 32
probs = [1.0 / V] * V                   # flat: every choice is "low-stakes"
 
def gen_watermarked(n, seed):
    stream, context = [], list(range(CTX))
    for pos in range(n):
        context_seed = tuple(context)
        best, best_score = -1, -math.inf
        for tok in range(V):
            score = math.log(probs[tok]) + gumbel_from(prf(context_seed + (pos, tok)))
            if score > best_score:
                best, best_score = tok, score
        stream.append(best)
        context = context[1:] + [best]
    return stream
 
def gen_plain(n, seed):
    import random
    rng = random.Random(seed)
    return [rng.randrange(V) for _ in range(n)]
 
def logl_gap(stream):                  # detector: sum over positions of
    gap, hits = 0.0, 0                  # log p_PRF(winner) - log(1/V) statistic
    context = list(range(CTX))
    for pos, tok in enumerate(stream):
        scores = [gumbel_from(prf(tuple(context) + (pos, t))) for t in range(V)]
        gap += scores[tok] - sum(scores) / V
        context = context[1:] + [tok]
    return gap
 
for N in (200, 500):
    wm = gen_watermarked(N, 1)
    pl = gen_plain(N, 99)
    s_wm = logl_gap(wm)
    s_pl = logl_gap(pl)
    print(f"N={N}: watermarked stream score S={s_wm:9.2f} | plain random stream S={s_pl:9.2f}")
text
N=200: watermarked stream score S=   699.60 | plain random stream S=    -8.64
N=500: watermarked stream score S=  1727.02 | plain random stream S=   -49.56

The watermarked stream accumulates roughly N Γ— ln 32 β‰ˆ 693 / 1733 units of excess score (observed: 699.60 / 1727.02); the unwatermarked streams hover around zero (-8.64, -49.56). At a flat 32-way choice the signal is enormous β€” this is the best case, every position maximally "low-stakes". The real question is what happens when marking is sparse, and that is exactly where Anthropic's own limits section lives. Run the same machinery with only a fraction of positions watermarked β€” the rest standing in for the choice-free stretches Anthropic describes ("Isaac Newton's most famous work was called Principia" β€” there is only one right continuation, "so the watermark would have nothing to act on") β€” and calibrate detection against 200 unwatermarked streams per sample size:

python
# Cell 3: sparse marking, short samples, thresholds, false positives
import math, hmac, hashlib, random
 
KEY = b"secret-deployment-key"
CTX = 4
V = 32
 
def prf(items):                        # keyed PRF, same as cell 2
    msg = b"|".join(str(t).encode() for t in items)
    d = hmac.new(KEY, msg, hashlib.sha256).digest()
    return int.from_bytes(d[:8], "big")
 
def gumbel_from(prf_value):
    u = (prf_value % 1000003 + 1) / 1000004.0
    return -math.log(-math.log(u))
 
def gen(n, seed, mark_frac=1.0):
    # mark_frac = fraction of positions watermarked; the rest are plain random
    # picks, standing in for choice-free stretches ("Principia ... Mathematica":
    # the watermark has nothing to act on because there is nothing to choose).
    rng = random.Random(seed ^ int(mark_frac * 10000))
    stream, context = [], list(range(CTX))
    for pos in range(n):
        if rng.random() < mark_frac:
            cs = tuple(context)
            best, bs = -1, -math.inf
            for tok in range(V):
                sc = gumbel_from(prf(cs + (pos, tok)))
                if sc > bs:
                    best, bs = tok, sc
        else:
            best = rng.randrange(V)
        stream.append(best)
        context = context[1:] + [best]
    return stream
 
def stat(stream):                      # detector statistic: mean over positions of
    context = list(range(CTX))         # PRF-Gumbel(chosen) minus the position max
    s = 0.0                            # over the vocabulary; 0 when the key's
    for pos, tok in enumerate(stream):  # favorite won, ~ -3.47 on average for
        scores = [gumbel_from(prf(tuple(context) + (pos, t))) for t in range(V)]
        s += scores[tok] - max(scores) # unwatermarked picks (E[max of 32] - E[Gumbel])
        context = context[1:] + [tok]
    return s / len(stream)
 
# Calibrate the unwatermarked (null) distribution empirically, per sample size:
cal = {}
for n in (50, 200, 500):
    vals = [stat(gen(n, 70000 + s, mark_frac=0.0)) for s in range(200)]
    mu = sum(vals) / len(vals)
    sd = (sum((x - mu) ** 2 for x in vals) / (len(vals) - 1)) ** 0.5
    cal[n] = (mu, sd)
 
def zval(stream):                      # positive z = consistent with the key
    mu, sd = cal[len(stream)]
    return (stat(stream) - mu) / sd
 
print("Detection z vs empirical plain null (positive = consistent with the key):")
for n, sp in ((50, 5101), (200, 5202), (500, 5303)):
    print(f"  N={n:>4}: plain z={zval(gen(n, sp, mark_frac=0.0)):+8.2f}   fully marked z={zval(gen(n, sp, mark_frac=1.0)):+8.2f}")
 
print("")
print("Null calibration (200 plain streams per N): the null sharpens as 1/sqrt(N),")
print("so a few marked positions that drown at N=50 can still separate at N=500:")
for n in (50, 200, 500):
    mu, sd = cal[n]
    print(f"  N={n:>4}: mu={mu:.4f} sd={sd:.4f}")
 
print("")
print("Sparse marking: mean z over 20 trials per cell (f = fraction of positions marked)")
print(f"{'f':>5} {'N=50':>8} {'N=500':>8}")
for f in (1.0, 0.5, 0.2):
    z50 = sum(zval(gen(50, 6000 + s, mark_frac=f)) for s in range(20)) / 20
    z500 = sum(zval(gen(500, 6000 + s, mark_frac=f)) for s in range(20)) / 20
    print(f"{f:>5.1f} {z50:>+8.1f} {z500:>+8.1f}")
 
print("")
print("Proofreading case, N=200: only 5% of positions are actually the editor's")
print("choices (grammar fixes in a human text); mean z over 20 trials:")
zp = sum(zval(gen(200, 8000 + s, mark_frac=0.05)) for s in range(20)) / 20
print(f"  f=0.05 -> mean z = {zp:+.2f}  (compare f=1.0 at N=200 above; even a")
print(f"  loose tau=+2 would call most such texts unwatermarked)")
 
print("")
print("Threshold sweep on the z-scale, N=500, f=0.2 (100 marked positions):")
for tau in (2, 3, 4):
    tpr = sum(1 for s in range(100) if zval(gen(500, 9000 + s, mark_frac=0.2)) >= tau) / 100
    fpr = sum(1 for s in range(2000) if zval(gen(500, 80000 + s, mark_frac=0.0)) >= tau) / 2000
    print(f"  tau=z {tau}: TPR={tpr:.3f}  FPR={fpr:.4f}")
 
print("")
print("Tail math -- false positives if the null were exactly normal (it is not):")
for zz in (2, 3, 4, 5):
    print(f"  threshold at z=+{zz}: one-sided normal tail P = {0.5 * math.erfc(zz / math.sqrt(2)):.2e}")
text
Detection z vs empirical plain null (positive = consistent with the key):
  N=  50: plain z=   -1.33   fully marked z=  +13.75
  N= 200: plain z=   +0.07   fully marked z=  +28.35
  N= 500: plain z=   +0.70   fully marked z=  +46.95
 
Null calibration (200 plain streams per N): the null sharpens as 1/sqrt(N),
so a few marked positions that drown at N=50 can still separate at N=500:
  N=  50: mu=-3.4621 sd=0.2518
  N= 200: mu=-3.4582 sd=0.1220
  N= 500: mu=-3.4663 sd=0.0738
 
Sparse marking: mean z over 20 trials per cell (f = fraction of positions marked)
    f     N=50    N=500
  1.0    +13.8    +47.0
  0.5     +6.9    +23.7
  0.2     +3.0     +9.2
 
Proofreading case, N=200: only 5% of positions are actually the editor's
choices (grammar fixes in a human text); mean z over 20 trials:
  f=0.05 -> mean z = +1.15  (compare f=1.0 at N=200 above; even a
  loose tau=+2 would call most such texts unwatermarked)
 
Threshold sweep on the z-scale, N=500, f=0.2 (100 marked positions):
  tau=z 2: TPR=1.000  FPR=0.0310
  tau=z 3: TPR=1.000  FPR=0.0030
  tau=z 4: TPR=1.000  FPR=0.0000
 
Tail math -- false positives if the null were exactly normal (it is not):
  threshold at z=+2: one-sided normal tail P = 2.28e-02
  threshold at z=+3: one-sided normal tail P = 1.35e-03
  threshold at z=+4: one-sided normal tail P = 3.17e-05
  threshold at z=+5: one-sided normal tail P = 2.87e-07

Read the table against Anthropic's limitations section, clause by clause β€” this is our measurement, not their internal data, but the shapes they concede are exactly the shapes the math produces:

  • "Detecting a watermark also doesn't work well on small samples, where there are fewer word choices and thus less information to go on"1. In the cells: at full marking the signal is overwhelming even at N = 50 (z = +13.75). It is not raw length that kills detection β€” it is length times marked choice density. At f = 0.2, N = 50 lands at a mean z of +3.0, marginal against a tau = +4 rule; stretch the same sparse stream to N = 500 and z climbs to +9.2. "As a passage increases in length, confidence about Claude's involvement increases too"1 β€” that sentence is a 1/sqrt(N) claim, and the null calibration row shows the mechanism: sd shrinks 0.2518 to 0.0738 between N = 50 and N = 500.
  • "Watermarking is sparser on factual passages where there are fewer choices that can be made without decreasing the accuracy of the text"1. The f column is that quote as arithmetic. The Nature paper states it in information-theoretic terms: "Tournament sampling performs better when there is more entropy in the LLM distribution, and is less effective when there is less entropy"2.
  • Proofreading: "because nearly all the words are the person's, there's very little (if anything) for the watermark to attach to"1. At f = 0.05 (a lightly edited human text, N = 200), mean z = +1.15 β€” inside the plain-text noise band, and note that this is the case Article 50(2) itself exempts from the marking duty ("assistive function for standard editing")4. The statute and the statistics agree.
  • Light edits versus rewrites: "Light editing probably won't remove the watermark completely; a complete rewrite where every word is replaced will"1. Light edits perturb the context windows locally; key hits β€” positions where the emitted token still matches the recomputed PRF noise structure β€” survive because most context windows and their winners are untouched. A full rewrite re-rolls every token under different noise (or no noise), and the correlation with your key decorrelates to zero. There is no cliff; the signal decays with the fraction of surviving marked choices, exactly the f column run backwards.
  • And the ambiguity the detector genuinely cannot resolve: "A watermark can only determine that Claude was likely involved with the content at some point. It cannot distinguish 'Claude wrote this' from 'Claude heavily edited this'"1.

The threshold sweep adds the compliance-relevant trade: at 100 marked positions out of 500, moving tau from z = 2 to 4 pushes FPR from 3.10% to 0.00% (empirically, on 2000 null streams) at unchanged TPR = 1.000 in this favorable toy. The normal-tail row is the planner's intuition line β€” a z = 4 rule has a 3.17e-05 false-positive rate if the null is normal, and the practical lesson of calibrating empirically is that you should never assume it is.

5. The compliance asymmetry nobody priced

Article 50(2) obliges providers. Marking is a provider-side property, and it splits the market unevenly across the open/closed boundary.

Closed providers can deliver it, unilaterally. Anthropic holds the weights, the endpoint, and the key. It applied watermarking globally β€” "Watermarking will be applied at the model level, which means it will be present no matter which Claude product or surface the text comes from", as it told press, but a provider still needs to assess whether its technical measure meets Article 50(2)'s effectiveness, interoperability, robustness and reliability criteria as far as technically feasible. Detection access is where it exercises discretion: the detection API is "in private preview", available to "eligible organizations as required under EU law" β€” regulators, law enforcement, media, fact-checkers, researchers β€” plus obligated enterprises, "and we plan to expand access over time"1. The mark is universal; the proof of the mark is gated. That is a policy choice the statute permits β€” Article 50(2) requires outputs be "detectable", not that everyone be handed the detector β€” but it converts a transparency instrument into an institutional one.

Open-weights deployments face the mirror image, in three parts.

  1. A watermark on open weights is a courtesy, not a guarantee. Once weights are downloadable, the provider has marked nothing about what anyone else serves with them. vLLM's watermarks are real and well-built, but they key to the deployer's config; a second deployment of the same weights with --watermark-config omitted, or a fine-tune on top, regenerates under different noise or none. Provider status and obligations depend on who places a particular AI system on the market or puts it into service; release of model weights alone does not settle the legal role of every downstream operator. (The AI Act's open-source licence carve-out in Article 2(12) does not rescue this: it explicitly keeps Article 50 obligations intact for systems that fall under it4.)
  2. Conversely, the deployer CAN prove provenance of their own stream. This is the under-priced upside on the open side. Because detection only needs the key and the tokenizer, anyone operating a vLLM endpoint can keep the watermark on, publish commitments, and produce detector results supporting that a given output was generated through their configured watermark; a positive statistical result is not a cryptographic proof of the serving infrastructure, and text can also be put in a signed file31. Open serving cannot guarantee marking downstream, but it can provide evidence about text generated by its configured endpoint β€” which covers a real slice of Article 50(2) compliance for deployers who ARE providers of their wrapped product.
  3. Nobody can prove absence. A negative watermark verdict proves only "not detected with this key" β€” on short texts the honest verdict is genuinely weak (Section 4), and an adversary who paraphrases through any unwatermarked model strips the signal anyway, as Aaronson concedes matter-of-factly about his own scheme's family7. So the asymmetry cuts both ways: closed endpoints generate marks that are strong but only testable through a gatekeeper; open deployments generate marks that anyone with the key can test, but that bind no one downstream.

That is the asymmetry the market has not priced: Article 50(2) mandates a property that is cheap and robust exactly where the provider already controls the entire path from logits to bytes, and merely available β€” opt-in, key-lost-on-fine-tune, strip-by-paraphrase β€” everywhere else. The Code of Practice had roughly 190 signatories by late July 2026, according to the Commission.10 For the open ecosystem, the same statute that created vLLM's watermarking PR budget also created a duty the flagship open deployments cannot fully perform for their downstream users β€” and compliance must be assessed for each provider and system.

6. Verdict

What these techniques offer, within their documented assumptions: a machine-testable statistical signal in sufficiently long and suitable text. The Gumbel-max math is exact (cell 1), the keyed version separates cleanly even at modest sample sizes (cells 2-3: z = +28.35 at N = 200 fully marked; TPR = 1.000 at FPR = 0.0030 with 100 marked positions), the live-test evidence at Gemini scale β€” nearly 20 million responses with no detectable quality delta β€” is the strongest deployment-viability data the field has2, and Anthropic says its implementation does not add tokens or user-identifying information.1 Its latency and real-world detection limits require separate measurement.

What it is not: provenance. It cannot attribute, individuate, or survive a motivated rewriter, and it degrades exactly where texts are short, factual, or barely machine-touched β€” the three cases a compliance program cares about most. "What is the likelihood this was partly written by Claude?" is the only question Anthropic claims their key answers1; the honest answer is a monotone function of length times choice-density, and the three cells above are that function, computed.

A provider controlling an endpoint can apply a marking policy consistently there. Operators using open weights can do so on their own endpoints as well, but neither provider can guarantee marking of outputs generated independently by another operator. Watermarking shipped. The asymmetry is shipping with it.

7. Sources

Footnotes

  1. Anthropic: How Claude's text watermarking works, August 14, 2026 (updated September 1, 2026), https://www.anthropic.com/news/claude-text-watermark. All quotes verbatim from the live page, fetched September 25, 2026. The press-level "no matter which Claude product or surface" phrasing is Anthropic's own, as reported by PCMag on the announcement. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15

  2. Sumanth Dathathri, Abigail See, Sumedh Ghaisas, Po-Sen Huang, Rob McAdam, Johannes Welbl, Vandana Bachani, Alex Kaskasoli, Robert Stanforth, Tatiana Matejovicova, Jamie Hayes, Nidhi Vyas, Majd Al Merey, Jonah Brown-Cohen, Rudy Bunel, Borja Balle, Taylan Cemgil, Zahra Ahmed, Kitty Stacpoole, Ilia Shumailov, Ciprian Baetu, Sven Gowal, Demis Hassabis, Pushmeet Kohli: Scalable watermarking for identifying large language model outputs, Nature 634, 818-823, October 23, 2024, DOI 10.1038/s41586-024-08025-4, full text via PubMed Central PMC11499265. Tournament-sampling description, H = 4, m = 30, g-values, scoring functions, and the 20-million-response live experiment are from the paper's Main and Methods sections, fetched September 25, 2026. ↩ ↩2 ↩3 ↩4 ↩5

  3. vLLM documentation: Text watermarking, https://docs.vllm.ai/en/latest/features/watermarking, part of the work tracked in vLLM RFC #53916, fetched September 25, 2026. Flag names, defaults (context_width = 4, Philox PRF), greedy bypass, context deduplication, and the detector API are quoted from the live docs page. ↩ ↩2 ↩3 ↩4 ↩5

  4. Regulation (EU) 2024/1689 (AI Act), Article 50(2) and Article 2(12), consolidated text CELEX 02024R1689 of July 27, 2026, https://eur-lex.europa.eu/eli/reg/2024/1689/oj. Article 50(2) paraphrased. ↩ ↩2 ↩3 ↩4 ↩5

  5. Regulation (EU) 2024/1689, Article 113: entry into force on the twentieth day after publication (entered into force August 1, 2024); "It shall apply from 2 August 2026." ↩ ↩2

  6. Regulation (EU) 2026/1744 (Digital Omnibus on AI) of July 8, 2026, published OJ July 24, 2026, recital 38: the four-month transitional period for Article 50(2) marking duties, scoped to "providers who have already placed their systems on the market before the 2 August 2026" β€” marking for such systems is due by 2 December 2026. https://eur-lex.europa.eu/eli/reg/2026/1744/oj ↩

  7. Scott Aaronson: Anthropic's LLM watermarking, Shtetl-Optimized, August 22, 2026, https://scottaaronson.blog/?p=10032, plus the 2022 materials Watermarking of GPT Outputs (talk slides with Hendrik Kirchner, https://www.scottaaronson.com/talks/watermark.ppt), and the December 10, 2022 TechCrunch report of his UT Austin lecture. ↩ ↩2

  8. John Kirchenbauer, Jonas Geiping, Yuxin Wen, Jonathan Katz, Ian Miers, Tom Goldstein: A Watermark for Large Language Models, arXiv:2301.10226, ICML 2023. Green-list framework and the "negligible impact on text quality" claim from the paper's abstract, fetched September 25, 2026. ↩

  9. vLLM API reference: WatermarkConfig, https://docs.vllm.ai/en/latest/api/vllm/config/watermarking. Gumbel is the default algorithm within an enabled WatermarkConfig. ↩

  10. European Commission, Code of Practice on Transparency of AI-generated Content, signatory information as of July 2026. ↩