Slip reads a photographed receipt on the phone. The reader is two models, a text layout pass and a small classifier, 11.4 MB together after 8-bit quantisation, running through Core ML on iOS and LiteRT on Android. The reason is partly privacy and partly the basement of a supermarket in Alvalade, where there is no signal and people still want to photograph the receipt before it goes in a pocket. Saying it runs on the device takes one sentence. Getting there took two rewrites.
The median phone is not the phone on our desk
A read takes 380 ms on an iPhone 14. On a Pixel 4a it takes 1.9 s, and on a Galaxy A13, which accounts for 11% of our Android installs, 3.4 s and climbing, because the phone starts throttling after about forty receipts in a row. The fix was unglamorous: batches of eight, a yield between them, and the whole import moved off the main thread. A backlog of 300 receipts went from a hot phone and a killed process to roughly six minutes with the screen off.
The import had to survive being killed
The first Android build kept both models in memory and ran the import as a foreground task. Android ended it around receipt 60 whenever the app was backgrounded, and the import then started again from zero, which is the sort of bug that makes a person stop trusting an app entirely. It was rewritten as a resumable job with a cursor in SQLite and writes that can repeat without duplicating anything, and shipped in 2.1.3 on 07 Apr 2026. A killed import now picks up at the receipt it was reading.
What is not on the device
Receipts forwarded by email are parsed on our servers in Frankfurt, not on your phone, and the original message is deleted after 30 days. Sync is the other exception: turn it on and your receipts are stored on our servers so that search and export work on a laptop, encrypted in transit and at rest. Turn it off and nothing leaves the phone, and the web app stays empty, which is what the app tells you at the moment you switch it. A claim about on-device processing is only worth anything with the exceptions written next to it.
What the small model costs us
A reader that fits in 11.4 MB is worse than one we could run on a server. On the 2.9% of the test drawer that is faded or folded through the total, a larger model would get more of them right, and we decided against it. What we did instead was make the asking path quick: the receipt opens with the uncertain field focused, the keypad already up, and the date filled in from the photo. Model load costs 240 ms at cold start, which we hide by loading while the camera preview opens.