Skip to content
Mudassir MohammedRésumé

Personal project

Koin

An Android-first expense tracker that logs UPI and bank transactions on its own, from SMS and payment-app notifications.

Role
Sole engineer
When
2026
Status
Android app, built with EAS
Stack
React Native · Expo · TypeScript · Kotlin · MMKV · Firebase

Overview

In India, most everyday spending goes through UPI apps and bank cards, and every transaction already produces a message: an SMS from the bank, or a notification from GPay, PhonePe or Paytm. Koin reads those signals instead of asking people to type their expenses in.

Captured transactions are parsed, categorised and stored on the device. Budgets, analytics, a spending heatmap and home-screen widgets sit on top. Biometric lock and anonymous cloud sync are optional.

How it works

  1. Signals
    • Bank SMS
      SmsBroadcastReceiver (Kotlin)
    • Payment-app notifications
      NotificationListener (Kotlin)
    • Shared text
      share intent
    TransactionBridgeModule → JS events
  2. Parse
    • SMS + UPI parsers
      amount, merchant, debit or credit, UPI VPA
  3. Categorise
    • Seven-step categoriser
      user corrections always win
  4. Store
    • MMKV
      synchronous, local-first
    • Firebase
      optional sync, anonymous auth
Capture pipeline. The native layer only parses and forwards; everything after that is shared TypeScript.

Android only exposes SMS broadcasts and other apps' notifications to native code. So two Kotlin components do the capturing. Multi-part SMS are reassembled per sender, non-banking messages and credits are dropped, and parsed debits go to JavaScript through a bridge module. Everything downstream is shared TypeScript.

Categorisation without a model

A merchant name alone is ambiguous: “Amazon” can be shopping, groceries or a subscription. The categoriser runs ordered steps and stops at the first confident answer:

  1. A correction the user made for this merchant before.
  2. The source app: a Swiggy or Zomato payment is Food.
  3. The UPI VPA in the message.
  4. Context clues in the surrounding text.
  5. Merchant and keyword matching.
  6. Categorising from the full text when the merchant didn't match.
  7. A payment app with no other context is most likely a personal transfer.

It's deterministic on purpose. It runs on the device with no API cost, every decision can be explained, and corrections feed step 1, so it improves for each user without a model.

Engineering decisions

  1. Local-first storage with MMKV

    Why
    MMKV reads are synchronous through JSI, so screens render from local data immediately and sync runs in the background.
    Instead of:
    AsyncStorage, which is asynchronous on every read.
  2. Native code only where the platform demands it

    Why
    The Kotlin layer does as little as possible: receive, filter, parse and forward. Keeping categorisation and storage in TypeScript means one implementation covers SMS, notifications and the share flow.
  3. Anonymous auth for sync

    Why
    Requiring sign-up is a fast way to lose people in a finance app. Firebase anonymous auth gives each device a stable identity for cloud backup without asking for an email.

Known gaps

  • It's Android-first. iOS doesn't let apps read SMS or other apps' notifications, so on iOS only the share flow works.
  • Bank SMS formats vary, and each new bank can mean new parsing patterns.
  • The repository needs a README and tests. The parsers and the categoriser are the obvious first candidates for unit tests.