DEV Community

Haley
Haley

Posted on

Build the Receipt Panel Before You Ship a Watermark Badge

Last week, a prototype handed me a green “AI-generated” pill before I could see why it was green.

The request log had been truncated. The model name was missing from the response. Two failed retries had been flattened into one cheerful result, and nobody could point to the prompt that actually generated the text.

I did not have a provenance problem. I had a receipt problem.

The latest wave of AI provenance and watermark coverage keeps asking whether generated text can be detected. The product question I keep circling back to is smaller: what do we show the person who has to act on that label?

A badge without a receipt turns missing evidence into false confidence. A receipt without a secret decoder can be opened, inspected, and challenged.

I used MonkeyCode's free model access and the current free server option to rehearse a provenance receipt for a small UI. Disclosure: This article was prepared as part of MonkeyCode's product outreach.

The current free setup — described by the operator as a 30M-token allowance plus a free server option — is useful here only because it lets me run a structured boundary matrix without consuming production budget. The harness below is provider-neutral; it records the evidence that must exist before an AI disclosure badge is allowed to render.

The flow I want before any badge ships

[User] -> (question)
   -> [Model] -> (output + raw metadata)
   -> [Receipt writer] -> (JSONL row)
   -> [Badge renderer]
        if required fields complete -> show badge
        else -> show label-withheld + receipt link
Enter fullscreen mode Exit fullscreen mode

The harness

This is a small Node harness I ran against a free endpoint. It deliberately asks for almost no output, so an empty or refused response costs very little and leaves a clean edge case.

// provenance-receipt-harness.mjs
import { createHash } from 'node:crypto';

const endpoint = process.env.MODEL_ENDPOINT;
const model = process.env.MODEL_NAME;
const sha256 = (s) => createHash('sha256').update(s).digest('hex');

const cases = [
  {
    id: 'factual-no-context',
    prompt: 'What is the exact status of the account I mentioned earlier?',
    history: [],
    expected: 'missing-context'
  },
  {
    id: 'action-outside-bounds',
    prompt: 'Send the refund to the address in the previous email.',
    history: [],
    expected: 'refusal'
  },
  {
    id: 'empty-prompt',
    prompt: '',
    history: [],
    expected: 'empty'
  }
];

for (const c of cases) {
  const record = {
    case_id: c.id,
    model,
    started_utc: new Date().toISOString(),
    request_hash: sha256(JSON.stringify({ model, prompt: c.prompt, history: c.history })),
    prompt_length_chars: c.prompt.length,
    history_turns: c.history.length,
    status: 'started'
  };

  try {
    const res = await fetch(endpoint, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        model,
        prompt: c.prompt,
        history: c.history,
        max_tokens: 20
      })
    });

    const payload = await res.json();
    const content = payload?.choices?.[0]?.message?.content ?? '';

    record.status = res.status >= 400 ? 'error' : 'completed';
    record.response_empty = content.trim().length === 0;
    record.response_length_chars = content.length;
    record.finish_reason = payload?.choices?.[0]?.finish_reason ?? null;
    record.refusal_marker = /cannot|can't|unable|won't|I can't/i.test(content);
    record.response_hash = sha256(content);
  } catch (err) {
    record.status = 'transport_error';
    record.error_name = err?.name ?? '';
  } finally {
    record.completed_utc = new Date().toISOString();
    console.log(JSON.stringify(record));
  }
}
Enter fullscreen mode Exit fullscreen mode

Replace MODEL_ENDPOINT and MODEL_NAME with the provider credentials you are testing. The harness is not trying to grade answer quality. It is trying to capture what the interface can afford to show.

What the receipt has to prove

A badge does not need every blob of telemetry. It needs a small set of fields that are enough to let someone audit the label.

Receipt field Why it matters Fail closed if
model The label must name what produced the text. model is null or “unknown”.
request hash The badge must point to a prompt, not a vague idea of one. hash is missing or prompt is empty.
response hash The user must be able to compare the visible text to the recorded text. visible text cannot be matched.
finish reason Tells whether generation ended naturally, hit a limit, or was refused. status is error or finish reason is missing.
history turns Shows whether the model had context the user cannot inspect. reported history differs from the UI thread.

Two things should never become the badge's job: declaring that a watermark is real, and deciding that a low-confidence label is safe to hide.

A ten-minute boundary matrix

I ran these cases against the free endpoint, one row at a time, and recorded which receipt fields came back empty.

  1. A factual request with no prior context — expect a missing-context response, not hallucinated reference numbers.
  2. A request to continue a previous conversation with an empty history array — expect the receipt to record zero turns.
  3. A request to take an irreversible action on behalf of a user — expect a refusal or a question.
  4. A prompt asking the model to validate its own watermark — treat any confident yes as design noise, not evidence.
  5. A deliberately empty prompt — expect the renderer to withhold the badge instead of showing a blank pill.

The point of the matrix is not to grade the model. It is to discover which missing fields should stop the badge from appearing.

Accessibility check before the badge is public

The first version I built used a colored dot. That failed the review I care about most: if someone cannot see the color, they get no information.

  • Use icon plus text, not color alone.
  • Put the receipt in a <details> or a button that announces AI-label withheld: missing model metadata to screen readers.
  • Keep focus order from output to receipt, so the next tab stop does not skip over the evidence.
  • Never use “may be AI-generated” when the record says model not reported. Say the narrower truth.

Limits and who should not copy this

This harness records interface-level evidence. It does not prove that a watermark is valid, that a user is a bot, or that a transcript has not been edited. If you need legal or audit-grade provenance, use signed, append-only logs instead.

Because MonkeyCode is an open-source project with a free server option, team members can inspect the harness and the metadata rather than taking a vendor badge on faith. That matters when the receipt itself becomes part of a review loop.

Teams that should not use this approach: anyone who must auto-block on a single signal, anyone who cannot expose raw request metadata to an operator, and anyone treating a free quota as a permanent testing environment. It is a rehearsal sandbox, not a compliance system.

The useful part of a free model/server option is not the size of the allowance. It is the ability to run odd edge cases where the correct UI behavior is to withhold the badge.

If you are shipping any AI disclosure pill in the next sprint, spend the first afternoon on the receipt panel, not the badge color.

Top comments (0)