The Iceberg: How to Make AI Tools That Actually Work
The demo is a Telegram message. The product is everything under the waterline. Why almost every personal AI tool dies, and the unglamorous architecture that made mine live.
Tonight I'm going to see The Hip Abduction with a friend.
That's the whole demo. No screenshot of a chat window, no cherry-picked transcript. A show is happening in my city tonight, I knew about it in time, a friend said yes. My system did that: a scheduled job crawls local venue calendars weekly, checks them against a plain-text file of artists I like, and pings my phone on a match. It's maybe the least impressive-sounding feature I've ever built.
It is also the entire point.
And in case that sounds rehearsed, here's the second demo: this post is one. I kicked it off with a single Telegram message on my way out the door โ the writing, the hero image, the starter repo, and the deploy all happened while I was in the crowd.
Then the show handed me a third demo. Mid-set they played "Pacific Coast Highway" and it stuck. Walking out of the venue, I sent one more message: make me a guitar tab. By the time I was home it was live on CaChink, a guitar app this same system built โ a playable chord chart with the loop tabbed out, tone notes, and fun facts. Heard it live at nine, learning it on the couch before midnight.
The hardest problem in personal AI is not intelligence โ the models are absurdly capable and getting more so. The hardest problem is that almost nothing we build with them survives contact with real life. Everyone I know has built an AI thing; almost no one still uses the one they built three months ago. The graveyard is enormous: the RAG pipeline over your notes you queried four times, the "personal assistant" that was a system prompt in a folder, the agent demo that worked exactly once, on stage, with you driving.
We built something different, slowly and through a lot of failure, and this year it finally crossed the line: a system that changes what I do in the real world, every day, without me opening a laptop. Here's why it took so long โ and why every piece that matters is a piece nobody would ever demo.
The tip: what people see
From the outside, my system looks like a Telegram bot. I text it like a person:
shopping: add tortillas, coffee beans, dish soap
agenda: close out the email items, add pricing discussion
new app: flashcards for my kid's spelling words, deploy it
promote deal 89
menu
And things happen in the world. The shopping list updates on my phone and in the shared Apple Note my family edits. A consulting client's meeting agenda gets reconciled against our actual message history. A working web app exists at a public URL forty minutes later. A private-equity deal gets scored, written up, and published. A briefing shows up before I'm out of bed; school events land on the family calendar with a ๐ซ prefix, no newsletter required.
That's the tip of the iceberg โ small, glowing, and the only part anyone asks about. But try to build the tip directly ("I'll make a Telegram bot that runs an LLM") and you will build a toy, use it for nine days, and quietly stop. I did, several times. What finally worked was everything below the waterline.
Below the waterline
The layers, top to bottom. Each exists because a previous version failed without it.
A context repo: your life as plain files
The foundation is a git repo of markdown files we call cam-context: who I am, who I work with, what's in flight, family logistics, health, goals, a daily journal, and โ critically โ docs for the system itself. Numbered folders, no database, no vector store, no app.
This sounds too dumb to be the answer. It is the answer. Plain files are the one substrate every model, every tool, and every future version of both can read; git gives you sync, history, and blame for free. When an agent gets something wrong about my life, I edit a file, commit, and every session after that is smarter.
A dispatch layer: the agent has to live where you live
A listener script long-polls Telegram on a Mac that's always on. Every inbound message becomes a fresh headless Claude Code session (claude -p) with full tool access, a routing prompt, and the context repo on disk. The phone is the interface because the phone is where life happens.
This is the piece that converts "AI tool" into "coworker." Not a better model โ a transport.
Protocols, not features
The shopping list is not a feature with code. It's a markdown file whose header comment is the protocol: what phrases trigger it, how items get slotted by store aisle, how it mirrors to the Apple Note my family shares. The meeting-agenda flow, the school-events sweep, the concert crawl โ same pattern: a file, a protocol written in English at the top of it, and a sync script.
When a protocol is wrong, I fix it by texting the bot or editing the file. No deploy. The English is the source code, and the model is the runtime.
Schedulers: boring cron, not clever agents
Everything proactive runs on launchd (macOS cron): the morning briefing, the school-email sweep, the Sunday concert crawl, pollers with watchdogs. The agent decides what to do; a dumb scheduler decides when. Every attempt at making the system self-directing about timing either spammed me or went silent. Cron does neither.
Corollary: silent-when-clean. A job that reports "nothing to report" trains you to ignore it, so most of my jobs only speak when something changed.
Propagation rules: "done" is defined in writing
The failure mode that nearly killed the whole system: an update lands in one place and the other four go stale. So there's a written rule โ new information about a person in my pipeline is not done until it's in the pipeline file, the engagement doc, the contact map, the CRM, and committed and pushed. Skip it and my own dashboard lies to me. We got burned, then wrote the burn into the constitution.
The plumbing nobody will ever demo
macOS permission grants that only work when AppleScript is routed through ssh localhost. State files so a 3-hour headless build can resume after a crash. Watchdogs for the watchdogs. Telegram messages rendered as HTML with a plain-text fallback because raw markdown silently breaks. None of this is AI. All of it is why the AI works on a random Tuesday.
What the flywheel unlocks: one-shot apps
Once the iceberg exists, marginal cost collapses. The most fun artifact is our one-shot app playbook: I text new app: ... from anywhere, and a headless session scaffolds a repo, applies migrations to a shared Supabase project (one auth pool, per-app table prefixes โ never a new project; that's a written money rail), deploys to Railway, verifies the live URL actually serves the new bundle, and texts me the link.
The playbook is a markdown file. The interesting part isn't "AI writes code" โ it's the rails:
- Resumable state files โ every build keeps a
NEXT STEPfile on disk, so a fresh session can pick up a half-finished build. - Money rails โ anything with a recurring cost stops and asks a human.
- Verify before announcing โ the session polls the public URL for the new bundle hash before it's allowed to text me "done." An agent that says done when it isn't is worse than no agent.
- Conformity checklist โ analytics tag, auth standard, admin panel, feedback affordance, registry row. Every app, every time, because a checklist doesn't get bored.
Roughly twenty apps live on this substrate โ games for my kids, a CRM, datasets, this blog's platform. Each one makes the next cheaper, because every failure hardens the shared playbook instead of dying with the project.
Why everything else failed first
This system is the survivor of a multi-year pile of dead versions. The graveyard pattern is consistent:
- Built for the demo, not the Tuesday. If it requires opening a laptop and remembering it exists, it's dead in two weeks. The bar: does it reach you, in your pocket, at the moment it matters?
- Impressive > useful. The concert crawler is embarrassing technology โ a cron job, a text file of band names, a crawler. It has produced more real-world joy than every clever agent architecture I've tried. "Wow" and "use," it turns out, are anti-correlated.
- Memory in the model instead of files. Everywhere knowledge lived that only the AI could read โ embeddings, chat history, fine-tunes โ rotted. Files I can read, edit, and diff are the only memory that compounded.
- No definition of done. Without written propagation rules, every automation slowly desynced from reality, and I stopped trusting all of it. Trust, once lost, is the whole product.
- Frozen at one model generation. The design assumption that paid off most: capabilities keep arriving. Each jump โ tool use, MCP, connectors, longer context โ let us delete glue code and promote another protocol from "manual" to "automatic." Because the substrate is plain files and English, upgrading the model upgrades the whole system for free.
The tip of the iceberg is the same size in a demo and in production. The difference is entirely below the waterline โ and below the waterline is where you'll spend two years.
Start your own iceberg (the first hour)
You don't need twenty repos โ you need three files and a listener. The full skeleton is in the public starter, producthackerai/iceberg-starter; here's the load-bearing core.
1. A context repo
my-context/
โโโ 00-identity/about-me.md # who you are, how you like to work
โโโ 01-work/pipeline.md # what's in flight, one line per thing
โโโ 02-life/shopping-list.md # a protocol file (see below)
โโโ 07-journal/ # one file per day, agent-appendable
โโโ 08-systems/ # docs for the system itself
2. A protocol file โ the header comment is the program
<!--
PROTOCOL: shopping list
Trigger: any message starting with "shopping"
"shopping: add X, Y" โ add under the right section, then sync
"shopping: done X" โ check it off, then sync
"shopping: pull" โ a human edited the mirror; mirror wins
Sync: push to the shared note/app after EVERY edit.
Always commit + push this repo when done.
-->
## Costco
- [ ] coffee beans
- [ ] tortillas
3. A dispatch listener โ the whole trick in 25 lines
#!/usr/bin/env bash
# telegram-listener.sh โ every message becomes a fresh agent session
source "$HOME/.config/iceberg/telegram.env" # BOT_TOKEN, ALLOWED_CHAT_ID
OFFSET_FILE="$HOME/.config/iceberg/offset"
while true; do
OFFSET=$(cat "$OFFSET_FILE" 2>/dev/null || echo 0)
UPDATES=$(curl -s "https://api.telegram.org/bot${BOT_TOKEN}/getUpdates?timeout=50&offset=${OFFSET}")
echo "$UPDATES" | python3 -c '
import json,sys
for u in json.load(sys.stdin).get("result",[]):
m=u.get("message",{})
print(u["update_id"], m.get("chat",{}).get("id",""),
json.dumps(m.get("text","")))' | \
while read -r UPDATE_ID CHAT_ID TEXT; do
echo $((UPDATE_ID + 1)) > "$OFFSET_FILE"
[ "$CHAT_ID" != "$ALLOWED_CHAT_ID" ] && continue # allowlist, always
REPLY=$(cd "$HOME/my-context" && claude -p \
"$(cat 08-systems/dispatch-prompt.md)
Message from me: $TEXT" --max-turns 50)
curl -s "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
-d chat_id="${CHAT_ID}" --data-urlencode text="${REPLY}"
done
done
That's genuinely it. The dispatch prompt tells the agent where the context repo lives; the protocol files tell it what the words mean; git makes every improvement permanent. Add one cron job (the starter includes a template) and you have a system that talks first.
4. Then iterate on what you actually use
Watch your own behavior for two weeks. The features you don't text? Delete their protocols. The thing you keep doing manually? That's the next protocol. This loop โ not any single capability โ is the system. Two years of it is how you get from "cool demo" to standing at a concert because your cron job knows your taste in music.
This assumes an always-on machine you control, an agent harness with real tool access (we use Claude Code), and comfort giving it that access โ with allowlists, money rails, and least-privilege written down. Start with read-only protocols (briefings, digests) and earn your way to write access.
The promise, delivered
For two years the gap between "AI can do this in a demo" and "AI did this for me today" was the whole story. Closing it had nothing to do with prompts and everything to do with plumbing: files, transports, protocols, schedules, and rules about what done means.
The payoff isn't abstract. It's a grocery list my family trusts. A client agenda that's never stale. School events I never had to find. Apps my kids play that didn't exist at breakfast. And tonight, live music with a friend โ surfaced and scheduled by a pile of unglamorous machinery nobody will ever ask to see. Even this post rode the same machinery: one text on the way to the venue, live before the encore โ and one more on the way out, so the song that stuck was waiting as a tab when I got home.
Build the boring part. The boring part is the product.
Get the starter iceberg
The context-repo skeleton, dispatch listener, protocol templates, a cron example, and a hardening checklist โ everything in this post, ready to clone.