On-device receipt extraction and exact money

The app does not decide the accounting for you. On-device OCR and rules propose candidates, you confirm them against the receipt image, and only confirmed records reach the monthly totals and CSV.

Source baseline: app 1.0.0 (3) · 23 September 2026

Scope and stack

Version 1 reads Korean and English receipts, one receipt per image, in multiple currencies without conversion. It keeps a summary per receipt rather than line items or categories. There is no account, server, or cloud sync.

UIKotlin and Jetpack Compose, minSdk 26, targetSdk 36
InputML Kit Document Scanner (one page) or Android Photo Picker (one image)
OCRBundled ML Kit Text Recognition v2, Korean model
ExtractionKorean and English label dictionaries plus a shared rule engine, owned by the app
StorageRoom database plus app-private image files
Shared codelibs/ai adapters for OCR and the scanner, and a pure-Kotlin core contract shared with the business card app

The shared modules know nothing about receipts. Receipt labels, the money type, and the merge policy stay inside the app so that a second consumer cannot inherit receipt assumptions by accident.

Input and image lifecycle

The home screen has two separate buttons, Scan and Pick photo. Cancelling the scanner is not a failure and does not open the picker automatically.

The first-run offline promise applies only to the bundled OCR. The scanner module is delivered by Play services, and the picker can list cloud photo providers. “The app does not send receipts to a server” and “obtaining the image never uses the network” are different statements.

Recognition and field states

One bundled Korean Text Recognition v2 model covers both Korean and Latin script, so English and mixed receipts do not need a second model. Supporting a script is not the same as being accurate on receipts, so English receipts are tested separately.

OCR lines are rebuilt into visual rows. ML Kit splits widely spaced text into separate lines. On a receipt the label and the amount sit at opposite ends of one row, so a synthetic Korean receipt measured on an Android 11 test phone produced a merchant and a date but no amount or tax. Pieces whose vertical overlap is at least half of the smaller height are now joined into one row before the rules run.

Each field is Missing, Needs review, or User confirmed. Only the user can produce the last state, and a field the user edited is not overwritten by later recognition. A record is Confirmed only when merchant, date, amount, and currency are all user confirmed and the user saves it as confirmed; everything else is a draft.

The design reserves a Gemini Nano (ML Kit GenAI Prompt) path that would return values plus the IDs of the OCR lines supporting them, validated by the app before use. It is not connected in this baseline: release builds do not include the Prompt module, and a debug-only availability probe on a Galaxy S25 returned UNAVAILABLE. Every result in this version comes from OCR and rules.

Money and currency

Amounts are stored as Money(currencyCode, unscaled: Long, scale: Int), so 12.34 USD is USD / 1234 / 2. The scale is saved with each record and is never reinterpreted from the device's current currency metadata, which can change with ICU and CLDR versions.

Two rules were added after testing on a real device. When every explicit currency mark on a receipt is the same currency, that currency is used as context for unmarked total labels and is marked as a context decision for review (the total's 원 had been misread, but 부가세: 0원 on the same slip survived). And when the currency is explicitly zero-decimal, such as KRW or JPY, the reading of 13,500 as 13.5 is dropped; without that, nearly every won amount carried a false ambiguity. Neither rule applies to ambiguous symbols or to two-decimal currencies.

Totals and CSV export

The home screen shows the current month per currency, with spent, refunded, and net amounts, and no conversion between currencies. Only confirmed records whose transaction type is known count. Drafts and records still marked unknown stay out of the totals.

Storage, privacy, and backup

Verification and known limits

The money, parser, extractor, row assembly, merge, CSV, review form, and monthly total logic is covered by JVM unit tests using synthetic OCR text and fictional receipts; real receipts are kept out of the repository.

A release-only crash. Build 1.0.0 (1) crashed on launch while debug builds ran normally. ML Kit's firebase-components consumer rule keeps registrar classes without members, and R8 full mode removed their no-argument constructors. Component discovery then failed silently and the text recognizer received a null component. The libs/ai OCR and scanner modules now ship a keep rule for the registrar constructors, and the fix was built as 1.0.0 (2). That version code was used up in Play Console without a release, so 1.0.0 (3) is the build prepared for release.