Next.js & Vercel AI SDK: an Art. 50 AI notice with <AIDisclosureLabel /> or a script tag

If you are a provider of a chat or agent feature that talks to people, Art. 50(1) of the AI Act asks that people are informed they are interacting with an AI system, unless that is obvious. It has applied since 2 August 2026 (dates and sources).

This guide shows three pieces:

  1. A visible notice in React with AIDisclosureLabel from spurvik/react.
  2. The same notice on any HTML page with one script tag.
  3. Optional: a signed receipt for each Vercel AI SDK call with spurvikMiddleware from spurvik/ai.

1. The notice in React / Next.js

npm i spurvik
// app/ChatHeader.tsx
import { AIDisclosureLabel } from "spurvik/react";

export function ChatHeader() {
  return (
    <AIDisclosureLabel
      notice="interaction"
      systemName="Support Assistant"
      locale="en"
      presentation="inline"
    />
  );
}
  • notice: "interaction" (Art. 50(1)) is the default case for chat. Other values: "generated", "deepfake", "public-interest", "emotion-biometric".
  • presentation: "inline" (a pill) or "block". The wording stays on screen either way, with no click needed.
  • locale: any of the 24 EU languages has default copy. Review the wording before use.
  • euIcon, brandName and colour props are optional. The icon alone does not meet Art. 50.
  • The component file is marked "use client", so you can render it from a Server Component.
  • beacon is optional notice telemetry. Leave it out and nothing is stored; set consent: true only after your consent tool allows it.
  • attribution is off by default. Pass attribution={true} to show a small “Art. 50 · Spurvik” link after the disclosure text. The link is optional; you can omit the prop at any time.

2. The notice on any site: the script tag

For Webflow, WordPress, Shopify or static HTML, add the script tag before </body>. Copy it from the Try it block below, or from the badge card in the docs.

Attributes: data-system (name shown), data-locale (for example da), data-theme (dark default, or light), data-position (bottom-right default, or bottom-left). For inline placement, put <div data-spurvik-badge></div> where the notice should appear and load the script once.

The badge sets no cookies, uses no storage and makes no network calls beyond loading the script. Language order: data-locale, then your page's <html lang>, then English. CSP hashes for older browsers are on the badge card in the docs.

Both the badge and the label are visible notices only. They do not mark generated content and are not Art. 50(2) marking in a machine readable format.

3. Optional: a signed receipt for each AI SDK call

spurvikMiddleware wraps a Vercel AI SDK model with wrapLanguageModel. Each call gets a provenance receipt, signed with HMAC SHA256, on providerMetadata.spurvik; prompt and output are recorded as keyed fingerprints, not text. This part sends events to the hosted console, so it needs a spurvik_live_ project key from a console workspace.

npm i spurvik ai @ai-sdk/openai
// app/api/chat/route.ts (Next.js App Router, Node.js runtime)
import { streamText, wrapLanguageModel } from "ai";
import { openai } from "@ai-sdk/openai";
import { after } from "next/server";
import { spurvikMiddleware } from "spurvik/ai";

// Create the middleware once, at module scope.
const spurvik = spurvikMiddleware({
  apiKey: process.env.SPURVIK_API_KEY!,
  signingSecret: process.env.SPURVIK_SIGNING_SECRET!,
  contentHash: { mode: "keyed", secret: process.env.SPURVIK_CONTENT_HASH_SECRET! },
  baseUrl: process.env.SPURVIK_BASE_URL!,
  riskTier: "limited_art50", // your own classification
  article50Disclosure: true, // set only when your UI shows the notice
});

const model = wrapLanguageModel({ model: openai("gpt-4o-mini"), middleware: spurvik });

export async function POST(req: Request) {
  const { prompt } = (await req.json()) as { prompt: string };
  const result = streamText({ model, prompt });
  after(() => spurvik.flush()); // send queued events after the response
  return result.toTextStreamResponse();
}

Setup notes from the SDK:

  • SPURVIK_SIGNING_SECRET and SPURVIK_CONTENT_HASH_SECRET must each be at least 16 characters, or setup throws. Keep them on the server; never use NEXT_PUBLIC_.
  • Set them at build time too: next build fails without them. On Vercel, add them to the project's environment variables.
  • baseUrl is required when NODE_ENV is production.
  • article50Disclosure: true records that you show a notice. It is configuration, not a finding that the notice is sufficient.
  • Read a receipt with provenanceFromMetadata(result.providerMetadata) after generateText.

The SDK and MCP server are MIT and run in your own stack.

Try it

Copy the badge
<script src="https://spurvik.com/badge.js" data-system="Your AI" data-attribution="true" defer></script>
  • A small “Art. 50 · Spurvik” link shows after the notice by default. On Pro (€19/mo) you can hide it with data-attribution="false" for a white-label badge.
Or in React
import { AIDisclosureLabel } from "spurvik/react";

Free €0 (10k events/mo) · Pro €19/mo · Team €79/mo · excl. VAT · B2B only.

FAQ

Do I need an account to show the notice?

No. AIDisclosureLabel from spurvik/react and the script tag both work without an account. Only the event receipts sent to the hosted console need a project key.

Which languages does the notice support?

Default copy for the 24 EU languages. Review the wording before use; you can set your own.

Does the script tag set cookies or track visitors?

No. It uses no cookies and no storage and makes no network calls beyond loading the script.

Does the badge or label mark content generated by AI?

No. They are visible notices that support Art. 50(1). They are not Art. 50(2) marking in a machine readable format.

Can I use AIDisclosureLabel in a Server Component?

Yes. The component file is marked "use client", so you can render it from a Server Component.

Sources

Reviewed 11 November 2026. Not legal advice. Supports parts of your Art. 50 work; responsibility stays with you. EyeTea Solutions ApS · Trekantområdet, Denmark

Your AI Act controls should live in a few lines of code, not in six months of legal memos.