Every month I had the same problem: my money lives in three places — HBL, ABL and NayaPay — and no single app shows the whole picture. Transfers arrive as emails, card spends arrive as SMS, and the "official" record only shows up weeks later in a PDF statement.
So I built FinSync: a personal finance engine that pulls transactions from every channel, collapses duplicates, reconciles them against bank statements, and turns it all into one clean ledger with analytics and a forecast.
👉 Live demo (no signup): https://finsync-green.vercel.app — click "View live demo"
👉 Source: https://github.com/danishzulfiqar/FinSync
The problem: no bank APIs in Pakistan
In the US or Europe, a budgeting app connects to your bank in two clicks through Plaid, TrueLayer or a PSD2 open-banking API. In Pakistan, none of that exists for everyday users:
- No public APIs. Banks don't offer developer APIs to read your own account data.
- No aggregators. There's no Plaid-style service that supports local banks, so global apps like YNAB or Mint simply can't connect.
- No clean exports. Most banks give you a PDF statement — not a CSV — and only for past months.
So your own financial data is locked inside three separate apps, and the only things the banks do hand you are the notifications they already send: SMS alerts, emails and PDF statements. FinSync is built entirely on those — the data you already legitimately receive — without scraping banking apps, reverse-engineering private APIs or ever asking for a password or OTP.
The real problem: one payment, three copies
A single card payment can reach me as:
- a real-time SMS (merchant, exact time, no balance),
- a notification email (reference number, sender name), and
- a statement line a month later (running balance, generic description, posting date — not the transaction time).
Naively importing all three triples your spending. Naively deduplicating on amount + date merges two genuinely different ₨1,500 payments on the same day. The interesting engineering is everything in between.
Architecture
Five tiers, one direction of flow: the signals banks already send → read-only ingestion → a deterministic reconciliation engine → a ledger where every real event is exactly one row → analytics on top.
- Backend: Python 3.12, FastAPI, SQLAlchemy 2, Alembic, PostgreSQL, pdfplumber
- Frontend: React 18, TypeScript, Vite, Tailwind, Recharts
- Infra: Render (API), Neon (Postgres), Vercel (dashboard)
1. One transaction, many sources
The key modelling decision: a financial event and the evidence for it are different things.
There's one transactions table and a separate transaction_sources table. When the same payment arrives via email and SMS and statement, those become three sources attached to one transaction — never three transactions. Each extra source raises the record's confidence and can fill in blank fields.
2. Deterministic fingerprints
Every parsed event gets a SHA-256 fingerprint built from:
provider + type + amount + currency + account_last4 + reference + rounded time bucket Ingestion then resolves to one of four outcomes:
| Outcome | Action |
|---|---|
unique | create a new transaction |
exact_duplicate | same fingerprint → attach the source only |
probable_duplicate | same money, close in time → attach, raise confidence, enrich fields |
needs_review | similar but conflicting reference → keep both and flag |
That last rule matters: when in doubt, FinSync never silently deletes money. Re-processing the same email or SMS is a no-op, guarded by unique source IDs and content hashes — so every sync is idempotent.
3. Reconciling alerts with statements
Fingerprints can't catch the alert-vs-statement case, because the statement has a different date and often no reference. A separate reconciliation pass handles it:
- match by reference when both sides have one (strong signal),
- otherwise the nearest time within a few days, same direction and amount,
- keep the statement row (it carries the authoritative running balance),
- enrich it with the alert's merchant and exact time, repoint the alert's source onto it, and drop the duplicate.
The result: balances that reconcile to the rupee, with the rich details from the real-time alerts.
4. Analytics and forecasting
Once the ledger is clean, the fun part is easy:
- per-account balances anchored on the newest statement running balance,
- income/expense flows that exclude internal transfers between my own accounts,
- "learn-once" categorization — re-categorize one merchant and every matching row follows,
- a Monte-Carlo balance forecast for the next few months.
Security: read-only by design
Handling bank data means being strict about what not to do:
- Gmail access is read-only OAuth (
gmail.readonly), nothing else. - No scraping of banking apps, no private APIs, never stores passwords, OTPs, PINs or card numbers.
- Raw SMS text isn't persisted — only normalized fields plus a one-way hash for dedup.
- The SMS webhook is authenticated with HMAC-SHA256 and timestamp anti-replay.
What I learned
- Model evidence separately from facts. The
transaction_sourcessplit made every later feature simpler. - Make ingestion idempotent from day one. Being able to re-run any sync safely is what makes debugging bearable.
- Prefer "keep both and flag" over clever merging. A false merge is far worse than a visible duplicate.
- Test parsers with real (redacted) samples. The test suite (71 tests) is mostly parser, dedup and reconciliation cases.
Try the demo at https://finsync-green.vercel.app and have a look at the code on GitHub. I'd love feedback — especially on the reconciliation strategy. How do you deal with the same transaction showing up in multiple places?


