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.
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
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.
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.
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.
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.
One message, five gates.
Every inbound message walks the same path. The LLM decides whichaction; code decides whether it's allowed to happen.
Context
Gather the sender's open invoices and the recent conversation — so it never re-asks what it already knows.
Decide
The agent returns exactly one intent: create, update, query, payment, contact, or clarify.
Rigor gate
Refuse to record or settle without a debtor and an amount. Any ambiguity becomes a question, never a guess.
Act
Write to (or answer from) the SQL ledger. Query numbers come from DB rows — the model never transcribes a figure.
Audit
Recorded, updated, or refused — each is written to the audit log with the source message as its citation.
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 andaction.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.
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.
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.
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.
# 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.
The engineering discipline.
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.
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.
_can_record refuses to create without a debtor, an amount, and confidence. A payment that could clear two different debts is neverguessed — the service asks which. Both refusal paths are pinned by tests so they can't quietly regress.
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.
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.
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.
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.
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.
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.
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.