DEV Community

Haku
Haku

Posted on

Your chatbot is now a regulated surface: AB 1609 + EU AI Act Article 50 compliance in code

Your customer-service chatbot has a fine schedule now. Two regimes turned it into a regulated surface this month, and neither is hypothetical. Here's the engineering version: four checkable booleans, a cadence matrix, and an ~80-line Python scorer that returns a 0–100 compliance score, per-requirement pass/fail, the regimes that apply to your footprint, and a fix-first gap report ordered by penalty exposure.

1. The fine schedule (all figures per CX Network's Oct 11–12, 2026 coverage)

CA AB 1609 (signed Sept 28, 2026) — $5,000 for the first violation, $10,000 for each subsequent one. It binds large private businesses: $500M+ in annual revenue, serving CA consumers. Three tripwires:

  • In-the-moment AI disclosure — "clear and conspicuous" disclosure wherever a reasonable person could be misled, and you may not represent the bot as human. A human-name avatar with no AI mention is the violation.
  • A 15-minute human handoff — a human agent within 15 minutes, OR an appointment within one business day. A handoff button that only works 9–5 while the bot runs 24/7 fails it.
  • A conspicuous service phone number on the site.

EU AI Act Article 50 (in effect since August 2026) — up to €15M or 3% of global turnover. No revenue threshold. Every person must know they're talking to AI — in the moment AND in your policy. Per the guidance: "Generic declarations of AI use will not suffice."

2. Four checkable booleans + the cadence split

Every bot setup reduces to four booleans — in_moment_disclosure, policy_disclosure, human_handoff_sla, conspicuous_phone — plus a cadence question for the patchwork: 12 US states now regulate chatbot disclosure (CA, CO, CT, GA, HI, ID, IA, NE, NY, OR, RI, WA as of Oct 2026) — but only CA and WA mandate frequent reminders; the rest accept a one-time disclosure.

So your disclosure logic has to branch: frequent reminders for CA/WA traffic, one-time elsewhere, in-the-moment + policy disclosure for EU, and a 15-minute handoff clock if you're $500M+ and serving California.

3. The scorer: applicability, the 49-cap honesty rule, fix-first ordering

Three design decisions in the code below:

  1. Regime applicability — AB 1609 applies when annual revenue ≥ $500M and you serve CA consumers. EU Article 50 applies whenever you serve EU users. State rules apply per state in your footprint.
  2. The 49-cap honesty rule — missing in-moment disclosure while serving EU users hard-caps the score at 49, because Article 50 makes it non-negotiable. A scorer that hands you an 80 while you're breaking the steepest rule is lying to you.
  3. Fix-first ordering by penalty exposure — gaps sorted EU Art 50 first (up to €15M / 3%), then AB 1609 ($5k/$10k per violation), then state-cadence gaps.

4. The scorer in ~80 lines

from dataclasses import dataclass, field

# Penalty exposure (all figures per CX Network, Oct 11-12, 2026):
# EU AI Act Art 50: up to EUR 15M or 3% of global turnover (in effect since Aug 2026)
# CA AB 1609: $5,000 first violation, $10,000 each subsequent (signed Sept 28, 2026)
STATES = ["CA", "CO", "CT", "GA", "HI", "ID", "IA", "NE", "NY", "OR", "RI", "WA"]
FREQUENT_REMINDER_STATES = {"CA", "WA"}  # only CA and WA mandate frequent reminders

@dataclass
class BotSetup:
    revenue_m: float = 0.0            # annual revenue, $ millions
    serves_ca: bool = False
    serves_eu: bool = False
    states: list = field(default_factory=list)
    in_moment_disclosure: bool = False   # clear and conspicuous AI disclosure
    policy_disclosure: bool = False      # policy states AI is used
    human_handoff_sla: bool = False      # human in 15 min OR appt in 1 business day
    conspicuous_phone: bool = False      # conspicuous service phone number on site
    frequent_reminders: bool = False     # reminders during the conversation

def applicable_regimes(s: BotSetup) -> list:
    regimes = []
    if s.revenue_m >= 500 and s.serves_ca:
        regimes.append("AB 1609 (CA)")
    if s.serves_eu:
        regimes.append("EU AI Act Art 50")
    for st in s.states:
        if st in STATES:
            regimes.append(f"State disclosure: {st}")
    return regimes

# (requirement, scope, penalty_tier) — tier 1 = fix first
REQUIREMENTS = [
    ("in_moment_disclosure", "eu", 1),      # Art 50: non-negotiable
    ("policy_disclosure", "eu", 1),
    ("in_moment_disclosure", "ab1609", 2),  # AB 1609 tripwire
    ("human_handoff_sla", "ab1609", 2),
    ("conspicuous_phone", "ab1609", 2),
    ("frequent_reminders", "state-frequent", 3),
    ("in_moment_disclosure", "state", 3),
]

def score(s: BotSetup) -> dict:
    eu = s.serves_eu
    ab = s.revenue_m >= 500 and s.serves_ca
    frequent = [st for st in s.states if st in FREQUENT_REMINDER_STATES]
    other = [st for st in s.states if st in STATES and st not in FREQUENT_REMINDER_STATES]
    results, gaps, earned, total = [], [], 0, 0
    for attr, scope, tier in REQUIREMENTS:
        applies = ((scope == "eu" and eu) or (scope == "ab1609" and ab)
                   or (scope == "state-frequent" and frequent)
                   or (scope == "state" and other))
        if not applies:
            continue
        passed = getattr(s, attr)
        total += 1
        earned += passed
        results.append({"requirement": attr, "scope": scope, "passed": passed})
        if not passed:
            gaps.append({"requirement": attr, "scope": scope, "penalty_tier": tier})
    pct = round(100 * earned / total) if total else 100
    # 49-cap honesty rule: no in-moment disclosure for EU users caps at 49
    if eu and not s.in_moment_disclosure:
        pct = min(pct, 49)
    gaps.sort(key=lambda g: g["penalty_tier"])
    return {"score": pct, "per_requirement": results,
            "applicable_regimes": applicable_regimes(s), "fix_first_gaps": gaps}

if __name__ == "__main__":
    import json
    setup = BotSetup(revenue_m=600, serves_ca=True, serves_eu=True,
                     states=["CA", "NY"], in_moment_disclosure=False,
                     policy_disclosure=True, human_handoff_sla=True,
                     conspicuous_phone=False, frequent_reminders=False)
    print(json.dumps(score(setup), indent=2))
Enter fullscreen mode Exit fullscreen mode

Sample response for the setup above:

{
  "score": 49,
  "per_requirement": [
    {"requirement": "in_moment_disclosure", "scope": "eu", "passed": false},
    {"requirement": "policy_disclosure", "scope": "eu", "passed": true},
    {"requirement": "in_moment_disclosure", "scope": "ab1609", "passed": false},
    {"requirement": "human_handoff_sla", "scope": "ab1609", "passed": true},
    {"requirement": "conspicuous_phone", "scope": "ab1609", "passed": false},
    {"requirement": "frequent_reminders", "scope": "state-frequent", "passed": false},
    {"requirement": "in_moment_disclosure", "scope": "state", "passed": false}
  ],
  "applicable_regimes": ["AB 1609 (CA)", "EU AI Act Art 50", "State disclosure: CA", "State disclosure: NY"],
  "fix_first_gaps": [
    {"requirement": "in_moment_disclosure", "scope": "eu", "penalty_tier": 1},
    {"requirement": "in_moment_disclosure", "scope": "ab1609", "penalty_tier": 2},
    {"requirement": "conspicuous_phone", "scope": "ab1609", "penalty_tier": 2},
    {"requirement": "frequent_reminders", "scope": "state-frequent", "penalty_tier": 3},
    {"requirement": "in_moment_disclosure", "scope": "state", "penalty_tier": 3}
  ]
}
Enter fullscreen mode Exit fullscreen mode

Three of seven pass — which would be 43% — but the missing EU in-moment disclosure caps it at 49 anyway. That's the honesty rule doing its job.

5. Disclaimer

⚠️ DiscloseKit is an audit aid, not legal advice. All regime summaries trace to CX Network's Oct 11–12, 2026 coverage; verify against the statutory text and your counsel before relying on any requirement or penalty figure.

6. The full kit

I packaged this as DiscloseKit — the full audit kit with six checklists (AB 1609, EU Article 50, state matrix, disclosure-UX patterns, handoff-SLA worksheet) and a runnable FastAPI checker: https://vittoriali.gumroad.com/l/disclosekit

$39 one-time, by Haku.

Top comments (0)