A consent gate that says no by default, 10DLC registration, a STOP that really stops, and a fence around test data. Not legal advice.
Swipe or tap Next · checked on 2026-10-06
Open Claude Code in your app's folder and paste this. It reads, reports and proposes. It doesn't change anything until you say so, and it never sends a message.
Start Claude in your app's folder
cd <your app's folder>
claudeThe audit prompt (copy all of it) · lines 1-2 of 10. Copy all copies the whole prompt; read it in full on the page.
Audit this app for the four checks in this guide, read-only:
https://receiptsgroup.com/guides/ai-app-texts-callsAudit this app for the four checks in this guide, read-only:
https://receiptsgroup.com/guides/ai-app-texts-calls
Find every place the app can send a text, place a call, or hand a contact to another system. For each one, tell me:
1. Consent gate: what decides this contact may be texted or called? Does it say no by default? Can an imported or purchased list pass it? Do we store who agreed, when, where, and the exact words they agreed to?
2. 10DLC: are texts sent from a 10-digit number, and is there anything that stops sending until the numbers are registered and attached?
3. STOP: which words opt someone out, is the opt-out checked before every text AND every call, and how fast does it take effect?
4. Test data: can a test or fake contact ever reach a real CRM, a real phone number, or a real customer's account?
Answer as a table: place in the code, check, what it does now, gap, the smallest fix. Don't change any files, don't run anything that sends, calls or posts, and don't print any keys or phone numbers. Then wait for me.This is the manual way: each check, why it exists, and how to build it. Links to the rule text are under Sources. Plain-English summaries, not legal advice.
States add their own rules on top (some on calling hours, some on recording calls). Check yours.
In ours, the rule is written right into the code: a contact that arrived by import is not consent. Otherwise a spreadsheet with a column called “consent” launders a bought list straight into your texting. The gate starts at no and only one thing turns it to yes: a real person agreeing on your form, recorded.
Example consent wording (show it to your lawyer; not legal advice)
[ ] By checking this box, I agree that <Your Business Name> may call and text me at the
number above about <what you'll contact them about>, including marketing messages sent
using automated technology. Agreeing is not a condition of any purchase. Message
frequency varies. Message and data rates may apply. Reply STOP to opt out, HELP for help.
<link to your privacy policy> <link to your terms>The gate, as code (Python example; same idea in any language)
def may_contact(contact, channel, suppressed):
"""True only when this contact has a recorded, form-given yes and hasn't opted out. Default: no."""
c = contact.get("consent") or {}
if contact.get("phone") in suppressed: # Check 3: STOP wins, on every channel
return False
if contact.get("is_test"): # Check 4: test data never contacts anyone
return False
if c.get("source") != "web_form": # imports, purchases and CSV columns are not consent
return False
return bool(c.get("agreed_at") and c.get("wording_version") and c.get("page")
and channel in (c.get("channels") or []))def may_contact(contact, channel, suppressed):
"""True only when this contact has a recorded, form-given yes and hasn't opted out. Default: no."""
c = contact.get("consent") or {}
if contact.get("phone") in suppressed: # Check 3: STOP wins, on every channel
return False
if contact.get("is_test"): # Check 4: test data never contacts anyone
return False
if c.get("source") != "web_form": # imports, purchases and CSV columns are not consent
return False
return bool(c.get("agreed_at") and c.get("wording_version") and c.get("page")
and channel in (c.get("channels") or []))Then make every send path call it: the app, the automations, the dialer, any script. One gate, no side doors. Ask Claude to find the side doors; that's what the audit prompt is for.
10DLC is the system US carriers use to know who's texting from a regular 10-digit number and why. You register a brand (your business) and a campaign (what you text about), then attach your numbers to the campaign. People review it by hand, and nobody tells you how long that takes. Ours: brand approved fast, campaign created the same day, numbers still waiting on the carriers over a week later.
Your legal business name, tax ID, address and website, exactly as they appear on your registration. Mismatches are the usual reason for a rejection.
Pick the use case that matches what you actually send. Write sample messages that look like your real ones, describe how people opt in (link to the form with the consent wording above), and list your opt-out and help keywords. Vague campaigns get rejected.
Assign the numbers you'll text from to the campaign. Until the provider shows them as attached and active, don't send.
Put a switch in the app that keeps the texting lane off until the numbers are attached, and check it on every send. “It'll probably go through” is how you get blocked numbers.
The switch (example)
TEXTING_LIVE = False # flip to True only after the provider shows every number attached to the campaign
def send_text(contact, body, suppressed):
if not TEXTING_LIVE:
return "held: 10DLC not attached"
if not may_contact(contact, "sms", suppressed):
return "blocked: no consent or opted out"
... # your provider's send callThe rules give you up to ten business days to honor an opt-out. Your code can do it in ten seconds, so do. One suppression list, checked before every text and every call, because a STOP by text revokes calls too.
Inbound handler (example)
OPT_OUT = {"stop", "quit", "end", "revoke", "opt out", "optout", "cancel", "unsubscribe", "stopall"}
def on_inbound_text(phone, body, suppressed):
words = body.strip().lower()
first = words.split()[0] if words else ""
if words in OPT_OUT or first in OPT_OUT:
suppressed.add(phone) # save it to your database, not just memory
return "You're unsubscribed and won't get more messages from <Your Business Name>."
if words == "help":
return "<Your Business Name>: <how to reach you>. Reply STOP to opt out."
return NoneOPT_OUT = {"stop", "quit", "end", "revoke", "opt out", "optout", "cancel", "unsubscribe", "stopall"}
def on_inbound_text(phone, body, suppressed):
words = body.strip().lower()
first = words.split()[0] if words else ""
if words in OPT_OUT or first in OPT_OUT:
suppressed.add(phone) # save it to your database, not just memory
return "You're unsubscribed and won't get more messages from <Your Business Name>."
if words == "help":
return "<Your Business Name>: <how to reach you>. Reply STOP to opt out."
return NoneAI-built apps get tested a lot, and fast. We once had our test runner push a fake lead into a live CRM after delivery went live. We caught it and removed it by hand, which is luck, not design. Now there's a fence: test contacts are marked, and every path that touches a real system refuses them.
The fence (example)
def is_test(contact):
return bool(contact.get("is_test")) or contact.get("email", "").endswith("@example.com")
def deliver(contact, destination):
if is_test(contact) and destination.get("production"):
raise RuntimeError("refused: test contact to a live system")
...1. Find the side doors
Search this codebase for every function, webhook, cron job or script that can send a text, place a call, send an email, or create a contact in another system. List each with its file and line, and whether it calls our consent gate first. Read-only.2. Add the gate where it's missing
For each send path you listed without the consent gate, show me the smallest change that calls may_contact() first and refuses on False. One path at a time, show me the diff, wait for my yes.3. Prove STOP works
Write a test (no real sends; mock the provider) that: texts a test contact, receives "Stop" from that number, then tries to text AND call it again, and asserts both are refused. Run it and show me the result.4. Check the consent record
Show me what we store when someone checks the consent box: every field, an example row with fake data, and anything missing compared with the list in this guide's Check 1. Read-only.Check the numbers are attached to an approved 10DLC campaign in your provider's portal. Unregistered 10DLC traffic is blocked.
Read the reason. Usually: sample messages that don't match the use case, an opt-in description without the consent wording, or business details that don't match your registration.
A send path skips the suppression list. Run real-use prompt 1, then prompt 3 until it passes.
Not by your form, so not by your gate. Talk to your lawyer before using it at all.
That's a lawyer question, not a code question. Bring them the exact wording and a screenshot of the form.
What each page backs up is listed at the end of the full guide.
No by default, registered before you send, STOP in seconds, and test data that can't get out. None of it is a feature anyone asks for, and all of it is what keeps the app you built in a weekend from becoming a problem you deal with for a year.
Not legal advice: have a lawyer read your consent wording and setup before your first message.
Want help setting this up? Receipts Group builds these systems.
Open the full guide as a page (every file in full, plus the table of contents).