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

In August 2026, two implementations of the same 2022 idea crossed the finish line within one legal window: Anthropic shipped SynthID-Text-style watermarking in Claude under the EU Code of Practice (Nature 2024: H=4 sliding-window seed, M=2^m candidate knockout tournament over m=30 Bernoulli g-value layers), and vLLM shipped keyed Gumbel-max sampling (--watermark-config gumbel, Philox PRF, context_width=4) with a weights-free detector. This guide does what the announcements do not: it derives the Gumbel-max trick (argmax of log p plus iid Gumbel noise is an exact categorical sampler), shows in Python how a keyed PRF turns the noise into a detectable signal (z=+28 at N=200 fully marked, z=+1.15 at 5% marking), prices Anthropic's own limitations section as math (sparse choice = sparse signal; the null per-token gap is a clean -ln 32 in our runs), and anchors the whole thing in Art. 50(2) EU AI Act (machine-readable marking, applied since 2 August 2026, with the Omnibus 2026/1744 four-month transition ending 2 December 2026 for pre-existing systems). The thesis: Art. 50(2) requires a property (provable marking) that closed endpoints can deliver and gatekeep at their own discretion, while open serving can only deliver it forwards — a deployer can prove provenance over their own stream, but nobody can prove absence, and downstream fine-tunes erase the mark entirely.

16 min readflozi00
aimachine-learningllmwatermarkingsecuritycomplianceregulation

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 constructions of the same 2022 Aaronson idea, 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 where the law asks open-weights deployments for a property they structurally cannot guarantee. Every number below is either quoted from a primary source or measured in the cells and labeled as such.

The duty, in the authentic consolidated text of Regulation (EU) 2024/1689:

Providers of AI systems, including general-purpose AI systems, generating synthetic audio, image, video or text content, shall ensure that the outputs of the AI system are marked in a machine-readable format and detectable as artificially generated or manipulated. Providers shall ensure their technical solutions are effective, interoperable, robust and reliable as far as this is technically feasible, taking into account the specificities and limitations of various types of content, the costs of implementation and the generally acknowledged state of the art4.

Three details in that text do the work later in this article. First, "machine-readable format and detectable" — a watermark qualifies only if a machine can test for it, which is precisely what a keyed statistical construction provides and an unkeyed one does not. Second, the qualifier "as far as this is technically feasible ... taking into account the specificities and limitations of various types of content" — the drafters wrote an entropy carve-out into the article without knowing it, because marking is only feasible where generation has choices (Section 4 shows this quantitatively). Third, an explicit exemption at the end of the paragraph for systems that "perform an assistive function for standard editing or do not substantially alter the input data" — proofreading your text is not a marking event, which matches Anthropic's own limits language almost clause for clause41.

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 introduces "a transitional period of four months for providers who have already placed their systems on the market before the 2 August 2026"6 — 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 whole machine both watermarks run on, so let us verify 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. In his own summary of 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 (the simplest scoring function being the plain mean across steps and layers), and compare against a threshold. Because every candidate was drawn from the model's own distribution and the tournament only re-weights between equally likely draws, the non-distortionary configuration leaves the marginal next-token distribution intact — a property Google validated on "nearly 20 million" live Gemini responses with no statistically significant difference in user ratings21.

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.

Both are, structurally, the same theorem: argmax of (log p plus keyed pseudorandom Gumbels) is exactly categorical given the key's noise, and detectably non-categorical in aggregate only through the key's lens. Now measure the detection side.

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 measures the average gap between the emitted token's noise value and 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"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, and its own limits notwithstanding, the marking duty is satisfied by keystroke: outputs from the endpoint are marked, full stop. 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. The statute's provider duty travels with the weights to the extent the provider ships watermarked defaults and their own serving endpoints — it does not travel to downstream re-serves. (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 weights-free proofs that a given output came from their infrastructure — a capability no C2PA-style metadata label gives on text, because there is no text file to sign31. Open serving cannot guarantee marking downstream, but it can guarantee attributability for what it itself served — 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's roughly 190 signatories are overwhelmingly entities that control their serving path1. 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 — only demonstrate good faith for their own.

6. Verdict

What watermarking is worth, measured honestly: a strong, effectively free, provably-distribution-preserving-at-the-margin marking technology for provider-controlled endpoints. 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's usage of it costs no extra tokens, no latency of note, and carries no user-identifying information1.

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.

And the part nobody priced: the law now makes the closed side's structural advantage — key, endpoint, gatekept verifier — a compliance asset, while open-weights deployments are handed a tool that proves their own integrity but binds no one else. Watermarking shipped. The asymmetry is shipping with it.

7. Sources

Footnotes

  1. Anthropic: How Claude's text watermark 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 ↩16 ↩17 ↩18

  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) quoted verbatim. ↩ ↩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. ↩