Skip to content

Repository files navigation

Expense Tracker

An Android expense tracker that reads bank SMS messages on your phone and turns them into a categorized, queryable ledger. Everything runs locally — there is no server, no account, and the app does not declare INTERNET permission.

Built with React Native + Expo, expo-sqlite, and TypeScript.

Why

Indian banks send a transactional SMS for every debit, credit, UPI transfer, and card swipe. Those SMS contain everything you'd want from a Mint-style tracker (amount, merchant, account, time) and they're already on your phone. The app reads them, parses them, dedupes them, learns your categories, and lets you slice the data — without leaving the device.

Features

  • SMS backfill — on first launch, reads the last 180 days of SMS from supported banks and populates the ledger.
  • Foreground rescan — pull-to-refresh runs a 7-day rescan; the dedupe key prevents duplicates.
  • Supported banks/streams
    • HDFC UPI (src/sms/parsers/hdfcUpi.ts)
    • HDFC Credit Card (src/sms/parsers/hdfcCc.ts)
    • Axis Credit Card (src/sms/parsers/axisCc.ts)
  • Manual entry — a Cash account is seeded on first launch; the floating action button on the Transactions screen opens a manual-entry form.
  • Smart categorization
    • Built-in seed rules (Swiggy → Food, Uber → Transport, etc.).
    • Learned rules: tagging a transaction's merchant once teaches the app for all future and (optionally) past transactions with the same merchant.
  • Reports — month-over-month spend, by-category, and by-account breakdowns.
  • Dedupe — transactions are keyed by account + IST-day + amount + direction with optional ±1-day fuzzy matching to handle settlement-day shifts.
  • IST-correct dates — all calendar-day math is done in Asia/Kolkata so "today" never drifts.
  • Paise-precision money — amounts are stored as integer paise; no floats.

Architecture

App.tsx                          # init: DB → seed categories → ensure Cash → register parsers
├── app/                         # UI layer
│   ├── navigation/              # Stack + bottom tabs
│   ├── screens/                 # Onboarding, Transactions, Detail, Add, Reports, Settings
│   ├── components/              # TransactionRow, CategoryPicker
│   ├── hooks/                   # useTransactions, useForegroundRescan
│   └── theme.ts
└── src/                         # Domain + persistence (platform-agnostic)
    ├── db/
    │   ├── driver.ts            # DbDriver interface
    │   ├── driver.expo.ts       # expo-sqlite implementation
    │   ├── driver.node.ts       # better-sqlite3 implementation (tests)
    │   ├── schema.ts            # SCHEMA_V1 + MIGRATIONS
    │   ├── migrations.ts
    │   ├── accounts.ts          # ensureCashAccount, account lookup
    │   ├── categories.ts        # built-in category seeding
    │   ├── transactions.ts      # insert/list/get/delete, dedupe, reports queries
    │   └── merchantRules.ts     # learned rule storage
    ├── sms/
    │   ├── senders.ts           # sender → bank mapping
    │   ├── parse.ts             # parser registry
    │   ├── parsers/             # one file per bank-stream
    │   ├── resolveAccount.ts    # parsed account-hint → accounts table row
    │   ├── backfill.ts          # SMS rows → transactions, with dedupe + categorize
    │   └── inbox.ts             # SMS reads via react-native-get-sms-android
    ├── categorize/
    │   ├── normalize.ts         # merchant normalization (lower, strip noise)
    │   ├── seedRules.ts         # built-in merchant → category map
    │   ├── apply.ts             # learned rule → seed rule → null
    │   └── learn.ts             # write learned rule, optional bulk-recategorize
    └── utils/
        ├── date.ts              # IST helpers, nowIso, istCalendarDay
        └── money.ts             # paise ↔ display formatting

Data model

SQLite tables (see src/db/schema.ts):

  • accounts — Cash + one row per detected (bank, kind, last4).
  • categories — built-in + user-defined; unique by name.
  • transactions — one row per parsed SMS or manual entry. amount_paise is an integer; occurred_at is ISO-8601 UTC; dedupe_key is the sha256 of accountId|IST-day|amountPaise|direction.
  • merchant_rules — learned merchant_norm → category_id mappings.
  • unparsed_sms — SMS from supported senders we couldn't parse. Useful for triaging new message formats.
  • sms_inbox_queue — reserved for a future background-receiver flow.
  • settings — key/value store.

Categorization precedence

resolveCategory(merchantNorm) (see src/categorize/apply.ts) looks up, in order:

  1. A learned rule for merchantNorm in merchant_rules.
  2. A built-in seed rule from seedRules.ts.
  3. null (transaction lands in "Misc").

When you change a transaction's category from the detail screen, the change is recorded as a learned rule, so future transactions from the same merchant are auto-categorized.

Getting started

Prerequisites

  • Node.js 20+
  • An Android phone with USB debugging enabled (the app is Android-only — see app.json).
  • For local Android builds: Android SDK + JDK 17. For cloud builds you only need an Expo / EAS account.

Install dependencies

npm install

Run the test suite (Node, no device needed)

npm test           # one-shot
npm run test:watch # watch mode
npm run typecheck  # tsc --noEmit

Tests use the better-sqlite3 driver (src/db/driver.node.ts) so they run in plain Node — no emulator required. Parsers, dedupe, categorization, IST date math, and migrations all have unit tests.

Build and install on a device

See docs/build-and-install.md for the full walkthrough. Short version:

# Cloud (no Android SDK needed)
npx eas-cli login
npx eas-cli build --platform android --profile preview
# download the .apk from the link in the output

# Local (requires Android SDK + JDK 17)
npm run prebuild
npm run build:apk

Then on the phone:

adb install path/to/expense-tracker.apk
# update an existing install without wiping data:
adb install -r path/to/expense-tracker.apk

First-run flow

  1. App opens to the Onboarding screen.
  2. Tap Grant access and import — Android prompts for SMS permission.
  3. Backfill reads up to 180 days of SMS, parses what it can, and stores the rest in unparsed_sms for later inspection.
  4. The Transactions screen opens. Pull to refresh to run a 7-day rescan.
  5. Tap a transaction → Change category to teach a merchant rule. Past and future transactions from that merchant pick up the new category.
  6. Tap the + floating action button to add a manual (Cash) transaction.

Privacy

  • All data stays on-device in a SQLite database managed by expo-sqlite.
  • The app requests READ_SMS and RECEIVE_SMS only.
  • INTERNET is intentionally absent from app.json. Audit android/app/src/main/AndroidManifest.xml after prebuild; if any plugin injects INTERNET, remove it before shipping. The build-and-install doc flags this explicitly.

Scripts

Script What it does
npm start Start the Expo dev server
npm run android Build + run on a connected device/emulator
npm run prebuild Regenerate android/ (run after dependency changes)
npm run build:apk Local EAS build → APK in build/
npm test Jest suite
npm run test:watch Jest in watch mode
npm run typecheck tsc --noEmit

Adding support for a new bank

  1. Add a parser at src/sms/parsers/<bank><stream>.ts that takes (body: string, receivedAt: string) and returns a ParsedTxn | null.
  2. Add fixtures under src/sms/parsers/__fixtures__/ and a matching <parser>.test.ts.
  3. Register the parser in src/sms/registerParsers.ts.
  4. Extend matchSender in src/sms/senders.ts if the sender ID needs a new bank tag.
  5. Run npm test.

Use unparsed_sms as a source of real-world fixtures: anything the app sees but can't parse lands there with the original sender, body, and timestamp.

License

Private / unpublished. No license granted.

About

An Android expense tracker that reads bank SMS messages on your phone and turns them into a categorized, queryable ledger. Everything runs locally — there is no server, no account, and the app does not declare INTERNET permission. Built with React Native + Expo, expo-sqlite, and TypeScript.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages