Skip to main content
Williams BalogunGet in touch
Case Study — Flagship / 04 · Personal product

Orviqo

A WhatsApp-native AI receivables clerk for Nigerian SMBs. The hard part was never the model — it was building an agent that returns a question instead of a hallucinated invoice, and knows when not to act.

Role Solo · full-stack, AI & productDomain Fintech · emerging marketsYear 2026Status Working demo, pre-pilot
Orviqo · WhatsApp
Mama Buchi bought goods worth 300k, she paid 200k, balance by Monday
Noted: Mama Buchi owes ₦100,000 for goods (2026-07-20) [#a1f]
Tunde owes me
Who owes you, and how much?
asked · nothing recorded
Chidi paid 50k
Chidi has 2 open debts — which one? · rice · ₦50,000 · generator · ₦50,000
asked · nothing recorded
01 — At a glance

Orviqo logs who owes a small business money, chases debtors politely on schedule, reconciles payments from plain WhatsApp messages, and keeps a clean ledger — all inside the chat app the business already runs on.

The interesting part isn't the parsing. It's the rigor gate: in a market that runs on memory and relationships, an agent that duns the wrong person or invents a figure is worse than no agent. Orviqo is engineered to ask before it embarrasses your customer, and that restraint is pinned by an offline test suite.

Role
Solo · full-stack, AI & product
Surface
WhatsApp — no app to install
Honesty KPI
Asks-for-clarification vs invented
Stack
PythonPydantic AIFastAPIAnthropic (Haiku)SQLAlchemy asyncPostgresAlembicWhatsApp Cloud APIFly.io
61
tests green in CI with no LLM key in the env
6
intents routed from one message (create · update · query · payment · contact · clarify)
0
amounts the agent is allowed to invent
100%
of decisions written to the audit log — refusals included
02 — The question

Most AI agents act
confidently. This one asks.

Tracking who owes a shop money isn't a new idea — Khatabook did it for millions in India, and Nigeria's Kippa signed up 350,000 businesses before it vanished. The demand is proven. What the winners got wrong was the interface, and trust.

Orviqo lives inside WhatsApp, where the business already happens, so there's no app to open. But the part I care about is trust: an agent trusted with someone's money must be hard to fool, including by a confident-sounding LLM. So the honest behaviour is enforced in code, not left to the prompt.

Rule 01

Never invent. No amount, date, or debtor is ever fabricated to look helpful — the model may only reuse what a message or the ledger actually contains.

Rule 02

Ambiguity is a question. If a payment could clear two debts, or an amount is missing, it asks — it does not guess and tell a customer they're square when they aren't.

Rule 03

Every decision is auditable. Recorded, updated, or refused — each is written with the source message as its citation, so “why did it do that?” always has an answer.

03 — How intake decides

One message, five gates.

Live loop

Every inbound message walks the same path. The LLM decides which action; code decides whether it's allowed to happen.

01

Context

Ledger + history

Gather the sender's open invoices and the recent conversation — so it never re-asks what it already knows.

02

Decide

One action

The agent returns exactly one intent: create, update, query, payment, contact, or clarify.

03

Rigor gate

Record vs ask

Refuse to record or settle without a debtor and an amount. Any ambiguity becomes a question, never a guess.

04

Act

Durable ledger

Write to (or answer from) the SQL ledger. Query numbers come from DB rows — the model never transcribes a figure.

05

Audit

Every decision

Recorded, updated, or refused — each is written to the audit log with the source message as its citation.

services/intake_service.py — the rigor gate
def _can_record(self, action):
 # never create without a debtor, an amount, and confidence
 return (
 action.debtor_name is not None
 and action.amount_ngn is not None
 and action.confidence >= self._threshold
 )

# a payment that matches two of a debtor's open invoices is never
# guessed — the service returns _ask_which(...) instead. We never
# guess with money.
04 — The proof

Honesty you can audit.

“It knows when it doesn't know” is easy to claim and hard to prove. Two artifacts make it checkable: a trace of every decision, and the questions it asks instead of guessing.

The decision ledger
message (the citation)decisionconf
Mama Buchi bought goods 300k, paid 200krecorded0.98
the debt is for a generatorupdated0.95
Tunde owes measked0.30
Chidi paid 50k (two open debts)asked0.95
Randi cleared her debtreconciled0.95

Every inbound message is written to the audit log with the message as its citation, the decision, and the confidence. The asked rows are the most valuable: they are the evidence Orviqo didn't invent a number.

It asks instead of guessing
“Tunde owes me”→Who owes you, and how much?no amount — a create is refused
“Chidi paid 50k”→Which of Chidi’s 2 open debts?ambiguous — money never guesses
“Ada paid”→How much did Ada pay?amount missing — offers the total

Three ambiguous inputs, three questions — not three confident mistakes. Each of these paths is a real test in test_intake_service.py, asserting that nothing is recorded until the gap is filled.

tests/ — the LLM is faked, so honesty is pinned offline
# a FakeAgent returns a canned action — no network, no key
async def test_create_without_amount_is_refused(make_agent):
 action = IntakeAction(kind="create", debtor_name="Tunde", amount_ngn=None)
 reply = await service.handle_message("biz1", "Tunde owes me")

 assert reply.recorded_invoice_id is None # nothing written
 assert (await audit.entries("biz1"))[0].decision is Decision.asked

# 61 tests like this run in CI with no ANTHROPIC_API_KEY set.
05 — Under the hood

The engineering discipline.

01Ports & adapters, deps pointing inward

The domain and the use-cases depend on ports, never on a framework, an LLM, or a database. Swapping the in-memory ledger for SQLAlchemy async changed zero lines in the services. An abstraction is added on the second real use, not a hypothetical.

HexagonalPydanticNo leaks
02The LLM is tested offline

A FakeAgent is injected in place of Claude, so the whole intake flow — including that it passes open invoices and history — is exercised end-to-end. 61 tests run green in CI with no API key in the environment.

Fakes61 testsNo secrets in CI
03The record-vs-ask gate

_can_record refuses to create without a debtor, an amount, and confidence. A payment that could clear two different debts is never guessed — the service asks which. Both refusal paths are pinned by tests so they can't quietly regress.

GuardrailsRefusal-firstPinned by tests
04Context-aware intake

Each message is decided against the sender's open invoices and the last six turns, reused from the audit log — no second table. So “the debt is for a generator” updates the invoice just recorded instead of re-interrogating the owner.

ConversationalNo re-askingAudit as memory
05Async durable ledger

SQLAlchemy 2.0 async Core on an injected AsyncEngine: Postgres in production, a real SQLite file in tests — no mocks. The adapter never knows which one it's talking to, so a blocking call can't stall the event loop FastAPI and the LLM share.

SQLAlchemy asyncPostgres / SQLiteAlembic
06WhatsApp Cloud API, idempotent

Redelivered webhooks are deduped by message id (processed_messages), marked only after a clean handle — so a retried “Chidi paid 50k” is never double-counted. Inbound routing honors STOP before owner intake ever sees the message. Reminders and chasing run as separate worker processes on the same ports.

Webhook dedupeSTOP opt-outWorker processes
06 — What it proves

The hard part of an emerging-market AI agent was never the model. It was the discipline to shut up when you're not sure — and to make that discipline testable.

The finding

In a low-trust, relationship-based market, restraint is the product. An agent that acts wrong is worse than one that pauses to ask — so the honest behaviour has to be the default, not a best-effort.

What I'm proud of

Enforcing that honesty in code, not the prompt — a record-vs-ask gate pinned by an offline suite of 61 tests, so it can't quietly regress into a confident liar.

The takeaway

Grounded AI for a real, messy, non-Western market: ports and adapters, LLM tested with fakes, a durable audit trail. Engineered, not vibe-coded — the same honesty thesis as Verdkt, different market.