James Carl Sitsit

OCR can read the right numbers and still choose the wrong one

GCash ads put valid peso amounts ahead of the receipt total in OCR text. Cashi checks that amount plus fee equals the total to reject ad noise.

By James Carl Sitsit · From the Cashi case study

I expected blurry text to be the hard part of receipt OCR. The worse failure was clean text in the wrong context.

GCash receipt screenshots can include ad banners with peso amounts. ML Kit may read the ad perfectly, then hand that number to the parser before the transaction total exists in the text stream. A parser that grabs the first currency value picks the wrong amount even though the recognizer did its job.

That changed what I thought I was debugging. OCR accuracy still mattered, but the harder problem here was selection.

Position and labels are weak evidence

On GCash load and bills-payment receipts, Amount, Convenience Fee, and Total sit in a left column and their figures sit in a right column. When an ad banner sits on the screenshot, ML Kit can read the left column of labels as one block, read the ad banner next, and only then read the right column of numbers.

In the resulting text, Total is no longer next to the total. A regex looking for a label followed by a peso amount either misses the separated figure and falls back to the first ₱ in the ad, or matches a phrase like Buy Load PHP 50 inside the banner itself.

Both position and label rules depend on layout. Because the ad is part of that layout, neither one can tell whether a valid peso amount belongs to the transaction or to the banner around it.

The parser needed evidence from the numbers themselves.

Use the relationship on the receipt

When a load or bills-payment receipt includes a Convenience Fee line, it carries three transaction figures: the amount, the fee, and the total. Those values satisfy an equation: the amount plus the fee equals the total.

Instead of trusting whichever peso value appears first, Cashi collects every currency and two-decimal number on the receipt and looks for three unique values where two add up to the third. On a load receipt with a 99.00 load, a 3.00 fee, and a ₱102.00 total, those three figures add up. An advertised figure from the banner on the same screenshot, such as ₱1,015.03 from a savings promo or PHP 50 from an insurance ad, does not pair with another number on the receipt to form a sum.

This does not make OCR smarter. It gives the parser a way to reject a number that looks valid on its own but does not belong to a valid set.

That distinction is the useful part for me. Recognition answers, "What characters are in this image?" Parsing has to answer, "Which of those characters describe the transaction?" The second question needs domain rules, not a better regular expression around the first number.

Where this stops working

Arithmetic consistency only helps when the receipt exposes values with a relationship the parser can check. In Cashi, that rule runs on load and bills-payment receipts that include a Convenience Fee line. Zero-fee load promos and send-money receipts carry a single amount, so they still rely on the labeled and currency patterns built from the receipt layouts I have tested.

Cashi does not treat that check as permission to save blindly. Parsed receipts still go through the confidence gate and the review screen before confirmation. The arithmetic rule cuts one class of false match. It does not certify the whole receipt.

That is the narrower lesson I would keep from this parser: an OCR result can be textually correct and semantically wrong. When the real values obey an equation, that equation is better evidence than where a number happened to land on the screen.


This is from Cashi, a Flutter app that reads GCash receipts on-device to reconcile cash and e-float. The case study covers the full build.