โ† Back to Blog
AI for PMsAI EngineeringLLMs

Shipping AI Features Under Article 50: A Transparency Checklist for Product and Engineering Teams

The EU AI Act's transparency rules have applied since 2 August 2026. Here's what your chatbot, content generator, and agent actually need to do โ€” disclosure UX, machine-readable marking, and the code to implement it.

By Joinloopยท2 September 2026ยท9 min read
ShareLinkedInXReddit
๐Ÿ”Ž

If your product uses an LLM to talk to users or to generate images, audio, video, or text, Article 50 of the EU AI Act now applies to you. The obligations became enforceable on 2 August 2026. They are not a high-risk classification and they do not require a conformity assessment โ€” but they do require specific, testable behaviour in your product, and the fines for ignoring them scale to 3% of global turnover.

This post is a working checklist: what the four obligations mean, where teams get them wrong, and the code to satisfy the machine-readable marking requirement. It is not legal advice โ€” the Commission's Article 50 guidelines and your counsel are the authority. But most of the gap here is engineering and UX work that a product team can scope in a sprint.

The four obligations, in plain terms

Article 50 splits duties between providers (you build or substantially modify the AI system) and deployers (you use someone else's system in your product). Most teams building on the OpenAI, Anthropic, or Google APIs are deployers for the base model and providers for the feature they wrap around it. You can hold both roles at once.

# Obligation Who Trigger What it requires
1 Chatbot disclosure Provider System interacts directly with humans Inform the person they are dealing with an AI, unless it is obvious to a reasonably informed user
2 Synthetic content marking Provider System generates synthetic audio, image, video, or text Output marked in a machine-readable format and detectable as AI-generated
3 Deep fake disclosure Deployer Content is a deep fake (image/audio/video resembling real people, places, events) Disclose that the content is artificially generated or manipulated
4 AI-generated text for public interest Deployer Text published to inform the public on matters of public interest Disclose that the text is AI-generated, unless human-reviewed with editorial responsibility

The exemptions matter as much as the rules:

  • Obligation 1 falls away when it is obvious. A button labelled "Ask our AI assistant" that opens a chat panel probably clears the bar. A support widget that silently hands off from a human agent to a bot mid-conversation does not.
  • Obligation 2 does not apply where the AI performs an "assistive" function that does not substantially alter input data โ€” grammar correction, minor retouching. It does apply to your "generate a product description" and "make me a hero image" features.
  • Obligation 4 falls away when a human editor takes responsibility for the published text. A newsroom byline covers it; an auto-published SEO blog farm does not.

Where teams get it wrong

Treating disclosure as a one-time onboarding modal. The obligation is that the person is informed "at the time of the first interaction." A user who lands deep-linked into a conversation six weeks later still needs to know. Put the disclosure in the surface, not in a setup flow they saw once.

Assuming the model provider's watermark covers you. Google's SynthID marks Gemini image and text output. OpenAI and Anthropic apply C2PA metadata to some outputs. But if you strip metadata on upload, re-encode images through a CDN transform, or paraphrase model text through a second pass, you have broken the chain and you are the provider of the final synthetic output. You own the marking obligation for what leaves your system.

Marking for humans but not machines. A visible "๐Ÿค– AI-generated" badge in your UI satisfies nobody's machine-readability requirement. The standard the Commission points to is embedded, survivable provenance data โ€” C2PA Content Credentials for media, and for text, metadata or watermarking that a downstream detector can read. The visible label is a separate, additional good practice.

Forgetting the agent case. An agent that sends emails, posts to a CRM, or files tickets on a user's behalf is "interacting with humans" through those channels. The recipient of an AI-drafted email is arguably owed disclosure. This is unsettled, but the conservative move is an "drafted with AI assistance" footer that the user can edit or remove before sending โ€” the decision stays with the human.

Implementing machine-readable marking

Text: attach provenance metadata at generation time

For text, the pragmatic approach today is to carry a signed provenance record alongside the content wherever your system boundary allows it โ€” API responses, exports, stored documents. C2PA now has a text manifest profile; where you cannot embed, keep provenance in a sidecar field and in your database.

import hashlib
import json
import time
from dataclasses import dataclass, asdict


@dataclass
class ProvenanceRecord:
    content_sha256: str
    generated_by: str            # e.g. "acme-copywriter/2.3"
    base_model: str              # e.g. "claude-sonnet-5"
    ai_generated: bool
    human_reviewed: bool
    created_at: float
    jurisdiction_note: str = "AI-generated content, EU AI Act Art. 50(2)"


def make_provenance(text: str, *, human_reviewed: bool = False) -> ProvenanceRecord:
    return ProvenanceRecord(
        content_sha256=hashlib.sha256(text.encode("utf-8")).hexdigest(),
        generated_by="acme-copywriter/2.3",
        base_model="claude-sonnet-5",
        ai_generated=True,
        human_reviewed=human_reviewed,
        created_at=time.time(),
    )


def annotate_response(text: str, human_reviewed: bool = False) -> dict:
    record = make_provenance(text, human_reviewed=human_reviewed)
    return {
        "content": text,
        "provenance": asdict(record),
        # Visible disclosure is separate from machine-readable marking.
        "disclosure": None if human_reviewed else "This text was generated by AI.",
    }


payload = annotate_response("Your monthly summary: spend is up 12%...")
print(json.dumps(payload, indent=2))

The content_sha256 lets a downstream consumer verify the record refers to this exact text. In production you sign the record (Ed25519, JWS) so it is tamper-evident, and you publish your public key so third parties can verify.

Images and media: preserve C2PA Content Credentials

If your model provider returns C2PA-signed images, your job is to not destroy the manifest. Most image pipelines do exactly that.

# pip install c2pa-python
from c2pa import Reader, Builder, create_signer, SigningAlg


def has_content_credentials(path: str) -> bool:
    try:
        with Reader(path) as reader:
            return reader.json() is not None
    except Exception:
        return False


def add_ai_assertion(src: str, dst: str, cert_pem: bytes, key_pem: bytes) -> None:
    """Attach an AI-generated assertion when producing a derivative image."""
    manifest = {
        "claim_generator": "acme-image-service/1.4",
        "assertions": [
            {
                "label": "c2pa.actions",
                "data": {
                    "actions": [
                        {"action": "c2pa.created",
                         "digitalSourceType":
                            "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia"}
                    ]
                },
            }
        ],
    }
    signer = create_signer(key_pem, SigningAlg.ES256, cert_pem)
    with Builder(manifest) as builder:
        builder.sign_file(signer, src, dst)


# In your upload / transform pipeline:
if not has_content_credentials("cdn_output.jpg"):
    # You re-encoded and lost the chain โ€” re-assert as the new provider.
    add_ai_assertion("model_output.png", "cdn_output.jpg", CERT, KEY)

Audit every stage that touches bytes: upload handlers, thumbnailers, sharp/ImageMagick transforms, CDN image resizing (Cloudflare Images, imgix), and social share-card generation. Each one is a place the manifest silently dies.

Chatbot disclosure: make it structural

function AssistantPanel({ conversationId }: { conversationId: string }) {
  return (
    <section aria-label="AI assistant">
      {/* Persistent, not dismissible into permanent hiding.
          Present on first render of the surface, every session. */}
      <p className="assistant-disclosure" role="note">
        You're chatting with an AI assistant. It can make mistakes.
        <a href="/ai-disclosure">How this works</a>
      </p>
      <MessageThread conversationId={conversationId} />
    </section>
  );
}

Keep the disclosure in the component that renders the chat, so every entry point โ€” deep link, resumed session, embedded widget โ€” inherits it. Do not gate it behind a first-run flag.

Rollout checklist

  • Inventory every generative surface. Chat, autocomplete, summarisation, image generation, voice, agent actions. Note which produce output that leaves your system.
  • Classify your role per feature. Provider, deployer, or both. Write it down โ€” you will need it for your records.
  • Chatbot disclosure is structural, present on first interaction in every entry path, with a link to a fuller explanation.
  • Synthetic output carries machine-readable marking โ€” C2PA for media, signed provenance metadata for text โ€” that survives your own pipeline.
  • Audit the pipeline for manifest destruction. Re-encoding, CDN transforms, metadata stripping on upload, second-pass paraphrasing.
  • Deep fake features disclose artificial generation clearly and visibly, not just in metadata.
  • Public-interest text is either labelled AI-generated or has a named human taking editorial responsibility.
  • Agent-sent communications carry an editable "drafted with AI" note; the human confirms before send.
  • Detectability test in CI. Run your own output through an AI-content detector and assert the provenance is readable.
  • Disclosure copy reviewed by counsel and available in all EU languages you serve.
  • Records kept. Which features, which role, what marking method, what version.

A CI test you can add this week

import pytest
from c2pa import Reader
from myapp.generation import generate_marketing_image, generate_summary


def test_generated_image_has_readable_provenance(tmp_path):
    out = tmp_path / "out.jpg"
    generate_marketing_image("a coffee mug on a desk", out)
    with Reader(str(out)) as reader:
        manifest = reader.json()
    assert manifest is not None, "image left the system with no Content Credentials"
    assert "trainedAlgorithmicMedia" in manifest


def test_generated_text_response_is_marked():
    resp = generate_summary("Q3 numbers...", human_reviewed=False)
    assert resp["provenance"]["ai_generated"] is True
    assert resp["disclosure"] is not None
    # Provenance hash matches the delivered content.
    import hashlib
    digest = hashlib.sha256(resp["content"].encode()).hexdigest()
    assert digest == resp["provenance"]["content_sha256"]

Next steps

Start with the inventory. Most teams discover two or three generative surfaces they had forgotten โ€” an autocomplete here, a share-card generator there โ€” and those are exactly the ones that ship unmarked output. Fix the chatbot disclosure first because it is cheap and visible, then work through the marking pipeline audit, which is where the real engineering time goes.

Article 50 is the low bar. If your feature touches recruitment, credit, education, or biometrics, you are likely in high-risk territory with a much heavier obligation set arriving in 2027, and you should be scoping that now.

If your team is building AI features and wants a structured way to level up on evals, guardrails, and shipping probabilistic systems responsibly, our product leaders' AI cohort works through exactly these decisions with real product cases.

ShareLinkedInXReddit

Want to build this yourself?

Join a live cohort and go from reading about AI to shipping it, with practitioners who do it daily.