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:
- A visible notice in React with
AIDisclosureLabelfromspurvik/react. - The same notice on any HTML page with one script tag.
- Optional: a signed receipt for each Vercel AI SDK call with
spurvikMiddlewarefromspurvik/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,brandNameand 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. beaconis optional notice telemetry. Leave it out and nothing is stored; setconsent: trueonly after your consent tool allows it.attributionis off by default. Passattribution={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_SECRETandSPURVIK_CONTENT_HASH_SECRETmust each be at least 16 characters, or setup throws. Keep them on the server; never useNEXT_PUBLIC_.- Set them at build time too:
next buildfails without them. On Vercel, add them to the project's environment variables. baseUrlis required whenNODE_ENVisproduction.article50Disclosure: truerecords that you show a notice. It is configuration, not a finding that the notice is sufficient.- Read a receipt with
provenanceFromMetadata(result.providerMetadata)aftergenerateText.
The SDK and MCP server are MIT and run in your own stack.
Try it
<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.
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
- npm: https://www.npmjs.com/package/spurvik (1.0.8;
dist/react.d.ts,dist/ai.d.tsinspected 11 Nov 2026) - Badge docs: https://spurvik.com/docs#badge
- Article 50 dates: https://spurvik.com/art50-deadline-checklist
Reviewed 11 November 2026. Not legal advice. Supports parts of your Art. 50 work; responsibility stays with you. EyeTea Solutions ApS · Trekantområdet, Denmark