CatatHisabExpense tracking that fills itself in from bank alerts

CatatHisab reads the push notifications and emails BCA sends for every payment and turns them into one spending timeline, with nothing typed in. We designed and built the Android and web app and the API behind it.

Three CatatHisab screens on phones: a payment's details, the timeline with spending for today, this week and this month and payments grouped by day, and insights with daily digests and a spending alert
Client
Own product
Sector
Personal finance
Year
2026
Platforms
Android app, Web app
Services
Product and UX design, Web development, Mobile apps

An expense tracker that fills itself in. It captures BCA's payment notifications on the phone, reads BCA's transaction emails from the user's mailbox, and merges the two into a single record for each payment, with a monthly summary and plain-language insights on top.

Built for

  • BCA customers who pay by QRIS, transfer, virtual account and debit card every day.
  • People who want to know where the month went without keeping a spreadsheet.

The problem

Expense apps ask people to type in every purchase. The bank already reports every purchase, usually twice: a push notification from the myBCA app within seconds, and an email receipt a little later.

Neither message is complete on its own. Notifications mask the other party's name, the emails come in several formats depending on how the payment was made, and some payments produce neither.

Counting has to be exact in both directions. The same payment reported twice must become one record, and two genuine payments of the same amount at the same shop must stay two.

Goals

  1. 01Record spending from BCA without manual entry.
  2. 02Count each payment once, whichever channels report it.
  3. 03Fill in masked names from the fuller data in the emails.
  4. 04Import a monthly statement to fill the gaps without creating duplicates.
  5. 05Summarise spending in plain language, in the app, by push and by email.

Our role

We designed and built the app and the API, and run the platform in production.

What we delivered

  • Interface design for a phone-first app, also served on the web
  • Expo and React Native app for Android and the web
  • Kotlin notification listener with an offline queue
  • Bun and Hono API with Drizzle and PostgreSQL
  • Six BCA parsers: notifications, four email formats and the statement PDF
  • Deduplication, merging and masked-name resolution
  • Encrypted mailbox connection and background email polling
  • Insights with AI wording and a template fallback, delivered in the app, by push and by email
  • Deployment pipeline with a type-check gate, health check and automatic rollback

How we worked

  1. 01

    Start from real bank text

    Every parser was written against sample BCA notifications and emails kept in the repository, and the tests use those samples inline, so a change in the bank's wording shows up as a failing test.

  2. 02

    Capture on the phone, read mail on the server

    A native service on the phone picks up myBCA notifications as they arrive. The server polls the user's mailbox for BCA emails in a background job, with the mailbox credentials encrypted at rest.

  3. 03

    Merge before anything is shown

    Every incoming signal is parsed, then matched against existing records by reference number, then RRN, then two levels of fingerprint. A match is merged into the existing payment, and anything else becomes a new one.

  4. 04

    Fill the gaps from the statement

    A monthly statement PDF can be uploaded to add whatever never arrived by notification or email. The import adds only the payments the timeline is missing.

  5. 05

    Ship through a gate

    A push to the main branch deploys through a webhook that type-checks, migrates the database, reloads the processes and checks health, and rolls back when the type check or the health check fails.

Screens and features

  • Nothing to type

    Payments arrive from the phone's notifications and from the user's email. Manual entry covers anything the bank never reports.

  • One record per payment

    When a notification and an email describe the same payment, the two are merged and the record shows both sources with the time each one arrived.

  • Masked names filled in

    Notifications hide most of the other party's name. The app compares the visible letters with full names from email data and fills the name in when the match is strong enough.

  • A statement import that cannot double count

    A statement is identified by the hash of its bytes, so the same file is never imported twice. A corrected statement for the same month still imports, and only the payments missing from the timeline are added.

  • Insights by app, push and email

    Daily digests, a monthly summary and spending alerts, each delivered on the channels the user has switched on.

  • Search and filters

    The timeline searches merchants and references, and filters by expense or income and by payment channel.

Technical choices

What the product is built with, and why.

A native Android notification listener
Only a native service can read another app's notifications. It is written in Kotlin on the Expo Modules API and saves each capture to the phone's storage first, so nothing is lost if the app is closed before it syncs.
An offline queue in two tiers
The native store survives the app being killed, and a JavaScript queue sends captures to the API in batches every 30 seconds, so a phone without signal catches up later.
Drizzle with postgres.js on Bun
Prisma's command line tools had bugs under Bun, and Bun's built-in SQL client had a connection hang, so the API uses Drizzle over the postgres.js driver.
Background jobs on BullMQ and Redis
Mail polling, parsing, insights and statement imports run in a separate worker process, each queue with its own concurrency, so a slow mailbox does not hold up the API.
Deduplication that prefers a duplicate to a gap
When a match is uncertain, both records are kept. A duplicate shows up on the timeline, while a payment merged away by mistake would simply be missing.
Figures computed before any AI is involved
Insight figures are built from the transactions with no AI call. A model only writes the sentences, and when AI is off or failing, a circuit breaker switches to template wording.

Turning messy messages into clean records?

Parsing, deduplication and background jobs are work we enjoy. Tell us where your data comes from.