Product case study · AI feature

When “I don’t want your texts anymore” doesn’t say STOP

At Maestra.io, a Klaviyo/Attentive-style marketing platform, SMS opt-out only worked on keywords. I designed and prototyped an LLM layer that catches natural-language refusals, with guardrails so it never acts alone on a shaky guess.

Role: Product ManagerPrototype: Python + Claude HaikuData: 450 synthetic replies + 104 hard cases

The problem

Customers who want to stop receiving texts don’t always type STOP. They write what they mean. The keyword system saw no keyword and kept them subscribed.

Flash sale! 30% off today. Reply STOP to opt out.
I don’t want to receive messages from you anymore
no keyword found → still subscribed

Why it matters

  • The customer asked clearly and was ignored. That is a compliance risk for the brand.
  • Ignored requests turn into complaints and spam reports, which can get the sender filtered by carriers.
  • Every message to someone who wants out is wasted spend.

The opposite mistake has a cost too. “Don’t stop the sales emails” and “how do I cancel my order” contain keywords and are not opt-outs. A fix that removes people who still want messages would lose revenue.

What I designed

Keyword rules still run first because they are fast and exact. Everything else goes to a small, cheap model that returns a label and a confidence. The model never opts anyone out by itself. Its confidence decides what happens next.

1. Inbound replyAny SMS reply from a subscriber
2. Keyword rulesA bare STOP-style reply is final
3. LLM classifieropt_out, not_opt_out or ambiguous, plus confidence
4. RoutingThresholds pick the action
Model saysAction
opt_out, confidence ≥ 0.90Opt out automatically
opt_out, confidence 0.60 to 0.90Send a confirmation SMS: “Reply YES to unsubscribe”
ambiguous, or low confidenceHuman review queue
not_opt_outNo action

The tradeoff behind the thresholds

A missed opt-out is worse than a wrongly removed subscriber: one is a compliance and complaint problem, the other is a lost sale that confirmation can often recover. So the automatic threshold is high, and uncertain cases get a question or a human instead of a guess. The reasoning and the failure-mode table are in the risk doc of the repo.

The prompt also treats each reply as untrusted data. A customer can’t instruct the classifier, and the output is limited to three labels.

How I tested it

Real Maestra data is confidential, so I generated 450 labeled replies in English, Russian and Spanish. The set includes the hard cases: refusals with no keyword (135), keyword traps like “cancel my order” (45), and ambiguous replies like “not interested” (42). The headline metric is recall on opt-outs with no keyword, since that is the gap in the old system. The baseline is the keyword rules. I split the data by phrase family into a dev set for tuning (275 messages) and a held-out test set (175), because many messages are small variations of one phrase and a random split would leak.

Results

Catches refusals that have no keyword

Share of 135 soft refusals detected as opt-outs
Keywords
0%
Keywords + LLM
89%

Gets fooled by keyword traps

Share of 45 non-opt-outs containing a keyword that were treated as opt-outs
Keywords
100%
Keywords + LLM
2%

Overall opt-out precision and recall went from 0.56 and 0.30 to 0.99 and 0.92. On the held-out test messages, recall on opt-outs with no keyword is 84% (42 of 50, plausible range 71% to 92%), so the headline number has real uncertainty at this sample size.

What routing did with the 195 true opt-outs

OutcomeCount
Opted out automatically149
Sent a confirmation SMS31
Sent to human review15
Ignored0

At the 0.90 threshold no non-opt-out reply was opted out automatically. The only false positive, “Quit it with the flash sales lol”, scored 0.75, so it triggered a confirmation message instead. The 15 human-review cases were borderline wording like “I’m done with these texts”, plus every “I never signed up for this garbage” reply, which the model scores as ambiguous. The next section covers the fix. The cost on the other side: 25 of 213 non-opt-outs, mostly deferrals like “Not now”, also go to review, which is wasted work for the queue. All figures in this section are from prompt version 1.

Choosing the automatic threshold

100%75% 50%25% 0% 0.90 chosen: 76% 0.500.600.70 0.800.850.90 0.950.99 auto opt-out threshold

Share of true opt-outs handled automatically. The rest go to confirmation or review, so nobody is dropped. False opt-outs are 0.4% at thresholds up to 0.70 and 0% from 0.80 up. I set 0.90 in the design before running the test. The results support it: it sits well above the 0.70 level where the first false opt-out appears, and the cost is that about one in four opt-outs gets a confirmation text or a review first.

Reading the failures and fixing them

I read all 52 misses from version 1, wrote one sentence on each, and grouped them into causes. Most were not “the model is weak”:

I also wrote 104 harder messages (typos like “STPO”, sarcasm, look-alikes like “remove my card”, prompt-injection attempts, requests buried in long messages, threats). Then I rewrote the prompt against the dev split only and scored the held-out test split once. Version 2 was a dead end: one clause I added made the model over-cautious and the hard-set score fell from 59 to 54 correct. Version 3 removed it.

Held-out test splitPrompt v1Prompt v3
Recall on refusals with no keyword (50)84%100%
Real opt-outs sent to human review (of 72)80
Harmless messages sent to review (of 90)123
Non-opt-outs opted out or sent a confirmation00
Hard set: correct labels (of 41)3740

Cost is about $0.00009 per classified message (illustrative at 2026-11 list prices for the small model). Latency is about 1 second at the median and 2 seconds at the 95th percentile, which is fine because nobody is waiting on an opt-out.

Read 100% as a ceiling. I looked at test misses while analyzing version 1, and the consent rule comes from a labeling policy built on a sample from the test split. The data is template-generated, so one rule fixes a whole family. Fifty messages at 100% still allows a true rate near 93%. I then ran the fair check: a fresh batch of 63 messages I wrote before any model saw them. Version 3 labeled 88% of the 34 opt-outs correctly (plausible range 73% to 95%). Routing sent 26 to automatic opt-out, 4 to a confirmation text and 4 to a person, ignored none, and opted out no harmless message. Three of the four misses are replies like “wrong number” that my blind hand label would remove but my written policy sends to review. That is the number to trust, and real replies in shadow mode are the next check.

Two open questions remain, flagged for a second labeler and compliance: “Enough already” messages that my labels call ambiguous but version 3 treats as requests to stop (some reach automatic opt-out), and whether “Delete my account” should remove SMS consent.

What I got wrong first

Try it

Pick a message to see what the classifier did with it. These are recorded answers from real runs of prompt version 3 on messages it had not been tuned on, including the ones it got wrong or routed to a person. Nothing here calls a model.

Select an example above.

Limits and next steps

Read these numbers as an upper bound. The dataset is synthetic and built from templates, so many rows are close variants of each other. Real inbound SMS is messier. The keyword baseline’s 0% is true by construction, because the refusal rows contain no keyword.

The rollout plan answers this by measuring on real traffic before acting on it:

Not legal advice. A compliance review would need to confirm the opt-out rules for each market before launch.