Blog · Car Brain / AI / Appwrite / Architecture

The model writes a draft. You save the record.

Car Brain 3.1.0 reads a fuel receipt photo into the refuel form. Why the scan never writes a refuel, what it leaves empty on purpose, and what the convenience costs.

Tomasz Stanisz6 min read

A fuel receipt is a document designed for a tax office, a thermal printer and nobody else. The litres share the paper with a tax ID, a cashier and a till address, the total hides under the word SUMA, and the decimal separator is a comma that one careless parser can turn into a five-thousand-litre refuel.

Car Brain 3.1.0 reads that receipt anyway. In Add Refuel Record and Edit Refuel Record on the phone, Scan Receipt takes a photo and fills in the form. Getting a model to read the paper was the easy decision. The harder one was what the model is allowed to do with what it read.

A logbook is only as trustworthy as its worst row. A wrong total does not stay in one refuel: History, the monthly recap and every cost figure built on saved rows inherit it. So the rule is short: the model writes a draft, the person saves the record.

A draft, not a record

The form keeps a 960 px copy as the refuel's receipt, as it did before scanning existed. A separate 1,600 px JPEG goes to scan-receipt, an Appwrite Function, and that copy is never uploaded to storage.

The model drafts. The person saves.1. The phone attaches the photo to the refuel form as its receipt. 2. It sends a JPEG copy with the signed-in session to the scan-receipt Appwrite Function, which holds the Gemini key. 3. The function counts and reserves one scan in the ai_usage table. 4. It sends the photo and a JSON schema to Gemini. 5. Gemini returns JSON. 6. The function checks value ranges and drops any tank state or odometer. 7. It returns a draft and warnings to the phone. 8. The form fills empty fields and the person checks them. 9. Only when the person taps Save is a refuel row written to the Appwrite database.RECEIPT SCAN / MOBILE 3.1.0The model drafts. The person saves.PhoneRefuel formscan-receiptholds the Gemini keyGeminivision modelAppwrite DBfuelRecords · ai_usage1. Photo attachedas the receipt6. Check ranges,drop tank + odometer8. Fill empty fields,person checks2. JPEG + session3. Count + reserve a scan4. Photo + JSON schema5. JSON7. Draft + warnings9. Save → refuel rowNo refuel row exists before step 9.The only earlier write is the usage counter.
Request sequence for Scan Receipt in Car Brain mobile 3.1.0, drawn from the code at tag v3.1.0. Solid: request. Dashed: response. The thick arrow is the only write to the refuel records. On a narrow screen, scroll the diagram horizontally.

The function runs with the caller's session. Appwrite sets the x-appwrite-user-id header, and a call without it stops at 401.

Nothing reaches the refuel records until the person taps Save. The scan's only write of its own is a usage counter, with no amounts and no station in it.

The Gemini key lives in the function's environment, never in the phone or web app. The VIN lookup needed the same boundary, and that story is already told: Your API key is private. Until you ship it. In 3.1.0 only the phone scans. The web app does not.

The form decides what the model may touch

The draft's keys mirror the form, not a parallel "receipt model": fuel type, litres, unit price, total, currency, date and station.

Two fields are missing on purpose. Fill state and odometer are almost never printed, so the function drops them even if the model sends them, and the phone drops them again. A scan cannot decide you filled up.

The scan is also polite about your typing. It fills only empty or untouched fields, so a station you already typed stays yours. Every filled field says "From the receipt." until you change it. If exactly one of litres, unit price and total is missing, the form calculates it and labels it the same way, so the label can mean "worked out from the receipt".

The prompt tells Gemini to copy only what is printed and leave out what it cannot see. Doubt differs from absence: a hard-to-read number that was kept arrives with a "Some numbers were hard to read. Check them." warning, not as an empty field.

Reading 50.85 litres, not 5085

Polish receipts print 50,85. Strip the comma and you get 5085, which is a tanker, not a car. Three layers stand in the way:

  • The prompt says it in plain words: 50,85 is 50.85, never 5085.
  • The parser reads 50,85 and 50.85 as the same number and never removes a separator to glue digits together.
  • Range checks drop what no car would log: litres above 0 and at most 300, a unit price under 50, a total of at most 20,000. An out-of-range value is dropped, not clamped to the limit.

The arithmetic gets a vote too. If litres times unit price misses the total by more than 2%, all three values stay and the form says they do not match. The date must be at most two years old and at most a day ahead. Amounts in another currency are filled but not converted, and the form says so.

None of this makes a misread impossible. It makes a misread visible, and fixing one means typing into an ordinary field.

What the convenience costs

CostHow 3.1.0 handles it
A paid Gemini call per scan3 scans per account in a rolling 24 hours. The scan is reserved before Gemini is called, so two quick taps cannot both slip past the cap
A receipt photo leaves the phoneNamed in the English and Polish privacy text. The function logs the user, the outcome and the duration, never the image, the amounts or the station
A network call inside a formGemini gets 25 seconds. A failed scan leaves manual entry open, and the photo stays attached when it fits as a receipt

A reserved scan stays used when the photo is not a fuel receipt, when Gemini times out and when the read fails, and the app says so. The cap exists so a vision bill cannot run away, and results are not cached.

The data-processing section of the in-app privacy text says: "When you scan a receipt, that photo is sent to Google Gemini through Car Brain's server to suggest form fields. You review those fields before saving. The scan does not store the photo." That is the whole claim. The Play Data safety form and the App Store privacy label need the same re-check, which no test in the repository can do.

When I would revisit it

The decision record names two directions. Neither is a plan.

  • On-device OCR. Rejected for the first version: Polish fiscal layouts vary, and the project already runs Gemini in a function. If the cost per scan or the privacy cost of sending a photo outweighs that, reading on the phone is the next question.
  • Service and expense receipts. The same function shape could fill other forms later. Today it fills refuels only.

The option I would not revisit is auto-saving the draft. A model is allowed to be wrong. A logbook that saves its mistakes for you is wrong for good, quietly, with charts.

Development snapshot — 14 November 2026: written from the code at tag v3.1.0 (13 November 2026) and ADR-0035. Release 3.1.1 changes only VIN decoding, not the scan. I have not measured accuracy, latency or cost per scan, so this article gives none.