How it works

Every number has to prove itself.

Privvy Ledger makes two promises: the numbers in your ledger are right, and they stay on your phone. Both are engineering decisions rather than intentions, so here is the machinery behind them — in enough detail to argue with.

Reconciliation · OPay wallet statementbefore any row is written
DebitsStatement64 · ₦169,106.00Privvy64 · ₦169,106.00matches
CreditsStatement64 · ₦169,106.00Privvy64 · ₦169,106.00matches
Rows readStatement128Privvy128matches
Every figure agrees — the import is offered. If one line disagreed, you would be told instead, and nothing would be written.

The statement states its own totals. The parser has to reproduce them exactly.

First promise

How do you know it read your statement correctly?

Reading a Nigerian bank statement

Most Nigerian banks do not export CSV at all. Zenith, FirstBank and Access email you a password-protected PDF and that is the only option; OPay gives you a PDF or an XLS. So the import engine reads all three formats, and the PDF path came first, because that is the one people actually have. The password your bank emailed you is used to unlock the file on your phone and is never stored or sent anywhere.

Each bank prints a different layout, so each needs its own row reconstruction — four are handled today, and more are added as real statement samples come in. The hardest is OPay's PDF, which scatters a row's description and its 24-digit reference across the lines above and below it, leaving about a quarter of rows with no text of their own. Rebuilding them is a claim-your-neighbour problem: a stray line belongs to whichever adjacent row is missing its description, and if both already have one, the line is reference noise. On the real 128-row sample, no row was left blank.

Why the numbers can be trusted

A statement already states its own totals, so the parser is not the authority on whether it read the file correctly — the statement is. Bank PDFs are checked with the balance they print: opening balance, plus every movement read, has to land exactly on the printed closing balance. OPay prints per-direction totals and counts, so its import is gated on all four figures — a stricter check than the banks get.

If any of it disagrees, you are told before a single row is written, and the import is not offered. The worst case is the ledger you already had, not a ledger that is quietly wrong. One limitation worth stating plainly: a row whose description is only partially inline keeps just that part, because letting it claim neighbouring lines risks stealing the row next to it. The merchant name survives, which is what the auto-tag rules match on.

Receipts, scanned without a network

A photographed or uploaded receipt goes through MLKit text recognition on the device, and the extracted text through a parser that scores lines for the likely total, merchant and date. PDF receipts are rendered to an image on the phone and take the same path. For the awkward ones there is an optional smart scan that runs a quantized model locally — a large download, and entirely your choice.

The parser is a pure module with a regression harness whose fixtures are frozen text from real scans of Nigerian bank receipts — LAPO, GTCO, OPay, FirstBank, Access, PalmPay. Every format that ever parsed wrong is now a permanent test. Nothing from a scan is kept: the image and the recognised text fill in the form and are then gone. Neither is ever written to the database.

Check it yourself — Turn on airplane mode and scan a receipt. It works, because none of it needed the network.

Second promise

How do you know it stays on your phone?

Where your money actually lives

Every transaction, category, budget and recurring rule sits in a SQLite database inside the app's private storage, behind your operating system's app sandbox and disk encryption. There is no backend to sync it to.

Amounts are stored as whole minor units in an integer column — kobo, cents — never as floating point, because a finance app that stores 0.1 + 0.2 has already lost. How many decimal places a currency has is derived per currency rather than assumed to be two, so the Japanese yen gets none and the Kuwaiti dinar gets three.

The lock on the front door

Your PIN is never stored. At sign-up the app generates a 32-byte random salt and keeps only the SHA-256 hash of the salt and PIN together — enough to check a PIN you type, useless for recovering it. Five wrong attempts and the only way past is the option that wipes the database.

Fingerprint and Face ID are handled entirely by the operating system; the app receives a success or failure and never sees anything biometric. Auto-lock is a timer you set — immediately, 30 seconds, a minute, five, or never — measured from the moment the app goes to the background.

Backups, and what they cost you

A backup is a single encrypted file you save wherever you like. The key is derived from a password you choose with PBKDF2, and the contents are sealed with AES-256-GCM. Type the wrong password and decryption fails outright on the authentication tag — there is no partial read.

The round count dropped from 100,000 to 10,000 in September 2026, and that is worth defending rather than quietly shipping: the ledger itself sits unencrypted in the app's private storage, protected by the OS sandbox rather than a password, so a backup file was never the weaker link — the ninety thousand extra rounds mostly cost the person unlocking their own phone, not an attacker. The count travels inside each file, so a backup or snapshot written before the change still opens at whichever cost it was saved with.

That cuts both ways, and it is worth being clear about the price: because there is no copy on any server, a forgotten backup password cannot be reset by us or by anyone. The guarantee and the inconvenience are the same fact.

Check it yourself — Open a .plbackup file in a text editor. You will find a salt, an initialisation vector and ciphertext — and nothing readable.

Automatic snapshots, and the bug that shipped with them

Every unlock, at most once a day, quietly writes an encrypted snapshot of the ledger to the device — five are kept, oldest dropped first. They exist for one specific accident: the lock screen's "erase & start over" is two taps and wipes everything, and if your last real backup is old or missing, a snapshot is what's left. They are deliberately built to survive that wipe. A factory reset or an uninstall still takes them with it, so an exported backup file remains the only thing that survives losing the phone itself.

What shipped to beta first was broken in the quietest way possible. A snapshot's key was derived straight from the PIN — needed so a wiped device could still open one with just the PIN — but a fingerprint unlock never asks for a PIN, so biometric-only users, which the lock screen defaults to, produced no snapshots at all. No error, no crash: the release notes said five, and some phones quietly had none.

The fix is envelope encryption. A random key, generated once, actually encrypts the ledger and lives in the device's secure hardware storage, so any unlock — PIN or biometric — can use it to write a snapshot. Every snapshot file separately carries that same key, sealed a second time under a key derived from the PIN. Wipe the device and only the hardware-stored copy is deleted; the files remain, still inert without the PIN. Restoring one is still not a way around the lock, which was the entire reason it was tied to the PIN to begin with.

Check it yourself — Turn on biometric unlock, then open Backup & Restore right after your very first unlock. A snapshot is already sitting there.

The one thing that leaves

Honesty is worth more here than a clean slogan, so: the app sends anonymous crash reports, and that is the only data that ever leaves your device. A report carries the error, a stack trace, your device model and the app version. A small sample of sessions also records a wireframe of the interface, fully masked — every piece of text and every graphic is blacked out before it leaves the phone, so no amount, category or note is ever visible.

No transaction, balance, budget, PIN or receipt is ever included, and there is no account or identifier tying any of it to you. It exists so bugs get fixed, and it is declared in the app's Play Store data safety section. Everything else stays where you put it.

The rule underneath all of it

Nothing leaves your device unless you explicitly ask it to. Features get built around that rule rather than the other way round, which is why the statement parser, the receipt scanner and the backup encryption all run on your phone. The full detail of what is and is not collected lives in the privacy policy.

Back to site