DEV Community

Amit
Amit

Posted on

How AI text watermarking works

#ai

An AI model can leave a watermark in the words it chooses. The text looks ordinary, and the mark can survive being copied into Notepad because it is carried by the token sequence. A detector with the matching key can check for that pattern later.

Anthropic explained its approach in August 2026 in "How Claude's text watermark works". It uses a version of SynthID-Text, which Google DeepMind published in Nature in 2024. Anthropic says it is introducing watermarking to meet the EU AI Act's transparency requirements.

I wanted to understand how ordinary word choices could carry a signature, and what a detector finding that signature would actually tell us. I followed the choice of one token through the process, then tested a small implementation with GPT-2 in Google Colab.

Starting with one word

Let's say you upload a photo of a cat to Claude and ask, "What is the cat in this image doing?" Claude starts its answer with "The cat" and now has to pick the next token.

A language model writes one token at a time. A token can be a word, part of a word or punctuation. GPT-2's tokenizer for example, has roughly 50,000 tokens. The model assigns probabilities to possible next tokens. For a simplified example, assume only these three options have non-zero probabilities:

  • sits: 50%
  • sat: 30%
  • lies: 20%

These are invented probabilities and not actual measurements. We will treat each word as one token throughout the example. Without a watermark, sampling from these probabilities would pick "sits" about half of the time. The chosen token joins the sentence, and model calculates probabilities for the next token. The watermark changes how the next token is selected. Model's weights remain unchanged. For the same input, it still produces same initial probabilities. Watermarking just influences the selection.

Without the watermark, a model might write "The cat sits on the mat." With the watermark, it might write "The cat sat on the rug" instead. These are just illustrative outcomes. The choices happen as the text is generated.

A secret key, combined with the preceding tokens, determines the watermark scores for each possible next token. Changing the preceding tokens can change the scores. With same key, settings and token context, calculation gives the same scores every time. That repeatability makes detection possible later.

The tournament

SynthID-Text uses a method called tournament sampling to choose the next token. We can follow it through an example with four candidates and two rounds.
Suppose a model has written "The cat" and assigns these probabilities to the next token:

Token Probability
sits 50%
sat 30%
lies 20%

First, we draw four candidates using those probabilities. Each draw can pick any of three tokens, including one already picked. One possible result is:
sits, sat, sits, lies

"sits" appears twice in this draw. It's 50% probability makes it more likely to appear, but that does not guarantee two places.
Next, watermarking algorithm uses key and preceding tokens to calculate a score of 0 or 1 for each candidate. Each round has its own scoring function. Within a round, repeated copies of the same token receive the same score. Suppose scores produce these matches:

Round Match Winner
1 sits (0) versus sat (1) sat
1 sits (0) versus lies (1) lies
2 sat (1) versus lies (0) sat

The higher score wins each match. Ties are broken randomly. A token's score can differ between rounds, which is why "lies" scores 1 in the first round and 0 in the second. The winning token is "sat", so text now reads "The cat sat". Generation continues from there.

Although "sits" had the highest original probability, both of its copies lost in this tournament. Another draw could produce a different winner. The model's probabilities determine how likely candidates are to enter, and watermark scores determine which candidates advance. This is method described in SynthID-Text paper.

Picking "sat" once provides little evidence of a watermark. An unwatermarked model could pick it too. Across many tokens watermarked text tends to have higher watermark scores than expected by chance. The detector measures that tendency. The bracket helps explain the method. The implementation discussed later calculates the equivalent winning probabilities directly.

Does watermarking affect writing quality??? 🤔

Drawing candidates from the model's probabilities keeps likely continuations well represented. That alone does not guarantee good writing. SynthID-Text has a quality-preserving configuration and another that produces a stronger watermark at a cost to quality. Google tested the quality-preserving configuration on nearly 20 million Gemini responses. The paper reports no statistically significant difference in thumbs-up or thumbs-down ratings between watermarked and unwatermarked responses. When the model has very little choice about the next token, there is less room to leave a watermark. This makes detection harder in passages where accurate wording leaves few alternatives.

How a detector checks without the prompt

The detector uses text, secret key and same watermark settings used during generation. It also needs the matching tokenizer to split the text into tokens. For each token, it takes the preceding tokens and calculates that token's watermark scores again. The calculation is repeatable: the same inputs produce the same scores.

This works without the prompt because the preceding tokens are already in passage. With a four-token context, detector skips scoring the first four tokens. From the fifth token onward, it has the context it needs. A simple detector averages recovered scores across the eligible tokens. It skips repeated contexts so that a repeated phrase does not count as fresh evidence. SynthID-Text also supports more sophisticated detectors.

For text written without this watermark, average is expected to be around 0.5 because the scoring functions assign roughly equal numbers of zeros and ones. Watermarked text tends to score higher because tournament favours tokens with higher scores. An average above 0.5 is not enough on its own to establish a watermark. Short passages can score high by chance. Longer passages usually provide more evidence, provided enough of the watermark survives.

Detectors use thresholds calibrated against unwatermarked text to control false positives. For a simple average-based approach, the threshold also needs to account for text length. Editing can weaken the signal because it changes the inputs to the scoring functions. With a five-token window, replacing one token affects the score inputs for that position and the next four. Replacing a whole word may affect more tokens, depending on how it is tokenized. Small edits can leave much of the original signal intact. Extensive rewriting can make the watermark undetectable.

How the code implements the tournament

A tournament with 30 rounds would require 230 candidates for each token, which is over a billion. The implementation calculates the equivalent winning probabilities directly.

for i in range(depth):
    g_values_at_depth = g_values[:, :, i]
    g_mass_at_depth = (g_values_at_depth * probs).sum(axis=1, keepdims=True)
    probs = probs * (1 + g_values_at_depth - g_mass_at_depth)
Enter fullscreen mode Exit fullscreen mode

g_values contains the watermark scores, and depth is the number of rounds. Each round adjusts the probabilities to favour tokens that scored 1. The adjustment depends on how much probability those tokens already hold. Using our cat example, the probabilities change as follows. In round 1, "sat" and "lies" score 1. In round 2, only "sat" scores 1; "sits" and "lies" both score 0.

Token Original probability After round 1 After round 2
sits 50% 25% 13.75%
sat 30% 45% 69.75%
lies 20% 30% 16.5%

The 69.75% comes from the chance that at least one finalist is "sat". After round 1, each finalist has a 45% chance of being "sat", so each has a 55% chance of being something else. The chance that neither is "sat" is 0.55 × 0.55 = 0.3025, or 30.25%. Since "sat" beats the other tokens in round 2, its chance of winning is 100% - 30.25% = 69.75%.

The implementation calculates these winning probabilities and makes one random choice from them. This gives each token the same chance of selection as running the tournament. "sat" is the most likely choice, but the other tokens still have a combined 30.25% chance.

Testing with GPT-2

I used GPT-2 in Google Colab to generate text with and without a watermark. The setup used 30 key values for 30 watermarking rounds, a five-token window, top_k=40 and temperature=0.7. Each response could contain up to 300 new tokens.

For each of five prompts, I generated one watermarked response and one plain response. I then took the first 20 words of the watermarked response. Separately, I made an edited version of the full watermarked response by replacing one word in four, starting with the first, with a random choice from "the", "a", "very" and "also". These replacements test the effect of disruptive edits rather than natural paraphrasing. I also scored an F1 text sample.

The detector averaged the watermark scores after excluding the opening tokens, repeated contexts and end-of-text padding. It produced a raw average score, not a probability that the text was AI-generated.

The first cell loads GPT-2 and sets the watermark configuration:

import torch, random
from transformers import (AutoTokenizer, AutoModelForCausalLM, SynthIDTextWatermarkingConfig, SynthIDTextWatermarkLogitsProcessor)

DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
tok = AutoTokenizer.from_pretrained("gpt2")
model = AutoModelForCausalLM.from_pretrained("gpt2").to(DEVICE)

CFG = dict(ngram_len=5, keys=[654,400,836,123,340,443,597,160,57,29,590,639,13,715,468, 990,966,226,324,585,118,504,421,521,129,669,732,225,90,960],sampling_table_size=2**16, sampling_table_seed=0,context_history_size=1024)
wm_config = SynthIDTextWatermarkingConfig(**CFG)
lp = SynthIDTextWatermarkLogitsProcessor(**CFG, device=DEVICE)
Enter fullscreen mode Exit fullscreen mode

The second generates a response with or without the watermark:

def gen(prompt, watermark, n=300):
    inp = tok(prompt, return_tensors="pt").to(DEVICE)
    out = model.generate(**inp, do_sample=True, top_k=40, temperature=0.7,max_new_tokens=n, pad_token_id=tok.eos_token_id, watermarking_config=wm_config if watermark else None)
    return tok.decode(out[0, inp.input_ids.shape[1]:], skip_special_tokens=True)
Enter fullscreen mode Exit fullscreen mode

The third calculates the average watermark score:

def score(text):
    ids = tok(text, return_tensors="pt").input_ids.to(DEVICE)
    g = lp.compute_g_values(input_ids=ids).float()  
    eos = lp.compute_eos_token_mask(input_ids=ids, eos_token_id=tok.eos_token_id)[:, CFG["ngram_len"]-1:]
    rep = lp.compute_context_repetition_mask(input_ids=ids)
    m = (eos * rep).float().unsqueeze(-1)
    return ((g * m).sum() / (m.sum() * g.shape[-1])).item()
Enter fullscreen mode Exit fullscreen mode

The fourth runs the comparisons:

prompts = ["Write about how coffee is grown and exported.", "Explain how trains work.",  "Descrieb a day at the beach.", "Tell the history of the bicycle.", "Explain how rain forms."]
human = """George Russell won the Azerbaijan Grand Prix in Baku on Saturday, 26 September, converting pole position into his third victory of the season. It was not a quiet afternoon. The race had two safety car periods, six retirements, and a finish so close that Russell crossed the line only a tenth of a second ahead of Max Verstappen. Isack Hadjar completed the podium in third place.

The first safety car came out after Alex Albon made an error and binned his Williams. The second followed by Franco Colapinto torpedo'ing himself in turn 1, which also ended the races of his teammate Pierre Gasly and Lando Norris. Both Aston Martins retired with mechanical problems. Oscar Piastri led for much of the race, but a lock-up cost him badly and he finished fourteenth. Charles Leclerc took fourth and Lewis Hamilton sixth. Kimi Antonelli had one of the best drives of the day, climbing from sixteenth to fifth.

The result extends Russell's championship lead and denies Verstappen his first win of the season."""

def edit(t, every=4):
    w = t.split()
    return " ".join(random.choice(["the","a","very","also"]) if i % every == 0 else x
                    for i, x in enumerate(w))

rows = {"WM long": [], "Plain long": [], "WM short (20 words)": [], "WM 25% edited": []}
for p in prompts:
    wm, plain = gen(p, True), gen(p, False)
    rows["WM long"].append(score(wm))
    rows["Plain long"].append(score(plain))
    rows["WM short (20 words)"].append(score(" ".join(wm.split()[:20])))
    rows["WM 25% edited"].append(score(edit(wm)))

for k, v in rows.items():
    print(f"{k:22} mean={sum(v)/len(v):.3f}  runs={[round(x,3) for x in v]}")
print(f"{'Human text (F1)':22} {score(human):.3f}")
Enter fullscreen mode Exit fullscreen mode

Results

These scores are from one run. Generation and word replacement are random, so reruns can produce different values.

Text Average score Range across five prompts
Watermarked responses 0.564 0.548 to 0.586
Plain GPT-2 responses 0.497 0.489 to 0.509
First 20 words of watermarked responses 0.573 0.545 to 0.607
Watermarked responses with every fourth word edited 0.500 0.495 to 0.504
F1 text sample 0.498 One sample

The watermarked and plain responses had no overlapping scores in these five runs. The F1 sample scored close to the plain responses. This shows a clear difference in this experiment, though more samples would be needed to establish a reliable detection threshold and measure false positives.

The short excerpts also scored high, but I did not test short unwatermarked excerpts for comparison. These results cannot establish reliable detection on short passages.

Replacing every fourth word brought the average down to 0.500, close to the unwatermarked baseline. This editing pattern weakened the signal measured by this detector. It does not establish that changing any 25% of a passage would have the same effect.

This experiment used GPT-2 and the configuration shown above. It does not measure the performance of Claude's watermark. A detected watermark provides evidence that the matching watermarking system was involved in producing the text. Failing to detect one does not establish that a person wrote it.

Sources

Top comments (0)