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:
- 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.
- 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.
- 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))
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}
]
}
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)