I Built an Open-Source Finance Tool Because I Didn't Trust My Bank Transaction Data

Every personal finance app seems to promise the same thing: connect your bank accounts, categorize your spending, and get insights into where your money goes.

But while working with financial data, I kept coming back to a more fundamental question:

How do I know the numbers are actually correct?

A dashboard can look beautiful. It can show spending trends, monthly budgets, and subscription costs. But if the underlying data is incomplete or incorrectly parsed, every insight built on top of it is potentially wrong.

And there is another problem: why should analyzing my own bank statements require giving a third-party service access to my accounts?

I wanted something different.

So I built Finances, an open-source, local-first personal finance and reconciliation tool written in Python.

It runs on your machine, imports statements and financial exports, verifies the numbers against their sources whenever possible, and provides a dashboard for exploring your finances.

No bank credentials. No Plaid. No mandatory cloud service.

But the most interesting part wasn't building the dashboard.

It was figuring out how to make financial data verifiable.

The Problem With Trusting Transaction Data

Most personal finance tools begin with a list of transactions.

You receive a date, merchant, and amount. Maybe the transaction comes from a bank API. Maybe it comes from a CSV or PDF.

Then you categorize the transactions, calculate totals, and display the results.

Seems straightforward.

But consider what happens when your parser silently skips a transaction.

Suppose your bank statement contains:

Transaction Amount
Grocery store -$85.40
Coffee shop -$6.50
Online purchase -$129.99
Electricity -$74.20

If your parser misses the online purchase, the resulting dataset still looks completely reasonable.

You have three transactions, all with valid dates and amounts.

Your dashboard works. Your charts render. Your reports are generated.

But you're missing $129.99, and nothing necessarily tells you that.

This becomes particularly dangerous with PDF bank statements, where transactions may be split across pages, columns can be misinterpreted, and negative signs might be lost.

I didn't want my application to silently assume that successfully extracting some rows meant successfully extracting the statement.

I wanted it to check its own work.

The Key Idea: Use the Statement as Ground Truth

Bank statements have something incredibly useful: balances and totals.

Instead of treating an extracted transaction list as the source of truth, Finances treats the original statement as the reference against which extraction is checked.

For a simple account, the underlying relationship is:

Opening Balance + Deposits - Withdrawals = Closing Balance 

Imagine a statement with:

Opening balance: $2,500.00 Total deposits: $3,000.00 Total withdrawals: -$1,250.00 -------------------------------- Closing balance: $4,250.00 

If the parser extracts only $1,150 in withdrawals, the resulting balance won't match the statement.

That's a signal that something is missing or incorrect.

Finances runs reconciliation checks against the information available in each source.

A successful verification means the applicable checks passed. A mismatch means something needs investigation.

And if a file doesn't provide enough information to independently verify its contents, the tool reports it as unverifiable rather than pretending everything is correct.

That's an important distinction.

A plausible result isn't necessarily a verified result.

Building a Verification Engine

The reconciliation engine gradually became a central part of the project.

I wanted the tool to do more than report a boolean saying that something passed or failed.

For every verification, I wanted to know:

  • Which source record was involved?
  • What was the expected amount?
  • What amount did the application calculate?
  • Which rule produced the result?
  • Can the result be reproduced?

That led to an evidence-oriented verification system.

Instead of only generating final totals, the engine retains information that connects financial records and verification results back to their sources.

It supports five outcomes:

Status Meaning
verified The applicable checks passed
mismatch The calculated result disagrees with the source
unverifiable Insufficient evidence to check independently
needs_review A potential match requires human judgment
incomplete Required information is missing

Why not just assign every transaction a confidence score?

Because a score can hide an important difference between uncertainty and contradiction.

A transaction from a CSV without balances may be perfectly legitimate, but I can't independently verify its effect on the account balance.

A transaction that contradicts an available statement total is a different problem entirely.

The system should communicate that distinction.

Supporting Different Financial Formats

Another challenge was that financial exports aren't standardized in the way you'd hope.

Some banks provide PDFs. Others provide CSVs with different column names, date conventions, and amount signs.

Some exports include running balances, while others contain only transaction rows.

Finances currently supports several formats, including:

  • PDF bank statements
  • CSV exports
  • OFX, QFX, and QBO
  • CAMT.053
  • MT940
  • Stripe payout reports and accounting ledgers for reconciliation

Each format has different verification opportunities.

For example, a CSV containing running balances can be checked row by row.

An OFX export may provide balances that can be checked across consecutive exports.

A CAMT.053 file can provide opening and closing balances against which booked entries can be reconciled.

But a basic CSV containing only descriptions and amounts may not have independent balance evidence.

The goal isn't to force every file through the same verification procedure.

It's to extract and use whatever independent evidence the format actually provides.

Matching Records Across Different Sources

The project also supports reconciliation across bank transactions, payment processor reports, and accounting ledgers.

Consider a business receiving a payment through Stripe.

The ledger might record a $260 payout, while the bank receives a deposit of $252 after processing fees.

A naïve matching algorithm sees different amounts and reports that they don't match.

But if the processor data accounts for $8 in fees, that difference becomes explainable.

The matching engine uses several stages:

  1. Match records using shared references when available.
  2. Look for exact amounts within a date window.
  3. Handle grouped settlements involving multiple records.
  4. Surface near matches for review rather than automatically accepting them.

The important rule is that an explanation isn't always sufficient grounds for an automatic match.

When the evidence is ambiguous, I want the system to say so.

Financial reconciliation should favor correctness over appearing comprehensive.

Keeping Financial Data Local

Privacy was another major design constraint.

I wanted to analyze financial data without requiring users to connect their bank accounts or upload statements to a hosted application.

Finances runs locally and stores its data on the user's machine.

The dashboard is served on localhost, and the project also supports running through Docker.

The core workflow doesn't require sending transaction data to an external service.

That makes it useful for people who prefer to manage their financial records themselves.

It also removes the need to build around bank-account aggregation APIs.

There is a tradeoff, of course.

Users need to download and import their own statements or exports.

But for someone who values control over their data, that tradeoff can be worthwhile.

Adding AI Without Making It the Source of Truth

I also wanted to experiment with natural-language financial analysis.

Questions like:

How much did I spend on food last month?

Which subscriptions increased in price this year?

Where did most of my money go?

These are useful questions, but I didn't want a language model inventing financial calculations.

Instead, the application exposes read-only tools that retrieve information from the underlying financial database.

The model can help interpret the question and present the answer, while the numerical data comes from the application's calculations.

Finances supports local models through systems such as Ollama and LM Studio, along with optional cloud providers such as OpenAI and Anthropic.

The privacy distinction matters here: when using a cloud model, the relevant question and tool results may be sent to that provider. With an appropriately configured local model, the analysis can remain on the user's machine.

There's also a local-model fallback for unsupported PDF layouts.

In that workflow, the model can attempt to extract transaction rows, but the deterministic parser independently reads the statement totals.

If those extracted rows don't reconcile, they are rejected.

The model can propose an extraction. It doesn't get to declare its own work correct.

That's the principle I wanted throughout the system.

What You Can Actually Do With It

Beyond reconciliation, Finances includes a browser dashboard and a Python CLI.

You can use it to explore spending by category, identify recurring expenses, track budgets, investigate subscriptions, and generate cash-flow projections.

There's also a year-in-review feature, which summarizes patterns across your financial history.

For developers, the CLI provides access to the underlying tools without requiring the dashboard.

You can inspect reconciliation results, generate reports, run verification commands, or integrate the verification engine into other Python workflows.

The project also exposes a read-only MCP interface for compatible AI assistants.

Try It Without Using Real Financial Data

One thing I felt strongly about was making the project easy to evaluate without exposing personal information.

So there's a demo that generates a year of fictional financial activity.

If you have uv, you can try it with:

uvx finances demo uvx finances web --db demo.db --open 

This creates a sample database and opens the local dashboard.

For installation:

uv tool install finances 

To import your own statements:

mkdir -p PDFs # Place your statements in the directory finances pipeline PDFs/ # Check the reconciliation results finances check # Open the dashboard finances web --open 

The project also supports Docker.

You can find the complete installation instructions and supported formats in the repository.

What I Learned Building This

The biggest lesson from this project is that data correctness deserves as much attention as data visualization.

It's tempting to prioritize dashboards, charts, and AI features because they're visible and immediately impressive.

But a beautiful visualization of incorrect information isn't particularly useful.

Building Finances pushed me to think more carefully about provenance, reproducibility, uncertainty, and the difference between extracting data and actually verifying it.

It also reinforced something I believe about AI-powered applications: language models can be useful interfaces, but they shouldn't automatically become authorities over facts that deterministic systems can check.

There's still plenty of work to do, particularly around additional bank formats, edge cases, and improving the experience for new users.

Open Source and Looking for Feedback

Finances is open source under the MIT license.

GitHub: https://github.com/roshanlam/finances

I'd especially appreciate feedback on the verification and reconciliation engine, support for additional financial formats, and the local-first approach.

I'm also interested in hearing from anyone who has built financial data pipelines or worked with bank statement parsing.

How do you handle missing transactions, incomplete exports, and reconciliation failures?

I'd love to learn how others approach these problems.

If you find the project useful, consider starring the repository or contributing.

Thanks for reading!