Professional
Collections assistant | Statements and payments
The client checks a balance, reports a payment or promises a date on WhatsApp. The model interprets; the code decides the action and what it may disclose.
- AI
- Full Stack
- Role
- Development
- Period
- 2026-07 — Present
Stack
- TypeScript
- Node.js
- MySQL
- Claude
- Docker
- Google Cloud Run
Context
WhatsApp collections for Intercargo’s operation in Panama. The client writes in, checks their statement, reports a payment or promises a date, and all of it happens inside the conversation.
Problem
Collections relied on lists of clients to be contacted one by one. Every payment receipt was reviewed by a person before the balance was up to date.
The client asked for that cycle to be automated. The debtor checks their own balance and attaches their own receipt, and the team steps in only where judgement is needed.
Technical decisions
A collections assistant discloses other people’s balances. If the model decided who sees them, every reply would depend on it never being wrong, and a language model does not give that guarantee.
So the model does one thing: turn a natural-language message into an intent and, where applicable, a date.
Everything else is code: validating identity, querying the account, recording a payment promise and deciding whether a reminder is due. The conversation runs on a state machine that returns actions, and a separate layer translates them into the WhatsApp transport.
The service uses no web framework: it starts a server with node:http and works over nine tables of its own, separate from the rest of the corporate platform. The model provider is swappable, with two implementations behind the same interface.
Architecture
The model occupies one stretch of the path. What goes into it and what comes out are structures the code validates before anything continues.
A phone number is not enough as an identity. Until identity is verified, the conversation discloses nothing about the account.
The model returns an intent, not a command. The available actions are a closed set defined in code, so an intent with no matching action executes nothing.
Working days, holidays and the contact window are computed on the server. Anyone who asks to stop receiving messages stops receiving them. The reminder goes out one working day before the due date.
Every turn stores the message received, the intent resolved and the action executed. An unexpected reply can be reconstructed later without consulting the model again.
The state machine returns actions and a separate layer translates them into the transport, so the conversation can be run without Meta credentials or a WhatsApp line.
Result
The service is in operation over nine tables of its own, separate from the rest of the platform. One command replays an entire dialogue against the real logic, and that is how the hard cases are tested: a promise with an ambiguous date, a payment reported twice, a client asking to stop receiving messages.
What I learned
That simulator walks the state machine end to end, so it catches a flow regression without locating it: when it fails, it points at the whole dialogue rather than the module. Tests over the action layer are the next step.