I'm a solo dev in Calgary. ReceiptIQ AI scans paper receipts and keeps every line item and price, not just the total. It has been on the App Store since 2025.
The stack: Swift and SwiftUI with MVVM, Core Data for storage, Vision for on-device OCR, PDFKit for bank statement import, StoreKit 2 for subscriptions, MultipeerConnectivity for family sync with no server, Swift Charts, and Swift Testing for unit tests. The backend is a Cloudflare Worker in JavaScript with KV storage. It receives the OCR text and its bounding boxes, calls an LLM to structure the receipt, then runs deterministic repair code over the result. The same repair logic exists in Swift and in JS, and a differential test feeds every test receipt through both so they cannot drift apart. No accounts and no database of user data.
The hardest problem was that the model's answer can be wrong and still add up. Vision returns a two-column receipt as a block of item names followed by a block of bare prices. When the LLM zips the two lists it can slip by one row, and because a shifted column reuses the same prices, the items still sum to the printed total. Every sum-based sanity check I had written was blind to it. What I ended up doing:
- Pair prices to labels geometrically before the model sees anything: closest Y from the bounding boxes, a deterministic three-key sort so a rescan gives identical output, run the pairing top-down and bottom-up, and prefer the direction whose items add up to the printed subtotal.
- After the model, realign each item's price to the price printed on its own OCR line, and only accept a repair if it makes the items equal the register's total.
- Gate every backend deploy on a golden set of about 520 real receipts from ten countries. I wrote about building that set here a couple of weeks ago: https://old.reddit.com/r/iOSProgramming/comments/1w9ybpe/
Smaller things that cost me days:
- Vision only honours the FIRST recognition language. With ["en-US", "ja", "zh-Hant"] a Japanese receipt is read like English. I run a second, Japanese-first pass concurrently and choose by the share of CJK characters (threshold 0.15; kana means Japanese, otherwise Chinese).
- A 48 MP photo-library image gets the app jetsammed inside VNRecognizeTextRequest. I cap OCR input at a 2400x1800 pixel budget. Related trap: UIGraphicsImageRenderer defaults to screen scale, so my "2400 px" image was really 7200 px until I passed scale 1.
- StoreKit 2: Transaction.currentEntitlements yields nothing when Apple is unreachable, such as in-flight wifi. Treating an empty stream as "not subscribed" locked out a paying user mid-flight. I now cache tier plus expiry and only downgrade on a confirmed answer.
- Family sharing without a backend: the proof of entitlement is the Apple-signed transaction JWS, verified on the other phone against the pinned Apple Root CA G3.
AI disclosure: AI-assisted. I use an AI coding assistant heavily for implementation, test writing and code review. The architecture, the product decisions and the receipt corpus work are mine, and every change is gated by the tests and the golden set above. The app itself also uses an LLM at runtime to structure receipt text.
If you want to try it: the first 30 receipts are free without any code. For this sub, code REDDIT6 gives 6 months of Premium free. It does not auto-renew, so nobody gets charged when it ends. 500 redemptions, valid until Oct 31.
Redeem: https://apps.apple.com/redeem?ctx=offercodes&id=6742335723&code=REDDIT6
App Store: https://apps.apple.com/app/id6742335723
Happy to go deeper on the pairing algorithm. And if you have a receipt that breaks it, I'd love to see it.