Building kharcha-core: Deterministic UPI Expense Engine in Rust
Every UPI payment in India arrives as unstructured text: an SMS from a bank sender (HDFCBK, SBIINB), a push notification from an app (Google Pay, PhonePe, Paytm), or an email receipt.
Building expense trackers on top of these text streams creates three persistent failure modes:
- Parser drift: Maintaining separate regex parsers in Dart and Kotlin causes the same notification to produce different extracted fields across client platforms.
- Floating-point inaccuracy: Storing amounts in standard floating-point numbers (
f32orf64) introduces cumulative rounding errors into balances. - Carrier noise duplicates: Telcos append dynamic promotional footers ("Recharge on Vi App to win 5GB") to the end of bank SMS. Naive string hashing treats identical transactions as distinct entries.
I built kharcha-core to resolve all three issues in a single deterministic Rust engine with UniFFI mobile bindings.
+-----------------------------------+
| Bank SMS / App Push Notification |
+-----------------------------------+
|
v
+-----------------------------------+
| kharcha-core (Rust cdylib) |
| - spam filter (OTP, loan, promo) |
| - fancy-regex pattern matching |
| - i64 paise integer money math |
| - triple-signal deduplication |
+-----------------------------------+
|
+---> ParsedPayment struct
+---> UniFFI Kotlin / Dart bindings
Architecture
kharcha-core compiles to a native C dynamic library (cdylib) and static library (rlib).
src/
lib.rs # Public exports and UniFFI scaffolding
engine.rs # Main entry point and batch parse orchestration
parser.rs # Bank SMS parser with fancy-regex lookahead
non_transaction.rs # Spam filter discarding OTPs, loans, and recharges
categorize.rs # Merchant normalization and rule matching
dedupe.rs # Triple-signal deduplication engine
money.rs # Fixed-point string parser into i64 paise
split.rs # Remainder-preserving bill split algorithm
ffi.rs # UniFFI interface definitions for Kotlin
tests/
parity.rs # 39-test Dart vs Rust parity test corpus
notifications.rs # 13-row push notification test corpus
1. Exact Money in Paise
Floating-point math has no place in financial ledgers. kharcha-core enforces integer amounts represented as Indian paise ($1\text = 100\text$):
pub struct ParsedPayment {
pub amount_paise: i64,
pub merchant: String,
pub is_income: bool,
pub upi_ref: Option<String>,
pub balance_paise: Option<i64>,
pub account_mask: Option<String>,
pub bank_name: Option<String>,
pub needs_review: bool,
}
An expenditure of INR 340.50 parses into 34050. Bill splitting operations distribute fractional remainders deterministically across participants rather than dropping cents to truncation.
2. Triple-Signal Deduplication
When a user pays via Google Pay linked to an HDFC account, the phone receives two alerts within seconds: an app push notification and a bank debit SMS.
kharcha-core reconciles cross-channel duplicates using three tiered signals:
- UPI Reference Number: 12-digit transaction references (RRN/UTR) match across channels regardless of text formatting.
- Normalized Content Hash: When SMS text lacks a reference number, the engine hashes the parsed tuple
(amount_paise, merchant, account_mask)rather than the raw text body. This ignores carrier promotional footers entirely. - 300-Second Time Window: If a notification arrives first and a bank SMS arrives two minutes later with matching amounts and sender accounts, the engine backfills the UPI reference onto the existing record instead of creating a duplicate.
3. Rejection of Non-Transaction Noise
Over 60% of incoming financial SMS messages are non-transactional. non_transaction.rs runs early rejection rules before entering the primary regex pipeline:
- OTP verification codes ("OTP is 481920 for your transaction").
- Bill payment reminders and recharge offers.
- Pre-approved loan advertisements.
- UPI collect and mandate requests.
- Transaction failure notices (marked separately to prevent false expense logging).
4. Test Suite and Parity
The crate includes 76 automated tests:
- 24 unit tests verifying money parsing, categorization, and deduplication logic.
- 39 parity tests validating identical outputs against historical Dart test fixtures across 15 Indian banks.
- 13 real-world notification fixtures from GPay, PhonePe, Paytm, CRED, and BHIM.
The library compiles with zero clippy warnings and exports zero-copy bindings for Android via UniFFI.
More Essays
Building jev-curate: Fast Synthetic Dataset Sifter in Rust
Filtering synthetic training data with TypeSafe AI Jev: streaming JSONL and Parquet rows through calibrated System One gates at 70ms latency with zero memory accumulation.
systemsBuilding jev-git: Sub-Second Git Reflex Gate in Rust
Screening staged git diffs for leaked secrets, destructive payloads, and AI hallucinations in 80ms using TypeSafe AI Jev System One.
systemsBuilding jev-scout: Zero-Hallucination Crate and Repo Scout
Preventing AI package hallucinations: discovering real crates and GitHub repositories using live registry APIs and TypeSafe Jev speculative fan-out scoring.