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
- Signals
- Bank SMSSmsBroadcastReceiver (Kotlin)
- Payment-app notificationsNotificationListener (Kotlin)
- Shared textshare intent
TransactionBridgeModule → JS events - Parse
- SMS + UPI parsersamount, merchant, debit or credit, UPI VPA
- Categorise
- Seven-step categoriseruser corrections always win
- Store
- MMKVsynchronous, local-first
- Firebaseoptional sync, anonymous auth
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:
- A correction the user made for this merchant before.
- The source app: a Swiggy or Zomato payment is Food.
- The UPI VPA in the message.
- Context clues in the surrounding text.
- Merchant and keyword matching.
- Categorising from the full text when the merchant didn't match.
- 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
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.
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.
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.