Cleared to Send · Build notes

Why I built Cleared to Send: the 10DLC help small businesses never get

Decode the rejection, fix it, and stay ready. Why I built it, where Claude does and doesn't belong in a compliance product, and what it taught me about SEO, Cloudflare, and pricing.

Published
October 4, 2026
The short version
  • I built Cleared to Send for the business with no compliance person. Getting cleared to text customers in the US means 10DLC registration, and rejections cost weeks. Big companies have teams to get through it. The dental office, the salon, and the plumber don't.
  • My favorite piece is the decoder. It takes the one vague line a provider sends back and explains it phrase by phrase, in plain English, with the fix.
  • I learned how to keep an LLM honest in a compliance product. Claude reviews like a carrier would, but against a written list of 14 known rejection reasons, with deterministic checks underneath it and a promise of "confidently," never "guaranteed."
  • SEO is the whole acquisition plan. I left a client-rendered prototype so every page could be crawled, then generated over 100 indexed pages from the same data the product runs on.
  • Two things were harder than the code: Cloudflare and pricing. Edge settings quietly broke SEO and my scheduled jobs. Pricing took three tries to land somewhere that covers costs and still feels fair to a small business.

Why I built it

Before Cleared to Send, I spent years on the product side of an SMS platform at a large enterprise SaaS company. A big part of that job was helping customers get through A2P 10DLC registration with The Campaign Registry (TCR) and the carriers.

If you've never done it: before a business can text customers from a regular 10-digit number, it has to register its brand and each "campaign" (what it sends and why). A reviewer checks the business identity, how people opt in, the sample messages, the privacy policy, and more. Get one of those wrong and the campaign comes back rejected, usually with a single vague sentence. Then you fix it, resubmit, and wait again. Each loop costs weeks.

At enterprise scale, I watched customers struggle with this even with help. They had me, a support team, and account managers who had seen hundreds of these. We knew what "unable to verify opt-in" actually meant. We knew which sample messages would pass and which would bounce. Even then, getting a campaign across the line took real work.

Then I thought about the business doing this alone. Providers like Twilio make it easy to buy a number and hard to understand why your registration bounced. There's a form, a rejection email, and a help center article. There's no one to say, "Here's what the reviewer was looking for, and here's the exact sentence to add to your booking page."

That's the gap. Cleared to Send gives a small business the help an enterprise customer gets. It never sends messages and never submits anything for you. It makes sure you're cleared and compliant before you do.

The Cleared to Send homepage: paste your rejection notice and find the fix
The Cleared to Send homepage

The product follows a lifecycle, and the name says it: Get Ready → Send → Stay Ready.

  • Get Ready: Clear walks you through a first registration and audits the package before you submit. Fix takes a rejection and rebuilds a corrected package.
  • Send: write, check, and compare messages before they go out.
  • Stay Ready: rules change, so a weekly scan finds rule updates and emails the businesses they affect.

My favorite moment: the decoder

The thing I'm proudest of is small. It's one section of the homepage.

When a campaign is rejected, the provider passes along one line from the carrier review. Something like:

Campaign rejected. Unable to verify opt-in: CTA does not clearly describe how end users consent to receive messages.

To someone who has done this a hundred times, that sentence is packed with meaning. To a dental office manager, it's noise. So the decoder breaks it apart phrase by phrase and says what each phrase actually means:

The decoder: each phrase of a rejection notice, translated into plain English
The decoder: each phrase, translated

"Unable to verify opt-in" means the reviewer opened your website and couldn't find where people agree to texts. "CTA does not clearly describe" means your sign-up wording needs your business name, what you'll send, and how to stop.

That's the knowledge I carried around in my head for years, turned into something anyone can read in five seconds.

The markup is simple on purpose. Each highlighted phrase gets a footnote number, and each number gets a plain-English meaning:

index.astro
<p class="decode-notice">
  Campaign rejected. <span class="v3-hl">Unable to verify opt-in</span><sup>1</sup>:
  <span class="v3-hl">CTA does not clearly describe</span><sup>2</sup> how end users
  consent to receive messages.
</p>
<div class="decode-means">
  <div><span class="v3-label">1 · MEANS</span>
    <p>The reviewer opened your website and couldn't find where people agree to texts.</p></div>
  <div><span class="v3-label">2 · MEANS</span>
    <p>Your sign-up wording needs your business name, what you'll send, and how to stop.</p></div>
</div>
The decoder on a phone
The decoder on a phone

The PM lesson inside it: the first job is translation, not diagnosis. I originally built the product around the audit and the corrected package. Those are the valuable outputs. But a scared, rejected business owner doesn't trust a corrected package until they understand what went wrong. The decoder earns that trust first. It's why the whole homepage now starts with "paste your rejection" instead of "start a registration."

Skill 1: Keeping an LLM honest in a compliance product

This is the skill I'm most proud of learning on this project.

An LLM is great at reading a messy opt-in description and judging whether a reviewer would accept it. It's also capable of confidently inventing a rejection reason that doesn't exist. In compliance, that second thing is a real cost: a business could spend another two to four weeks fixing the wrong problem.

So I split the work by what each part is good at.

Judging opt-in language, sample messages, and the overall package
Claude
Needs reading comprehension and judgment
Mapping a real rejection to its cause and rebuilding the package
Claude
Same reviewer logic, run in reverse
EIN format and IRS prefix checks
Code, no LLM
Has a right answer
HELP and STOP replies, privacy policy wording
Code templates
Needs to be correct and correctly formatted every time
The free rejection lookup
Code, in the browser
Instant, free to run, nothing stored
Who can export what
Code, on the server
Billing can't be up to a model

A written rubric, not open-ended judgment

Claude doesn't freelance. Every review runs against the kill-list: a written list of the 14 recurring reasons 10DLC campaigns get rejected, each with what the reviewer checks and how to fix it. It's the same list the public rejection pages are built from, so what the product checks and what the site teaches always match.

killlist.ts
{
  id: "optin-missing",
  title: "Opt-in / consent not described",
  severity: "critical",
  description:
    "The #1 rejection cause. The campaign must describe exactly how end users consent to receive messages, and the consent must be verifiable.",
  remedy:
    "Describe the opt-in method in detail (web form, keyword, point of sale, paper form) and capture a screenshot of the exact CTA. ...",
},

Claude acts as the reviewer, checks the package against every rule, and has to answer in a strict structured format: a verdict, plus a finding and a fix for each rule. One constant sets the model for the whole product, and every surface (Clear, Fix, Send) calls the same engine.

A deterministic floor under the model

Some things aren't judgment calls. An EIN with an invalid IRS prefix is wrong no matter what the model thinks. So that check runs in plain code after the model answers, and it wins:

engine/index.ts
// Put our deterministic finding first; drop any prior copy of it.
const findings = [finding, ...result.findings.filter((f) => f.id !== finding.id)];
// Don't let the package read "pass" while we're flagging a likely-bad EIN.
const verdict = result.verdict === "pass" ? "warn" : result.verdict;

The bug that taught me the most

The Fix flow asks for a few optional details. Early on, if someone skipped a field, it reached Claude as "(not provided)." Claude read that as "this was missing from their carrier submission" and listed it as a rejection cause. A business would paste a rejection about opt-in and get back three causes, two of them invented.

The fix was partly wording and partly a rule. Skipped fields now say "(not shared with us)," and the prompt states that this is unknown, not missing. Then a grounding rule that I now think every diagnosis prompt needs:

fix-prompts.ts
Ground every cause in evidence. List a cause only when the verbatim rejection
reason names or clearly implies it, or when something the business actually
shared shows the problem. ... If the rejection names one problem, return one cause.

The lesson: the model treats everything in its context as evidence. If you hand it a placeholder, you've handed it a claim.

"Confidently," never "guaranteed"

No one can promise a carrier will approve a campaign, and I won't pretend otherwise. The product says so right where the verdict appears:

This is a pre-submission estimate based on the known 10DLC rejection reasons, not a guarantee of carrier approval. Carriers and TCR make the final decision, and their criteria can change.

That line is a product decision, not legal boilerplate. Honest confidence is the brand.

Skill 2: SEO as the acquisition plan

Cleared to Send started as a proof of concept on Base44. It proved the scoring idea worked. It also rendered everything in the browser, which meant search engines saw an almost empty page.

For this product, that was fatal. People don't search "10DLC compliance software." They search the exact words of their rejection, usually within a day of getting it. If I'm not on that results page, I don't exist.

So the rebuild runs on Astro, where marketing pages are prerendered HTML and only the app screens run on the server. Then I generated the content from the same data the product uses:

  • One page per rejection reason, built from the kill-list
  • Provider pages for Twilio, Telnyx, and Bandwidth, plus a page for each provider and reason pair
  • Error-wording pages for people who paste the exact text they got
  • Plain-English guides and industry pages (dental, salons, medical practices, and more)

That's over 100 indexed pages, and adding a new rejection reason to the kill-list ships a new page with its own social card automatically.

The piece I'd point any builder to is the free rejection lookup. Paste your notice and it matches it to a reason instantly. It runs entirely in the browser: no login, no API call, nothing stored, and it costs me nothing to run. That makes it the one thing I can hand anyone, anywhere, without asking for anything first.

The free rejection lookup, prefilled from a shared link
The free rejection lookup, prefilled from a shared link

It also takes a ?q= link. When I answer someone's rejection question in a forum, I paste their text into the lookup and share a link that opens already matched. The link carries a tracking tag so I can see which answers actually bring people in.

One discipline I hold to: the error-wording pages never claim a specific carrier's exact string as fact. They say "your rejection said something like this," and I keep checking the wording against the real rejections people bring in.

The hard part, round one: Cloudflare kept getting in the way

The app runs on Cloudflare Workers. It's fast and cheap, and the edge sits in front of everything. That last part bit me three times.

Duplicate pages in Search Console. I wanted www to redirect to the bare domain. I set up the redirect rule and it never fired. A Worker attached directly to a hostname skips redirect rules entirely. Search Console flagged the duplicates before I did. The fix was to detach www from the Worker so the redirect rule could actually run.

Redirects on every crawl. The site serves URLs with a trailing slash. My canonical tags and internal links didn't use one, so every crawler hop went through a redirect, and the canonicals disagreed with the sitemap. Small mismatch, real SEO cost. Now every URL is written the same way everywhere.

Bot protection blocked my own scheduled jobs. The daily lifecycle emails and the weekly rule scan used to run from a scheduler that called my own public URL. One day they stopped. Cloudflare's zone-wide bot protection had started challenging the scheduler's datacenter IP, and the request never reached my code. The tell was the status code: my own auth failure returns a 401, and these were 403s. That setting can't be turned off for a single path.

So I stopped making the call. Scheduled work now runs inside the Worker itself:

worker.ts
// A Cron Trigger runs inside the Worker: no public request, nothing to block,
// and no shared secret to keep in sync with GitHub.

The lesson I wrote into the project notes: fix the class of problem, not the instance. Anything that has to reach the app machine-to-machine sits behind the same edge, so I check that path first now.

(Bonus round: Safari wouldn't play the homepage tour video, because static files on the edge ignore the "send me just this byte range" requests Safari depends on. The video now goes through the Worker so it can answer them.)

Skill 3, and the hard part, round two: pricing

Pricing was the hardest product decision on this project. Two goals pulled against each other: cover real operating costs, since every review and diagnosis is a paid model call, and stay within reach of a business that might be texting appointment reminders from a single location.

I went through three versions.

  1. The spec (July): a one-off credit around $79, a three-pack, and a ~$39/month Pro plan with a credit every quarter.
  2. Live billing (August): the same shape at $79.99, three for $199.99, and $39.99/month. I spent a whole pass just making the credit understandable: "One credit = one registration, exported and submit-ready."
  3. Free-first beta (late August, live now): diagnosis of any rejection is free. Your account and your first corrected package are free. Each package after that is $19.99 one-time. No subscription to sell.
Pricing today: a free workspace and $19.99 per additional registration
Pricing today

What changed my mind: the early goal isn't revenue, it's completed submissions with known outcomes. Every "did it pass?" answer sharpens the kill-list, which makes the product better for the next business. A subscription asks someone to commit before they've seen the product work. A free first package lets them see it work.

Three things made free-first safe to run:

  • The paywall lives on the server. The first version assembled the submit-ready package in the browser, which meant a "pay to unlock" screen could be bypassed by reading the page. Now the package is built on the server from the saved registration, behind a credit check.
  • The free things that get shared are free to run. The lookup runs in the browser and never touches the model.
  • Every model-backed endpoint is rate limited per person, so one bad actor or a runaway bug can't run up the bill.
api/clear/export.ts
// Server-authoritative export. Assembles the submit-ready 10DLC package from the
// persisted registration row (not from browser state) so a paid gate can't be
// bypassed by reading the finished text out of the page.

The PM lesson: price for the behavior you need right now. Right now I need businesses to finish a registration and tell me what happened. The pricing should get out of the way of that.

Working with Claude and Codex

I build with AI coding agents, and on this project the split was clear.

Moving off Base44 to a self-hosted Astro app on Cloudflare
Codex
Led the initial transition and hosting setup
The engine prompts (audit, diagnosis, sample messages)
Claude
Writes the reviewer logic and the guardrails around it
The redesign (the "Clearance" design system, every page)
Claude
Designed and built it, and checked every indexed page kept the same SEO
Pricing strategy and the roadmap
Claude
My thinking partner on where to go next and how to charge

After the move off Base44, nearly all of the work has been with Claude. That includes the decisions, not just the code: Claude helped me work through three versions of pricing and then built the one we landed on. The skill I'm building is knowing which agent to hand which job, and re-checking that as the models change.

One habit from the redesign I'll keep: a visual redesign isn't allowed to change SEO. Every indexed page was checked against the old build for title, description, canonical, structured data, heading order, links, and copy. The design changed. What Google sees didn't.

What I learned

Translate before you diagnose.
People don't trust a fix until they understand the problem. The decoder does that job first.
An LLM in a compliance product needs a rubric and a floor.
The kill-list keeps the model on-topic. Deterministic checks catch what has a right answer. Honest copy says what neither can promise.
The model treats everything in its context as evidence.
Placeholders, empty fields, and stray labels all read as claims. Ground every output in what the user actually said.
Your infrastructure is part of your SEO.
Redirect rules, trailing slashes, and bot protection all decide what a crawler sees, long before your content does.
Price for the behavior you need.
Free-first isn't generosity. It's how I get the outcome data that makes the product better.

What's next

  • First customers. The product is live. Now I need to learn whether ready-to-submit packages actually cut down the rejection loop.
  • Close the feedback loop. Every approved or rejected submission reported back makes the kill-list sharper.
  • Verify the EIN before TCR does. A real IRS name and EIN match would turn one of the biggest "we can only warn you" cases into "we checked."
  • More channels. WhatsApp templates and business verification next, then email authentication (SPF, DKIM, DMARC).

If you're a PM learning to build with AI, or a builder thinking about where an LLM belongs in a product with real consequences, I'd love to compare notes.

Try Cleared to Send ↗Look up a rejection free ↗Back to the Cleared to Send case study →

a hidustin labs. creation

© 2026 hidustin labs.

hidustin.fyi · v2.0

✌️