31 days, 166 commits: why we built our CRM instead of buying one
· Jack Jia
- xonark
- salex
- ai-native
- build-vs-buy
At Xonark, “the team” fits in one video call: Rachel Chen, Qiyan Jie, and me. We’re building Xona, an AI phone agent for dental clinics. It answers calls, books appointments, and plugs into the practice management systems clinics already run. (Internally and in our repos it’s still called susuai — the engineering codename.)
Six months in, Xona worked. Clinics liked it. The hard part was getting in front of the next clinic.
The standard playbook — HubSpot + Outreach + Apollo + an SDR — runs roughly $18k a month before the first reply lands. We’d be a worse customer of those tools than the clinics we were trying to win.
So we wrote our own. We call it Salex.
What 31 days bought us
Salex’s first commit was March 27. As I’m writing this on April 27, it has 166 commits and is running campaigns in two verticals: a 1,154-lead Vancouver dental cohort, and a tire-shop cohort that went live today — 87 shops queued for first-touch tomorrow morning, scheduled in their own local time from Vancouver to St. John’s. One Fly.io machine. Three people on the call.
The interesting part is the voice channel: Salex calls leads using Xona. Our product is the outreach layer for our own product. Every voice-quality fix for Xona improves our own funnel. Every edge case Salex hits in the wild teaches us what customers will hit a month later. We don’t have to imagine what’s hard about running an AI phone agent in production — we run one against ourselves every day.
Three decisions, briefly
We made a handful of unusual choices early. None of them are “right” generically — they’re right at three people, one product, one customer segment, one month in.
- SQLite, not Postgres. Postgres on Fly costs roughly 10x more at our volume, and we don’t need the concurrency yet. We get continuous backups via Litestream; recovery is a download.
- One scheduler, one heap, one reconcile loop. Day five, we threw out minute-level polling and built exact-time dispatch. Stage transitions fire when they should, not on the next tick.
- Wiki-first, not meeting-first. Fifty-two pages of decisions, incidents, and ADRs, in-repo. Future-us — usually a Claude Code agent picking up a task days later — catches up by reading, not interrupting.
We expect to outgrow at least two of these. That’s fine. The point is the cost of changing them later is small, because we wrote down why.
The 58 fake leads
Day eleven. We were paging through production data and noticed something that shouldn’t exist: 58 leads named “Anonymous susuai.” No email. No phone. No name. Just shells.
The cause was a polite mistake. Our event-tracking endpoint had a fallback that created a lead whenever an event arrived without a clean identity, on the theory that some signal beats no signal. For guest traffic, “some signal” turned out to mean a session ID — which expires, isn’t a person, and shouldn’t anchor a lead record.
We fixed it on the seventh: rejected unanchored events at the boundary, added a deterministic fallback for the cases we could identify, wrote tests, cleaned up the production rows.
The next morning, six anonymous leads were back. The fix had been wrong.
We had treated session IDs as a valid anchor when paired with anything else; turns out some of our own systems were sending session IDs as the primary identifier, which slipped past the gate. The day after that, we narrowed the rule again, added a tighter guard for that specific case, and re-ran the cleanup.
Two days, three fixes, one incident chronicle in the wiki — written because the fix took two passes, so the third regression won’t cost us the same hour twice. Eighteen days later, zero anonymous leads have come back.
What this episode taught us isn’t “we shipped a bug.” Everyone ships bugs. It’s that the combination we’d built — a single-file SQLite database, fixture-replay tests pulled from production, a wiki page per incident — let one engineer reproduce a production bug end-to-end on a laptop in under five minutes, fix it twice, and leave a paper trail behind.
That’s the part we wouldn’t have if we’d bought a CRM.
What’s next in this series
This is the first in a short series about how three people and a lot of AI build, ship, and break things at Xonark. Drafts in flight:
- Post 2 — The day we cold-pitched a competitor’s complaints inbox.
- Post 3 — Why our scheduler is one heap and one ten-minute reconcile loop.
- Post 4 — Telnyx + Twilio + Azure Realtime: a phone stack that swaps providers per number.
- Post 5 — A 52-page wiki our agents read.
Subscribe via RSS at /rss.xml if any of that’s interesting.