CommunityProductivity & Collaborationgithub.com

2016judea/sms-native-product

Build a product whose interface is text messages rather than an app or a website — "no app, no website", "they just text a number", "chat with our agent natively in Text Messages", "can we do this over text", "an SMS bot", "a Telegram bot", "make it a group chat", "minimum viable friction". Fires equally when it has already gone wrong — "my campaign got rejected", "our number is blocked", "the texts aren't arriving", "why do I need to register". Covers the gate that decides whether such a product can send at all (US A2P 10DLC registration, SHAFT content rules, permanent rejection codes), group MMS from code, what each channel can't do, an architecture that survives losing the channel, and guarding model-rewritten user content.

What is sms-native-product?

sms-native-product is a Claude Code agent skill that build a product whose interface is text messages rather than an app or a website — "no app, no website", "they just text a number", "chat with our agent natively in Text Messages", "can we do this over text", "an SMS bot", "a Telegram bot", "make it a group chat", "minimum viable friction". Fires equally when it has already gone wrong — "my campaign got rejected", "our number is blocked", "the texts aren't arriving", "why do I need to register". Covers the gate that decides whether such a product can send at all (US A2P 10DLC registration, SHAFT content rules, permanent rejection codes), group MMS from code, what each channel can't do, an architecture that survives losing the channel, and guarding model-rewritten user content.

Works with✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/2016judea/brick-mortar-skills/tree/HEAD/skills/sms-native-product

Installed? Explore more Productivity & Collaboration skills: steipete/gemini, steipete/gh-issues, steipete/skill-creator · View all 6 →

Ask in your favorite AI

Open a new chat with this agent skill pre-loaded.

Documentation

Products whose interface is text messages

The appeal is real: people don't like installing apps or visiting websites, but everyone can text. The catch is that you do not own the channel. US carriers do, and they have a content policy and an approval process that can refuse you permanently. That gate decides what the product is allowed to be, so it goes first.


Step 0 — the gate. Do this before design, not after.

Since Feb 2025 US carriers block unregistered A2P traffic outright, so a brand and campaign must be approved before a single message sends. Re-read the live error docs every time; carrier policy moves.

CodeTriggerResubmit?
30953explicit/suggestive language, or a linked site hosting "pornographic, escort, dating, or adult entertainment content"No
30885"high risk" — deceptive marketing, or a registration that misrepresents the trafficNo
30898too many brands on one EIN, or a duplicative-looking registrationYes, with cause

Rule: name the product's content category out loud in the first reply, and say whether it is on that list. If the category (e.g. dating) is literally a keyword in a rejection rule, finding that in minute five changes the architecture, the landing copy and the fallback plan. SHAFT is the shorthand: Sex, Hate, Alcohol, Firearms, Tobacco — plus Cannabis, and dating rides alongside.

Rule: register a side project as a Sole Proprietor brand, not under an existing company's EIN. Risk patterns follow the tax ID (30898), so a refusal on a side project must not touch your main business's ability to send. Sole Proprietor is scoped to individuals with no Tax ID; throughput is low, and low is plenty for a beta.

Rule: never build a decoy landing page to get a campaign approved. A page that hides what the traffic actually is is the 30885 pattern, and it converts a one-number risk into an every-number-you-ever-register risk. State the odds plainly and let the owner decide.

Describe the service by what it mechanically does, and only if that is true (e.g. journaling and small-group accountability, when that is what the messages are). If no true description clears the gate, the answer is a different channel, not a better euphemism.

Vetting rewards facts you can point at: inbound-only opt-in, conversational volume, no links in recurring messages, STOP handled ahead of everything, explicit content refused at the door.

Step 0.5 — read the brief for NUMBERS and OBJECTS, not for intent

Keep the founder's brief verbatim as README.md and say in CLAUDE.md that it outranks every engineering doc.

Rule: before writing a state machine, list every duration in the brief and every noun it acts on. If two durations exist, they are two features. A brief with a one-week delay on friends' replies reaching the author and a two-week lock on the author's own update is easy to collapse into one seven-day hold on the entry — the one object neither delay was about — and every downstream error follows from that merge.

Rule: name the object of every delay — "held FROM whom". Held from the friends and held from the author are opposite products.

Rule: when the brief's literal design can't be enforced on the channel, say so and give the alternatives — don't silently pick the enforceable one. (The creator of a group chat is a member of it, so no code can withhold the group's replies from them.) That is a premise decision, and premise decisions are the owner's.

A coherent misreading does not feel like one. Keep a superseded design paragraph struck through in place rather than deleting it; it shows a later session how the brief was misread.

Step 1 — what the channel can and cannot do

  • Group texting from code works. Twilio Conversations does group MMS on +1 long codes only — not toll-free, not short codes — capped at 10 participants including your own number. Don't assert otherwise from memory.
  • Group-MMS SMS participants carry an Address and no ProxyAddress, so one number serves every room — but the participant/proxy uniqueness rule (50425) has no documented behaviour for proxy-less participants. Never design many concurrent rooms that share one number and one participant set. One durable room per person has a stable participant set.
  • A webpage cannot read an iPhone address book. The Contact Picker API is Chrome-on-Android only. "Sync your contacts" is dead on arrival; replace it with a line the user forwards — the recipient's inbound text is the consent record carriers want anyway.
  • A Telegram bot cannot create a group or add anyone to one, and with privacy mode on it only hears commands addressed to it. Both help: the creator adds the bot to the chat they already have, and general chatter can't be mistaken for input. Creating a bot needs a human in @BotFather (no API); ask for the token.
  • WhatsApp Cloud API has no group messaging.
  • iMessage APIs (Sendblue, LoopMessage) do blue bubbles and real group chats, but cost around $100/line/month plus setup fees.

Step 2 — architecture that survives losing the channel

Because the channel can be refused, the transport must be one file:

machine.js   pure: (context, event) -> {replies, effects}    no I/O, no clock
engine.js    context assembly -> machine -> effects          the whole path
effects.js   createEffects({db, channel})                    performs decisions
twilio.js  / telegram.js                                     interchangeable
api/*.js     thin shims over engine.js                       ~10 lines each
sim/         in-memory db + console channel
  • The decision core takes now as an argument and touches no network. Every bug in this class of product is a sequencing bug (a reminder that fires twice, a message to an empty room, a held item that never lands), and none are testable against a live network and a seven-day timer.
  • Adapters are arguments, never imports, in the effects layer — the same refactor the simulator and the fallback channel need.
  • No logic inside an HTTP handler. Channel-specific onboarding written inside api/telegram.js is untestable; extract it.

Step 3 — drive it, don't just test it

node sim/drive.js          scripted: the full loop, a week jumped, the guards
node sim/drive.js --repl   type as any number; /as <n>  /jump <days>  /state
  • The simulator must run the production path, not a parallel copy.
  • Expect a first drive to surface several real defects unit tests can't reach: a room that greets itself before anyone is in it, a bare weekday for a date a week out, a reminder that doesn't say which item it means, a room whose members all left.
  • When a test goes red, check the test first (a date regex that doesn't allow the comma in Sun, Aug 30; a doesNotMatch that fires on a different, legitimate message).
  • Never decide or print from a store-stamped timestamp. The store writes created_at from the real clock, not the engine's injected now, so under a jumped clock it lies (and prints dates in the past). Decide only from columns the machine wrote, and hang related dates off one stored anchor.

Step 4 — copy for a lock screen

One file holds every outbound string.

  • No emoji, no exclamation marks, no marketing.
  • Never a bare weekday for a date more than a day or two out. Sun Aug 30.
  • Forgive punctuation and case on every keyword — Wait. and Yes! must land.
  • Prose containing a keyword is still prose: "we should wait and see" is not WAIT. Match the whole canonicalised message, never a substring.
  • STOP / HELP / START are parsed before all application state and before any model call, so opt-out can't be broken by a bug elsewhere or an API outage.
  • If a model guards content, fail closed: if the check can't run, send nothing and say so.

Step 5 — if a model rewrites user content, run it before trusting it

A guard that strips third-party names from user entries can read perfectly and still:

  1. Invent a fact — asked to replace "Jane Doe" with "a natural handle", it wrote "the girl from the app" when nothing mentioned an app; recipients read a fabrication as fact.
  2. Over-correct and delete real detail — "Jane from the run club" became "her".

Rule: the instruction must say what the model may NOT invent and what it may NOT delete. The working form: ground the substitution in the source, replace the token not the phrase around it, and prefer a neutral fallback over reusing a detail the sentence already states.

Rule: pin each live-model defect as an opt-in test, skipped without an API key, so prompt edits can't silently regress them without running up costs on every test run. Budget 2-5s per guard call.

Costs (order of magnitude; confirm live)

A +1 long code is a dollar or two a month; SMS under a cent a segment each way; MMS a few cents. Sole Proprietor registration is a few dollars one-time plus a small monthly campaign fee. Telegram is free.

Ask vs. decide

  • Ask: whether to submit an honest registration knowing it may be refused permanently, or start on Telegram (money and an irreversible outcome); anything that changes what the product is.
  • Decide: architecture, copy voice, the state machine, test coverage, which adapter first, date formats, guard behaviour.

Before reporting anything as blocked, enumerate every candidate credential (one exhausted API key on disk doesn't mean there isn't another that works).

See also: build-remote-mcp-server (another "native surface" pattern).

Individual skills in this repo

This repo contains 7 individual skills — each has its own dedicated page.

2016judea/ascii-charts

Build charts as plain Unicode-block text — bar, range, bullet, gap-bracket, star-histogram, heatmap, line, Venn, pie, donut and scatter — with a dependency-free Python script, for anywhere only monospace text survives (newsletter code blocks, Markdown, terminals, plain-text email, chat). Use when a chart has to embed as literal text instead of an image or a third-party chart service, or when you want a hand-made terminal look with no branding footer.

2016judea/buffett-checklist

Apply Warren Buffett's investment framework as a discipline lens when weighing a buy, hold, or sell decision — circle of competence, economic moat, owner earnings & intrinsic value, margin of safety, management/capital-allocation quality, and market psychology (Mr. Market, fear vs greed, temperament). Use when asked to "run this through Buffett", "what would Buffett say", "does this pass the moat test", "is this cheap enough", or for a second, more skeptical read on a name being evaluated or already held. This is a discipline overlay, not a replacement for a strategy — it applies the Buffett lens to how someone actually invests, rather than issuing generic advice.

2016judea/image-riff

Turn one or more reference images into original art in that reference's own visual language, then cut it to whatever surface it's for. Use when you paste or point at art and say "use this as the base image", "generate art from this", "riff on this", "some creative ideation with these images", "make me a channel banner / avatar / cover / plate", "wilder version of this", or want a spread of options off one piece of reference. This is the TASK skill; the engine is the `make-art` tool skill.

2016judea/leonardo

Draw the way Leonardo da Vinci drew — a study sheet, a notebook folio, a red-chalk or silverpoint study, a machine cutaway, a storm of vortices — in his real materials and page-logic, generated with the make-art engine and anchored on fifteen of his actual (public-domain) sheets. Use when you say "Leonardo", "da Vinci", "like his notebooks", "a codex page of X", "draw this as one of his study sheets", "sanguine", "silverpoint", or wants a design, diagram, or illustration in that hand. Also a reading guide to his notebooks (Richter's 1883 public-domain translation) for "what did Leonardo say about shadow / trees / water". This is a TASK skill; the engine is `make-art`.

2016judea/mcp-stack

Which free MCP server or framework to reach for, and its traps — finance/gov data (SEC EDGAR, Yahoo, SAM.gov, USAspending), web research (Exa, Firecrawl, Crawl4AI, Jina reader), design/art/brand (Canva, Inkscape, Penpot, Figma, Orshot, Hugging Face image gen, vectorizing, charts), data/spatial (DuckDB spatial, live ArcGIS layers, OpenStreetMap/Overpass, Census, MapLibre), and ops (PostHog, iMessage, context7 docs). Use when making a social post, OG card, poster, logo/vector, chart, map, or brand asset; when querying parquet/geojson/duckdb or a city's open-data layer; when an MCP "needs authentication" or fails; or when asked "what tools do we have", "is there an MCP for X", "should we add X".

2016judea/records-request

Get a specific record out of a US government office by public-records request — "request the assessor's model", "file a data practices request", "FOIA this", "open records request", "can we just ask the county for it", "who do I write to at <county>", "chase that request", "did they ever answer". Covers checking the website first, finding the right office and statute, the carve-out paragraph that makes a request answerable instead of refused, web-form traps (hidden character caps, WAFs that reject apostrophes), chasing (there is usually no deadline), whether what comes back may be republished, and parsing multi-year tables without silent errors.

2016judea/vertical-market-agent

Build a chat agent that knows one industry in one metro better than any general model because it carries a precomputed digest of real public records (or a mined review corpus) and can pull live records mid-conversation — finding the moat, building a byte-stable substrate, enumerating what the records can't, the data traps that produce plausible wrong numbers, deterministic math in tools, measured prompt cost and cache TTL, shipping as an ad destination, an off-sector ending, and a precomputed spatial "what is this next to" layer. Use when building a vertical AI agent grounded in records rather than general knowledge.

Related Skills