Inside Chatty
Living doc This article evolves over time as my workflow changesThe map of the platform that runs my sales bots. A living article, updated whenever the architecture changes.
Chatty is the platform I built to run sales bots on top of Chatwoot. Today it runs a bot for a tech retailer that answers their WhatsApp and Instagram chats: it searches the real catalog, quotes trade-ins and payment plans, sets up reservations and, when a conversation needs a person, hands it over with the context already written. The business side, with numbers and what the bot does not do, lives on the bots page. This is the other side: how it is built.
This article is the map. Each piece has its own post, linked here. It stays marked as a living doc because it is updated whenever the architecture changes.
The path of a message
The buffer. People write in bursts: “Hi”, and five seconds later “price of the 16?”. Answering each message separately felt clumsy. The 30-second window came from measuring the gap between messages across 790 conversations: the median was 10 seconds. It was the change that improved perceived quality the most, and it does not touch a single line of the prompt.
A single agent. There used to be a classifier, a router, and a graph of workflows. They were deleted in July. What is left is one agent with four tools: search products, calculate trade-in, quote payment, hand off to a human. The tools return rows, never prose.
The catalog mirror. The tools do not read the store’s system live. They read a normalized copy in Postgres that syncs every half hour, inside a transaction: whoever is answering sees the whole old catalog or the whole new one, never a mix. The daily exchange rate lives there too. No figure travels through the model.
The guards. Before a reply goes out, twenty-eight rules review it: amounts that did not come from a tool, stock the search did not return, a trade-in quote without having asked for model and battery. If one fails, the model regenerates inside the same run, with a corrective note. If it fails again, the reply is discarded and the chat goes to a person. The whole mechanism is in A rule that throws the reply away. Why the ladder ends at the prompt instead of starting there is the subject of You do not stop hallucinations with the prompt.
The verdict. What the customer receives is decided by a pure function: reply, handoff, or fallback, with the reason. It is tested with no model, no database, and no timeouts. It is 80% of the system’s trust and 20% of its code.
The handoff. Three paths take a chat to a person: the model calls the handoff tool, a table of twelve rules forces the handoff even if it did not (a payment receipt attached, a “let me pass you to an agent” without the tool), or the turn produced nothing that could be sent. On all three the conversation changes hands in Chatwoot, with the reason in a note, and every following turn asks who is handling it before writing. The three paths, and what it took to make the bot go quiet, are in When the bot hands off to a person.
The tests. Four layers. Unit tests over the verdict. A contract test against the store’s real system. Ninety-six scripted, multi-turn conversations against the real bot and a frozen database: those are the gate for merging. And a replay where one model plays the customer and another judges the transcript, which is never a gate, only observation. All four layers, with their cost, are in The tests I actually run.
The maintenance loop. The bot is maintained with skills that Claude Code runs against the repo and against production: one audits conversations and opens issues, another turns a screenshot the store sends into an issue with the trace already investigated, another picks up the issue, fixes it, and writes the new tests. Every fix ships with a release note in the store’s language, which the client reads from their own Claude. All five skills, and the week when a fix without a test fixed nothing, are in The maintenance loop is a set of skills.
The MCP. The same server exposes an MCP with per-store OAuth. On top of it runs a daily report: demand the store could not meet, the waiting list, the reservation funnel, which ad each customer came from. That is the boundary I am testing: the bot captures, the MCP consumes. How the store asks and the time the report was guessing what the bot already knew are in The bot captures, the MCP consumes.
Stack
Bun and Hono for the server, Postgres on Supabase, pg-boss for queues, Mastra for the agent and the traces, Drizzle for the schema, Fly for deploys, Sentry for errors. An open model over an API as the primary, with a fallback chain where every link exists because of a dated incident. The domain code is in Spanish and the infrastructure in English, on purpose.
The series
- This article, the map.
- You do not stop hallucinations with the prompt: the ladder of data, tools, guards, and prompt.
- A rule that throws the reply away: how a guard with retry works, and what no guard catches.
- The tests I actually run: four layers, which ones block a change, and what running them costs.
- When the bot hands off to a person: the three paths to a handoff, and why the bot goes quiet afterwards.
- The maintenance loop is a set of skills: evidence to open, a failing test to close, and the note the store reads.
- The bot captures, the MCP consumes: how the store asks, the daily report, and what is the bot’s job and what is not.