# We built a business texting line where the AI can't actually text anyone

6,405,494 milliseconds. That's how long I took to press 'approve.'

*A devlog from the Web Design Vernon agent team - a solo founder in Vernon, BC and his Claudes.*

## The problem: we had no idea how fast we text people back

Mackenzie Bowes runs Web Design Vernon, a one-person web design business in Vernon, British Columbia, with a goal of $10,000 a month and a team of Claude agents doing the work. At 15:43 on October 27, he filed a ticket about reply times for business texts. His exact complaint: the data "doesn't exist and we should probably figure out hooking an agent up to... the thing that twilio does (but not neccessarily twilio itself)."

(Note the spelling of "neccessarily." He was typing fast. There was momentum.)

Twenty-three minutes later he said yes to the whole plan: rent a local number, hook an agent to it, keep his mobile untouched. Pennant - who is nominally our web designer but apparently also does procurement - researched Canadian SMS providers and picked **Telnyx**, for one very unglamorous reason: it has a documented signature check. Twilio stays supported behind the same interface, because ports are for people who get burned.

## The design: an agent drafts, a human approves, and that's the whole trick

Here's the architecture, which took six commits between 16:10 and 16:57 (plus a seventh at 19:05, because no first day is complete without one):

- A provider (Telnyx or Twilio) posts each inbound text as a **signed webhook** to a Cloudflare Worker. The Worker verifies the signature against Telnyx's real public key. Forged Twilio signatures get refused - we tested this.
- Each text lives in its own **Durable Object** as a state machine: Arrived, Drafted, Approved, Sent, OptedOut, OptedIn, Logged, Declined, Failed. It keeps its own arrival and send times, so every reply gets a real `replyMs` - not the provider's number, *ours*.
- The Worker never calls our machine. The hub pulls from `/inbox`, and a Replies page shows each sender's words with the draft below them and two buttons: **Approve** and **Decline**.
- A Rust CLI lets agents read the inbox and write drafts. That's it. That's the whole CLI.

## The three locks on the door

We were very deliberate about this part. Three separate things stop an agent from sending a text:

1. **The state machine.** A text can only be approved from the Drafted state. A sent text can't be approved twice. A sender who texted STOP can't be approved at all.
2. **The CLI has no send command.** `textline inbox --needs-draft` lists, `textline draft <id> "words"` drafts. There is no approve command. It's not hidden. It doesn't exist.
3. **The footer.** Every outgoing reply ends with: *Web Design Vernon · webdesignvernon.com/text · STOP to opt out* - because Canada's anti-spam law (CASL) covers texts, and a commercial message has to identify the sender and offer an unsubscribe.[1] Pennant's research covered this too. Pennant is having a day.

The line sends **no automatic reply at all**. Telnyx itself answers STOP, START and HELP from the messaging profile, so we just log those keywords. STOP opts the sender out; START lifts it. Someone who texted STOP is never messaged again - even after deletion, the opt-out list survives.

## The live test, or: 106.8 minutes of silence

At 17:01, Mackenzie texted the real number from his mobile. Twice.

- **START**, stamped by Telnyx and arriving at our Worker 1,106 ms later. Signature verified. Sender marked opted in. Nothing sent back. Correct.
- A **short greeting**, arriving 493 ms after Telnyx stamped it. It sat in the Arrived state. Also correct.

Then a Claude agent drafted a reply. And Mackenzie... went about his evening. He approved the draft at 18:47.

The recorded reply time: **6,405,494 ms.** That's **106.8 minutes**. The machine measured every single one of them, which is the entire point - before this existed, that number was a vibe. Now it's a fact with a timestamp.

Once approved, the machinery was fast: the text went out through the Telnyx API, Telnyx reported it sent 222 ms later and delivered 1,180 ms after that. The footer was on it. So the honest summary of our first live text is: *incredibly slow human, blisteringly fast system.* We know which one we're going to work on.

## The tests, because "it works" is not a test result

`bun run e2e` boots the Worker under wrangler dev and posts signed fixtures from outside. It passed **35 checks**, including the ones that matter most:

- A genuine text is accepted as Arrived, and a retry is acknowledged but not duplicated.
- Nothing is sent before approval. Ever.
- A forged Twilio signature is refused.
- STOP sends nothing, and approval for an opted-out sender is refused.
- The CLI can list and draft but has no way to send.
- A retention sweep 731 days on deletes every thread.

Deletion, by the way: `textline forget <number> --yes` wipes every text from a sender, and a daily alarm deletes senders with nothing in 24 months. Only the opt-out list persists, so a person who said STOP stays protected even when their history doesn't.

## What's not built yet (honesty section)

- Pulling texts into the hub's People and Orgs as contact events.
- A push to Mackenzie on arrival - which, given the 106.8 minutes, might be the single highest-value feature on the board.
- We haven't yet seen a live STOP, a live Twilio webhook, or the hub Replies page in the wild.

It's MIT licensed - a Rust CLI plus a TypeScript Worker - and it's live at **text.mackenziebowes.com**.

## Why you should steal this pattern

If you run any kind of business where people text you, the interesting part isn't the SMS provider or the Worker. It's the shape: **an agent that can draft but never send, a human who approves, and a state machine that makes "sorry, it already went out" impossible.** You get the speed of an AI that's always awake and the accountability of a human who's legally responsible for every word - with real reply times in milliseconds to prove it.

Even when the human takes 106.8 minutes.

## Sources

1. Government of Canada, An Act to promote the efficiency and adaptability of the Canadian economy (CASL), section 6, Justice Laws Website. <https://laws-lois.justice.gc.ca/eng/acts/E-1.6/page-1.html>

---

Published 2026-10-27. Web page: https://www.mackenziebowes.com/posts/we-built-a-business-texting-line-where-the-ai-cant
