All work

Rekhon

Mobile · AI · Case studyOpen source · dual-licensed

Android personal-finance app that shows its working — every figure cites the rule and version that produced it. Fully offline and encrypted on the device, with no account and no server. Rupees are integer paise end to end, and a custom lint rule fails the build if a floating-point type ever holds an amount.

Role
Sole author
Team
Solo project
Timeline
July 2026 to present
Commits
349 of 350
Stack
Kotlin · Jetpack Compose · Room / SQLCipher · Hilt · ML Kit · Coroutines / Flow · Android Keystore · Gradle

Responsibilities

  • Module graph: 60 Gradle modules on a one-way feature → domain → data/core dependency rule
  • Deterministic wealth engines: amortisation, XIRR, allocation and concentration
  • On-device encryption: SQLCipher keyed from the Android Keystore
  • Capture pipeline: ML Kit receipt OCR and bank-SMS parsing behind revocable consent
How it worksA receipt becomes a cited figure
  1. 1

    Photo

    feature/transactions

  2. 2

    On-device OCR

    ml/ocr (ML Kit)

  3. 3

    Classification

    domain/engines/classification

  4. 4

    Exact paise

    core/model/Money.kt

  5. 5

    Encrypted write

    core/database (Room + SQLCipher)

  6. 6

    Rule evaluation

    ai/rules + domain/engines/insight

  7. 7

    Dashboard + widget

    feature/dashboard, widget

DataSQLCipher (Room)Proto DataStoreAndroid Keystore

Situation

Why a personal finance app should show its working

Most finance apps hand you a number with no way to check it: a health score, a projection, a “you can afford this” — with no statement of what was measured or what it was measured against. A figure you cannot check is a verdict. The apps that do explain themselves generally want your bank credentials and a server round trip to do it, which is a steep price for arithmetic that a phone can do on its own.

Task

Building an offline finance app whose figures can be checked

Build a finance app where every figure is checkable and nothing about the money leaves the device. That makes two things non-negotiable rather than nice to have: the numbers must be exact, and the rule behind each one must be addressable — a specific id at a specific version — so an answer stays reproducible months later.

Action

Exact paise, on-device encryption and 60 Kotlin modules

The app is 60 Gradle modules on a one-way dependency rule (feature → domain → data/core), with the engines as pure Kotlin that cannot import Android — so the money maths is unit-testable without a device, and 355 test files holding 3,822 tests run on the JVM. Money is integer paise end to end: a custom Android Lint module flags any floating-point declaration with a monetary name and fails the build, because binary floating point cannot represent 0.10 and the error is too small to notice until a reconciliation disagrees. Room sits on SQLCipher under a key held in the Android Keystore. ML Kit reads receipts and, with explicit consent that is off by default and revocable, bank SMS is parsed into proposed entries — neither leaves the phone. Financial thresholds are versioned JSON rows in ai/rules rather than constants in code, and every insight cites the rule id and version that produced it. 77 ADRs record why each of these is the way it is. The wealth maths is where the determinism requirement bites hardest, and XIRR is the clearest case.

domain/engines/investment/.../Xirr.ktkotlin
// XIRR is normally NPV(r) = Σ cf · (1+r)^(−d/365), solved by Newton-Raphson from a guess.
// Both halves are hostile to exactness: the fractional exponent has no exact decimal form,
// so an implementation reaches for Double; and Newton's answer depends on the starting guess.
//
// Substituting x = (1+r)^(1/365) and multiplying through by x^dMax clears every division:
//     F(x) = Σ cf_i · x^(dMax − d_i)
// — a polynomial with whole-number exponents, so BigDecimal.pow is all it needs.
private fun bisect(
    signAtLow: Int,
    values: List<Long>,
    exponents: List<Int>,
): BigDecimal {
    var low = X_LOW
    var high = X_HIGH
    // Fixed iteration count over a literal bracket, with no early exit: no guess to depend
    // on and no tolerance branch to drift, so the same flows give the same basis points on
    // every machine and every build. Slower than Newton, and it does not care.
    repeat(ITERATIONS) {
        val mid = low.add(high).divide(TWO, CONTEXT)
        if (polynomialAt(mid, values, exponents).signum() == signAtLow) {
            low = mid
        } else {
            high = mid
        }
    }
    return low.add(high).divide(TWO, CONTEXT)
}

Result

Fourteen epics, 3,822 tests and a public repository

Fourteen epics are built and verified — foundation, onboarding and security, capture, categorisation and budgets, dashboard and export, wealth, goals, backup, the AI core engines, the advisor and chat, privacy hardening, release quality and expansion — at v0.13.9, with the most recent committed work dated 10 October 2026. Eleven account types roll up into a daily net-worth snapshot; credit cards carry their real billing cycle and utilisation; loans amortise to the rupee; holdings carry dated lots and a money-weighted return. The source is public on purpose — the central claim is that nothing about your money leaves your device, and that is not something a store listing can prove.

349

of 350 commits authored

3,822

tests across 355 files

60 modules

one-way dependency graph

Where it stands

Roadmap

Version
0.13.9
As of

Shipped and verified14

  • Epic 0 — Foundations and the AI blueprint

    v0.1.0

  • Epic 1 — Foundation and core platform

    issues 1.1–1.10

  • Epic 2 — Onboarding, security and accounts

    v0.2.0

  • Epic 3 — Transactions and capture

    v0.3.0

  • Epic 4 — Categorisation and budgets

    v0.4.0

  • Epic 5 — Dashboard, export and widget

    v0.5.0

  • Epic 6 — Wealth: loans and investments

    v0.6.0

  • Epic 7 — Goals and emergency fund

    v0.7.0

  • Epic 8 — Backup and restore

    v0.8.0

  • Epic 9 — AI core engines

    v0.9.0

  • Epic 10 — Advisor and chat

    v0.10.0

  • Epic 11 — Privacy and security hardening

    issues 11.1–11.7

  • Epic 12 — Quality, testing and release

    v0.12.0

  • Epic 13 — Expansion

    v0.13.0

Most recent committed work3

Open findings1

Designed, not built1

  • Account Aggregator integration

    The design landed and one reserved source value shipped with it, because the enum had to be right before data existed. The integration itself is not built.

    ADR-0074 · v0.13.5

Blocked1

  • iOS through Kotlin Multiplatform

    “Android-free” turned out not to mean “KMP-portable”. Money is the blocker: the exact-paise type is the part that does not port, which makes this a rewrite of the one thing that must not change.

    ADR-0075 · v0.13.6

Read from Rekhon CHANGELOG.md and docs/adr/ on the public dev branch. Items that are designed, open or blocked are labelled as such and are not claimed as shipped.

Build journal

How the thinking moved

Dated entries on the decisions behind this project, each linking to the record it came from.

All entries

Looking back

What I would do differently

  • Account Aggregator is designed and one reserved value has shipped, but the integration itself is not built (ADR-0074).
  • iOS through Kotlin Multiplatform is blocked rather than scheduled, and the blocker is Money: the exact-paise type is the part that does not port (ADR-0075).
  • ADR-0078 closed the import-batch finding and opened one of its own, on orphaned receipt blobs.
  • Publish the golden-file vectors for the wealth engines, so a reader can re-run the money maths without building the app.