The capture parser
POST /v1/capture/text turns a short message like gave mum 5k yesterday into a draft entry for the person to
confirm. It uses rules only: no AI model, no cost, and nothing leaves the server. A model is only ever meant to
be a fallback for messages the rules cannot read.
What it understands
| Part | Examples |
|---|---|
| Amounts | 500, 5k, 2.5k, 1.5m, 1,500, ₦500, N500, #500, NGN 3000, 500 naira, 53.75 |
| Number words | two thousand five hundred, five hundred and fifty, one million |
| The price, not the quantity | 2 bags rice 45k is 45,000 (not 2). bus 300 for 2 people is 300 |
| Dates, in your timezone | yesterday, last night, 2 days ago, monday, 12/10 (day first) |
| Money in or out | salary 150k, mum sent me 10k, received 20k from Ada are income. gave mum 5k, paid rent are spending |
| Categories | The built-in Nigerian categories, with everyday and Pidgin words (okada, keke, suya, esusu, nepa, aso ebi…), plus your own category names |
| Accounts | Matches the accounts you have by name (opay, gtb, cash), or suggests adding one you mentioned |
Phone and account numbers are ignored. The result is always whole kobo.
It explains itself, and does not guess silently
Every draft carries the word that decided the category (matched), a confidence and level, and plain-words
reasons to double-check. It will:
- refuse to invent an amount: no number means
canSave: false; - flag two different amounts in one message instead of picking quietly;
- say so when it cannot tell the category, and offer “Other” for the person to change.
Pasted bank alerts
Paste a credit or debit alert into the same box and it is read by a separate, stricter path:
- The label decides the direction.
Credit:(orCR, “credited”) is money in;Debit:(orDR, “debited”) is money out. Nothing else in the message can change that. - The amount is the one beside the label. The balance (
Bal:) is never taken, and it is removed before the pasted text is stored. - The alert’s own time is used, in the person’s time zone. A time in the future, or one that is not a real date, is ignored and the draft says so.
- The person behind a transfer is pulled out of the description (routing codes such as
FTandMoniare dropped), and the account number is never kept. - It only counts as an alert when it has a second marker (an account line, a balance, a description or a time). A short phrase like “credit 5000” still goes down the normal path.
- The category is honest about being a guess: money in from a person might be a gift, sales or pay, so the draft asks for a check.
What is proven so far: one real layout (Credit / Acct No / Desc / Bal / Time). Debit handling and other banks’ layouts are covered by synthetic examples only, and the plan’s target (300 real alerts, at least 95% right on amount and direction) is not met until real samples are collected. Each new layout is added with a real sample and a test.
How accurate is it? An honest answer
| Test set | Fully correct | Amount | Direction | Category |
|---|---|---|---|---|
| 217 phrases the team wrote, used while building | 100% | 100% | 100% | 100% |
| 59 fresh phrases, never tuned against | 86.4% | 94.9% | 98.3% | 89.8% |
The first row is flattering by construction: the parser was improved until it passed those phrases. The second row is the realistic estimate.
Neither set is real user data. The product target is 90% on real Nigerian phrases, and that is not yet proven. To prove or improve it we need real messages. The app records, on each confirm, whether the person changed the draft, so real accuracy and real corrections will accumulate from the first pilot users.
Known weak spots: quantities written as x2, sums like keke 100 + okada 300, and words that mean different
things in different places (for example dash means “gift” to most people, and “borrowed” money is not income).
How the two repositories stay in step
The web app keeps a light copy of the parser for instant previews, even offline. The API’s answer is authoritative.
Both are tested against the same phrase file, phrases.v1.json, which must stay byte-for-byte identical in both
repositories (its checksum is checked). A phrase file is never edited after it is published: a new set is a new
version (phrases.v2.json).
Learning from corrections
When the person confirms a draft, POST /capture/{id}/confirm compares what they saved with what we drafted. If
they changed the amount, direction, category, account or date, that is kept as a labelled example. This is also the
“parse accuracy” metric: the share of drafts saved without edits.
The text people type is personal data. It is stored with the capture, and encryption of those fields is planned (see Security).