13 min read

New York's Chatbot Law Gates Features, Not Chatbots. Here Is What the Text Says and How to Build for It.

New York signed S9051B on 28 October 2026. It bars unsafe chatbot features unless the operator finds the user is not a minor. The text, and a tested build.

A thick light blue line runs across the image and off the right edge. Four thinner light blue branches peel off it, two upward and two downward, at staggered points and with different lengths. Each branch stops just short of a short amber bar.

On Wednesday 28 October 2026, Governor Hochul signed S9051B, a New York law on AI chatbots and minors. It has been described as a law that makes chatbots verify every user’s age. The text says something narrower, and more useful for engineers.

It makes a list of “unsafe chatbot features” unlawful to provide to a user in New York unless the operator has determined that the user is not a minor. The chat itself is not on the list. So AI chatbot age verification under this law is a question about features: which ones you switch on, for whom, and what you do when you don’t know. This post goes through the text, marks where news coverage differs from it, and shows a small tested policy layer you can build on.

What we read, and what we could not confirm

We read the bill text as published on the New York Senate page for S9051B, and the Governor’s release of 28 October. We also read coverage by ID Tech, TechTimes and Fortune.

The Senate page also carries an automated summary. It disagrees with the bill text in several places, so we followed the text.

We haven’t read the chaptered law as filed, and we found no rules from the Attorney General yet. Nothing here has been tested in court. Where we map the law to engineering choices, that is our reading, and it is not legal advice.

Where the coverage and the text differ

Point What some coverage says What the bill text says
Start date 1 January 2027 (TechTimes and one other report) The 180th day after the act becomes law. Signed 28 October, so by our count 26 April 2027.
Age methods “More than one way to verify age” (Governor’s release, ID Tech, Fortune) Section 1804(1): offer “at least one method” that doesn’t rely solely on government ID, or that lets the user stay anonymous to the operator.
Private lawsuits TechTimes says the final version removed a private right of action Section 1802(1) lets a person who is injured by a violation sue. The Attorney General can also act.
The test for a minor “Users whose age isn’t confirmed” “Covered minor” means a user when the operator has actual knowledge the user is a minor. The unlock, though, asks the operator to determine the user is not one.

The last row is the one to read twice. We come back to it below.

Who is in scope

  • The product. An “advanced chatbot” is a generative AI system with a natural language interface, in writing or sound, that gives ongoing, adaptive responses to user inputs. That is wide. A general-purpose assistant fits the words. We don’t know how the Attorney General or a court will read them, and TechTimes hedges on the same question.
  • The company. A “chatbot operator” is anyone who directly or indirectly provides or makes available an advanced chatbot to covered users. That reads as including a business that builds on someone else’s model.
  • The user. A user in New York who isn’t acting as, or for, an operator. Section 1805 puts a user who is physically outside New York outside the law.
  • The exemptions. The prohibition doesn’t apply to a chatbot made available solely for customer service, information about an entity’s own products and services, or account information. It also doesn’t apply when it is solely for a business’s internal use or employee productivity. The word “solely” is doing the work. A support bot that also chats about life is not solely a support bot.

The gate is a feature gate

Section 1801(1) makes it unlawful to provide “unsafe chatbot features” to a covered user unless the user isn’t a covered minor and the operator used permitted methods to find that out. Section 1800(5) defines those features as design features that, at any point during a chat, do things like these:

  • suggest the bot is a person, or has a role such as friend, romantic partner, coach or counselor
  • suggest it is human, alive or has emotions
  • use first-person pronouns
  • frame output as personal opinions or emotional appeals
  • flatter the user, or agree with them over their safety
  • ask emotion-based questions nobody asked
  • use health or personal information from more than twelve hours earlier or from a past session
  • optimise engagement above the bot’s own safety guardrails
  • produce sexually explicit content, or content that promotes self-harm, disordered eating, drug or alcohol abuse, or secrecy from trusted adults

The Attorney General can add more by rule.

Look at what that does to the engineering. Several items are not content filters. First-person pronouns and long memory are properties of how you configure the product. Sexual content and self-harm content are things a safety filter handles for everyone, adult or not. A single “is this user verified” flag doesn’t cover both.

The age determination duty

Section 1804 sets three rules.

  1. Offer at least one method that doesn’t rely solely on government ID, or that lets the user stay anonymous to you.
  2. Use the data only to make the determination.
  3. Delete it immediately after an attempt, unless state or federal law requires you to keep it.

The law doesn’t list the methods. Section 1801(1) points to Article 45 of the same chapter and to rules the Attorney General may write. We haven’t seen those rules.

One question we can’t settle from the text: is the stored result of a check (“adult”, valid until a date) “data” that must go? We think a one-bit decision with a method label and dates isn’t the evidence the section is aimed at, but that is our reading. If you keep a decision, keep as little as possible, and ask your counsel.

Enforcement

  • Private claims. Under section 1802(1), a person injured by a violation may sue a responsible party. There is a rebuttable presumption of causation in some self-harm cases.
  • Attorney General. Injunctions, restitution, damages, and civil penalties of up to $25,000 per violation. Remedies can include destroying unlawfully obtained data and any algorithm trained on it.
  • Contract terms. Waivers and terms that shift liability are void as a matter of public policy.
  • Group companies. Affiliates that purposely and unreasonably limit or avoid liability can be jointly and severally liable.

What counts as one violation is not defined in anything we read. A violation per user, per session or per message would give very different numbers.

The “actual knowledge” problem

Read sections 1800(6) and 1801(1) together. A minor is defined by what you know. The exception that lets you offer the features is defined by what you did: you must have determined the user isn’t one, using permitted methods.

One reading is that an operator who knows nothing has no covered minors, and so no duty. The other is that the exception is the only safe route, so an operator who never determines age can’t use it. The Governor’s release and most coverage describe the duty as verifying age, which fits the second reading. We would build for it. It costs less to be wrong that way.

The build: three states and a feature table

The policy layer is small. It turns what you know about a user into a yes or no for each gated feature.

  • State. adult, minor, or unknown. Unknown is the default, and it is what you get when a decision expires.
  • Features. One flag per design feature in the list above, so the same code can turn off first_person_voice and long_memory without touching the chat.
  • Disclosure beats a stored result. If a user says in the chat that they are 15, the state is minor, whatever was stored before. After that, you have actual knowledge.
  • Location. Treat an unknown location as New York.
  • The record. Four fields: state, method, when decided, when it expires. No image, no document, no birth date, no score.

Here is the code. We ran the tests with node --test on Node 22: 8 tests, all passing.

export type AgeState = "adult" | "minor" | "unknown";
export type Jurisdiction = "ny" | "other" | "unknown";

// One flag per design feature we map to S9051B section 1800(5). The mapping is ours.
export const GATED_FEATURES = [
  "relationship_persona",    // (A)(I)   romantic partner, friend, coach, counselor
  "human_claims",            // (A)(II)  suggests it is human, alive, has emotions
  "first_person_voice",      // (A)(III) first-person pronouns
  "opinions_and_appeals",    // (A)(IV)  personal opinions, emotional appeals
  "flattery_tuning",         // (A)(V)   flattery or sycophancy over safety
  "unprompted_emotion",      // (A)(VI)  emotion-based questions nobody asked
  "long_memory",             // (A)(VII) personal data older than 12 hours or from past sessions
  "engagement_tuning",       // (D)      engagement optimisation above guardrails
] as const;
export type Feature = (typeof GATED_FEATURES)[number];

// What we keep after a check. No image, no document, no birth date, no score.
export interface AgeDecision {
  state: "adult" | "minor";
  method: string;      // e.g. "browser_check"
  decidedAt: string;   // ISO time
  expiresAt: string;   // ISO time
}

export interface Context {
  decision: AgeDecision | null;
  jurisdiction: Jurisdiction;
  statedAge?: number;  // something the user said in the chat
  now: Date;
}

export function ageState(c: Context): AgeState {
  if (c.statedAge !== undefined && c.statedAge < 18) return "minor"; // we now know
  const d = c.decision;
  if (!d) return "unknown";
  if (c.now >= new Date(d.expiresAt)) return "unknown";
  return d.state;
}

export function features(c: Context): Record<Feature, boolean> {
  const outside = c.jurisdiction === "other";
  const allowed = outside || ageState(c) === "adult";
  return Object.fromEntries(GATED_FEATURES.map((f) => [f, allowed])) as Record<Feature, boolean>;
}

// Turn a vendor result into the record we keep. Anything else in the result is dropped here.
export function recordDecision(
  result: { adult: boolean; method: string; [extra: string]: unknown },
  now: Date,
  ttlDays = 30,
): AgeDecision {
  return {
    state: result.adult ? "adult" : "minor",
    method: result.method,
    decidedAt: now.toISOString(),
    expiresAt: new Date(now.getTime() + ttlDays * 86_400_000).toISOString(),
  };
}
import { test } from "node:test";
import assert from "node:assert/strict";
import { features, ageState, recordDecision, GATED_FEATURES, type AgeDecision, type Context } from "./policy.ts";

const now = new Date("2026-11-04T09:00:00Z");
const adult: AgeDecision = { state: "adult", method: "browser_check", decidedAt: "2026-11-04T08:00:00Z", expiresAt: "2026-12-04T08:00:00Z" };
const base: Context = { decision: null, jurisdiction: "ny", now };
const on = (c: Context) => GATED_FEATURES.filter((f) => features(c)[f]);

test("unknown user in New York gets none of the gated features", () => {
  assert.deepEqual(on(base), []);
});

test("confirmed adult may be offered all of them", () => {
  assert.equal(on({ ...base, decision: adult }).length, GATED_FEATURES.length);
});

test("confirmed minor gets none", () => {
  assert.deepEqual(on({ ...base, decision: { ...adult, state: "minor" } }), []);
});

test("an expired decision falls back to unknown, and the features go off", () => {
  const c = { ...base, decision: adult, now: new Date("2026-12-05T00:00:00Z") };
  assert.equal(ageState(c), "unknown");
  assert.deepEqual(on(c), []);
});

test("a stated age under 18 beats a stored adult decision", () => {
  const c = { ...base, decision: adult, statedAge: 15 };
  assert.equal(ageState(c), "minor");
  assert.deepEqual(on(c), []);
});

test("unknown location is treated as New York", () => {
  assert.deepEqual(on({ ...base, jurisdiction: "unknown" }), []);
});

test("a user known to be outside New York is not gated by this policy", () => {
  assert.equal(on({ ...base, jurisdiction: "other" }).length, GATED_FEATURES.length);
});

test("the stored decision drops everything but four fields", () => {
  const d = recordDecision({ adult: true, method: "browser_check", selfie: "base64...", dob: "2001-01-01", score: 0.93 }, now);
  assert.deepEqual(Object.keys(d).sort(), ["decidedAt", "expiresAt", "method", "state"]);
  assert.equal(d.state, "adult");
});

Three notes on using it.

  1. Unknown means safe mode, not a block. The conversation still works. The gated features are off. A user who never verifies still gets a chatbot.
  2. The flags have to reach the model. A flag in a database does nothing if the system prompt, the memory store and the persona layer ignore it. Turning off first_person_voice means a different instruction set for that session, and probably a post-generation check. We haven’t tested how well any specific model follows such an instruction, so measure it on your own output.
  3. Filters stay on for everyone. Sexual content and self-harm content aren’t toggles. This policy sits beside your safety filters, not in place of them.

Where the decision comes from is a separate problem. If you have several age signals, the post on conflicting age signals covers how to pick one. How long a decision should last is in the post on re-verification cadence. A method that doesn’t need an ID is the subject of the no-document ladder post.

What it could cost

This is an example, not customer data. Say 50,000 New York users a month reach the gated features. Each takes a browser Check, which costs €0.01 to €0.03 on a paid plan. That is €500 to €1,500. Say 15% of them (7,500) can’t be cleared by a Check and need a document Verification at €0.10 to €0.30. That is €750 to €2,250. The total is €1,250 to €3,750 a month.

The 15% is an assumption. So is the idea that a decision lasts 30 days and a user is checked about once a month. A longer life cuts the cost and raises the risk of a stale answer. Users who stay in safe mode cost nothing, and some will choose to.

Checklist for this week

  1. Decide whether you are an operator under the definition, and whether either exemption applies. Check the word “solely”.
  2. List every design feature of your chatbot that matches section 1800(5). Include memory, persona, voice and engagement tuning.
  3. Give each one a flag, and a default of off for unknown.
  4. Offer at least one age method that doesn’t rely only on government ID, or that keeps the user anonymous to you.
  5. Keep the decision, not the evidence. Delete the evidence right after the attempt.
  6. Decide what a user can say in chat that moves them to minor, and what happens to their memory when it does.
  7. Set a start date for your build against 26 April 2027, and a date to check for Attorney General rules.
  8. Test the unknown-user path on your real stack, with the memory store switched on.

The short version

New York didn’t ask chatbots to check everyone at the door. It said that some features can’t be offered to a user you haven’t determined to be an adult. Build the features as flags, default them off, turn them on per user from a short-lived decision, and keep the evidence out of the record. Read the text, not the summary. Where this post relies on our reading, we’ve said so.

For the wider picture, see our posts on state chatbot age-assurance bills, companion AI laws and the Texas case against OpenAI.


Xident provides age verification and age estimation with a decision record on every check. A browser Check is the lower-cost first step, and a document Verification is for users who need a stronger answer. On paid plans a Check costs from €0.01 to €0.03, and a document Verification costs from €0.10 to €0.30. We make no claim about what New York will accept. Talk to us about a per-feature age gate for your chatbot, or see pricing.

Share this article

Ready to implement age verification?

Get started in minutes with our simple SDK. Free trial includes 100 verifications.

Book a 20-minute demo