The problem
I started Nett after getting a credit card and deciding that I needed to pay more attention to my spending and budget. A normal banking app can explain what happened in one account, but it loses the wider picture when money is spread across different banks, cards, savings accounts, and eventually investments. Transfers between my own accounts also look like new spending and income unless I reconcile them myself.
I wanted one personal view of where my money was going. The first version is intentionally specific to Singapore, SGD, and the local bank statements I use because I am currently the only user.
What I built
Direct bank connections were not a practical option for this project in Singapore and would create a much larger security and trust problem. Nett therefore imports PDF or CSV statements. Each bank format has its own versioned parser, with Pdfium reading the text layer and local OCR available for scanned documents. Extracted transactions enter a review screen where mistakes can be corrected before anything affects the reports.
Every statement is checked against a simple reconciliation rule: opening balance + transactions = closing balance. Transfers between a person's own accounts and possible duplicates are flagged so they can be excluded from spending without losing the original record.
There is no LLM in the parsing path. Bank data is sensitive, model calls cost money, and ordinary PDF parsing already does the job. Adding a model where it is not needed would make the system harder to trust without giving the current version a meaningful advantage.
Decisions and tradeoffs
Permanently read-only, by constitution
Nett does not need the ability to make a payment, transfer money, or trade. Leaving those capabilities out reduces the damage the product could cause and keeps its purpose focused on understanding finances.
Local parsing before any model call
Statement extraction uses Pdfium with a local OCR fallback. The process remains deterministic and inspectable, and sensitive financial documents do not need to be sent to a model provider.
A visible invariant, not a blocking one
Every statement is checked against opening + Σ(transactions) = closing. A failed check is shown for review rather than blocking the import because a genuine statement can still contain a case the current parser does not understand.