Otto: Four checks before a stranger answers
A phone agent where the AI disclosure is built in code and verified three times, the number gate fails closed, and the thing it is least allowed to do is spend your money.
Solo — orchestrator, Expo app, shared core package, three test tiers · 2026
Otto has not placed a real call yet. Both halves are built, tested and talking to each other; what a receptionist does when they pick up is still unknown. The demo on the work page runs on the local store.
The problem
Handing a phone to an agent is not a hard engineering problem. It is a hard trust problem. The person who picks up never agreed to anything, cannot see what is calling them, and has no way to check what it is allowed to do.
So the interesting question is not whether a model can hold a conversation with a receptionist. It is what has to be true before the phone rings, and who finds out when one of those things is not true.
Every property below is enforced by a mechanism rather than a sentence in a prompt, because a prompt is a request and a stranger on the other end of the line deserves a guarantee.
Architecture
Four checks between a queued call and a stranger answering. Any one of them failing means no call.
- A spoken errand becomes a structured plan
- You approve the plan before anything dials
- advance.ts, a pure function of task, time and calling window, decides the next command
- The database row is the mutex: one update claims the call, the racing tick drops it
- The number gate allows landline, toll free and fixed VoIP only; any error is a refusal
- The disclosure sentence is built in code, then checked in the bytes being sent
- After the call, the transcript is checked for the sentence in the first agent turn
- A missing disclosure is an incident, not a warning
- Recordings are deleted at 30 days, transcripts and citations at 90
Key decisions
The decision is pure, the doing is quarantined
advance.ts takes a task, the current time and the calling window, and returns commands. execute.ts is the only code in the orchestrator with side effects. The design before it kept task state in memory the webhook handler never wrote to, so every wave timed out, successful calls were marked failed, and the spend guard read zero forever, which meant the budget ceiling could never fire. Splitting the two turned "what happens at 8:59pm on a Sunday in Anchorage" into a test that runs in a millisecond.
The row is the mutex
One statement moves a call out of queued, and it only succeeds if the call is still queued. Two ticks racing both decide to dial; exactly one gets a row back and the other drops it silently. No task lock, no advisory lock, no queue service. The state column already carried the information, so a webhook, a sweeper and an HTTP request can all drive the same task without coordinating.
The disclosure is asserted three times, in three different places
The opening sentence is built in code, not generated and not editable, with the name sanitised because a display name is untrusted input. It is then checked on the request object, in the exact bytes being posted to the provider, and finally in the transcript. The middle check exists because the first cannot see what the adapter serialises. The third exists because everything upstream only proves we asked: if the provider config overrides the first message, the call still places and only a non-2xx would have thrown. A transcript that does not contain it sets disclosure_verified to false and fires the one event wired to leave the process.
The gate fails closed, and charges the quota last
Numbers are classified through Twilio Lookup and only landline, toll free and fixed VoIP are allowed. Non fixed VoIP is denied because it is the line type most often issued to individuals, and unknown is denied because absence of evidence is not evidence of a storefront. Every step is wrapped so a lookup outage or a database blip produces a refusal rather than a permit, and the per number quota is consumed after the decision, so a refused number does not burn the caller’s allowance.
Otto can ask a business to hold something, and cannot pay for it
The rule is in the system prompt for every call and restated verbatim in the confirm back prompt rather than inherited, because that is the call where it is under real pressure. On an asking call nobody offers to take a payment. On a call that opens with "they would like to go ahead", somebody will.
Other people’s speech has a clock on it
A transcript is verbatim speech by someone who agreed to a phone call and nothing else. Recordings are deleted at 30 days, transcripts and finding citations at 90, tracked by a watermark so a sweep is idempotent and resumable, with an immediate version for a user who asks. Recordings are downloaded rather than linked, because a provider URL that expires is not a recording you can promise to delete.
Numbers
- 526 — assertions passing (451 unit, 63 integration, 48 contract)
- 3 — places the disclosure is checked (request, wire, transcript)
- 4 — checks before a phone rings (any one failing means no call)
- 30 / 90 — days to deletion (recordings, then transcripts)
- 0 — payments Otto can make (it may ask for a hold, nothing else)
No real call has been placed, so there is no answer rate, no completion rate and no evidence about how a receptionist reacts to the disclosure. The number gate is only as good as Twilio Lookup, which mislabels some legitimate business lines as unknown, and unknown is denied. The tests prove the mechanisms hold; they cannot prove the product is pleasant to use.