- AI/ML
- OCR
- Gemini
- Document AI
- Vertex AI
AI as evidence, not judge: reading bills and odometer photos
Staff type numbers off paper bills and odometer dials all day, and typed numbers drift. How Ritwik One uses OCR with a rules-based parser for bills and a Gemini vision model for odometers, both in Mumbai, both nothing-kept, and neither allowed to decide anything.
I'm an AI/ML engineer in Ritwik Finance's Digital Transformation and Alliances Department, and I built the document reading in Ritwik One. This post is about where we let AI in, and the much longer list of things we don't let it do.
Two kinds of paper drive money in Ritwik One. In Finance, vendors upload GST invoices and branches photograph shop bills for petty cash. In Travel, field staff photograph the odometer at the start and end of every trip. In both cases a person then types numbers off that paper: an invoice number, a taxable value, a total, a reading in kilometres. Typed numbers drift: a slip of the finger, a rounded figure, or a taxable value of ₹4,200 entered as ₹4,500. Either way, approvers are left comparing photos against forms by eye, bill after bill.
Reading a document is exactly what modern models are good at. Deciding whether to pay is not. So everything in this post follows five rules:
- AI fills in and flags. It never decides. It never approves a bill, sets an amount, or sets a trip's distance. A person does all of that.
- Use the least powerful model that does the job. Bills go through OCR and our own rules; only odometer photos meet a large model.
- The data stays in India, isn't kept and isn't used for training.
- A warning must be something an approver can trust. A guess never accuses anyone, and every bill is still seen by a person.
- When AI is off or down, everything works exactly as before.
Part 1Bills: OCR, then rules we can read
The company's rule for bills is strict: a bill may be read by OCR, but never sent to a large language model, and only if Google neither keeps the file nor trains on it. That rule shaped the whole pipeline.
We use Google Cloud Document AI's Form Parser through its online endpoint on the Mumbai regional address. The file goes inside the request itself: no Cloud Storage input, no batch jobs, no human review, no datasets. Google's terms for online requests say they are processed in memory and not written to disk, and that customer data isn't used for training.
One detail mattered more than it looks. Document AI's newer "Enterprise OCR" was only a release candidate in the Mumbai region, and under Google's pre-release terms the Cloud Data Processing Addendum doesn't apply. So the processor is pinned to Form Parser's generally available version, and the setting refuses any other. A better model that weakens the data-protection terms is not a better choice for an invoice.
OCR gives us text and key-value pairs, not an invoice. Turning one into the other is done by our own deterministic parser for Indian GST invoices. No model is involved, so the same text always produces the same fields, and every field can be explained. It:
- repairs GSTINs using their check character, and tells the seller from the buyer (the company's own GSTINs are always the buyer);
- finds the invoice number and date and the e-invoice reference (IRN);
- reconciles every money line against the total.
taxable value + CGST + SGST + IGST + cess − discount + freight ± round-off = total
Each field comes out HIGH, MEDIUM or blank. Confidence has to be earned. A tax amount is HIGH only if it is a real GST rate applied to a printed taxable value. The total is HIGH only when something independent vouches for it: the amount written in words, the HSN summary table, or a checked taxable value plus taxes that add up to it. When the parser isn't sure, it leaves the field blank. A reading of a simple bill might look like this:
| Field | Read | Confidence | Why |
|---|---|---|---|
| Taxable value | ₹4,200 | HIGH | printed, and the taxes and total agree with it |
| GST | ₹756 | HIGH | exactly 18% of the printed taxable value |
| Total | ₹4,956 | HIGH | taxable value plus taxes add up to it |
| Invoice date | (blank) | none | two dates on the page and no label to tell them apart |
Blank is better than wrong.
The parser is tested the way it will be abused. 41 tests cover real layouts: Tally, shop, restaurant, hotel, fuel, Zoho, marketplace and NIC e-invoices. Others feed it damaged and hostile text and require that no HIGH value changes when lines are lost, merged or reordered. A 200 KB input must parse in under a second.
Part 2Fill the blanks, flag the differences
When a vendor uploads a bill through their link, the form fills in the invoice number, date, taxable value, GST and IRN, but only in fields the vendor hasn't typed. A note says what was filled, which values were hard to read, and that the bill was read on Google Cloud in India without being kept. For petty cash, a photo of a shop bill fills in the shop, amount and date, and "Retake photo" takes back what the old photo filled in.
The useful part comes next. When the bill is submitted, what was typed is compared with what was read, wherever the reading is certain. Any difference becomes a single warning for approvers and Accounts, in words they can check in seconds:
Details don't match the bill: taxable value ₹4,200 (typed ₹4,500); total ₹4,956 (typed ₹5,256)
The same warning catches a seller GSTIN that isn't the vendor's, a GSTIN on a bill from a vendor without GST, a missing IRN, and a quotation or similar document submitted as if it were a bill. If the browser never asked for a reading, the submit reads the bill itself, so the comparison can't be skipped. The same file is read only once, and the form and the submit share that result.
What we keep is only the fields our parser found, in an append-only table. Never the bill's text. Logs record status codes and error names, nothing from the document. Reading runs within budgets, so costs stay predictable.
Part 3Odometers: a vision model with a fixed answer shape
An odometer photo is a different problem. The hard part isn't reading digits; it's judgement. Is this an instrument cluster, or a photo of a screen, a drawing or a random object? Which number is the odometer, as opposed to trip meter A, trip meter B or the speed? Does the display show a tenths digit? That is a visual task, and the photo carries nothing more sensitive than a vehicle's dial, so here we use a Gemini model on Vertex AI, called on the Mumbai region.
The model isn't allowed to chat. It must answer in one fixed structure (is this an odometer, the reading, a confidence and a one-line reason), enforced by Vertex AI's structured output and validated again before anything uses it.
Temperature is 0, so the same photo gets the same answer. One lesson learned the hard way: newer models "think" before answering, and that thinking uses the same output-token budget as the answer. With a small cap, the reply came back empty. The budget is now set high enough for both.
The result is evidence, never a decision. The reading is pre-filled for the officer, who confirms or corrects it. Then the approver may see two flags, each with the model's own words:
- "Start photo may not show an odometer (AI check: …). Verify the photo." when the model says it isn't one;
- "Start reading entered as … km, but the photo reads … km (AI, confidence …%)." when the typed and read values disagree.
If the model is switched off, the image type isn't supported, or the call fails, the officer simply types the reading, and the trip carries on as it always did.
Part 4Guardrails around the models
- The bill rule is enforced by a test, not a promise. A test walks every import Finance makes and fails if any path reaches a language-model SDK or any outside service that isn't on a short list. The OCR client is the only route for bills, and that one file and its single request are pinned.
- Reading never blocks work. The bill-reading service never throws: if reading isn't set up or fails, forms behave exactly as before and nothing is flagged. A submit never waits long on Google.
- Budgets. Reads per hour and per day, files per vendor link and photos per person are all capped, so a bug or a curious user can't run up a bill.
- Every reading is traceable. The audit row for a bill records which reading it used and how that reading came out.
What's next
More invoice layouts, and more kinds of paper, under the same five rules. Every bill is still seen by a person, so reading only ever adds checks; it never removes one.
What I'd tell anyone putting AI into a money workflow
Let AI be the witness. Keep a person as the judge.
- Pick the least powerful model that works. OCR plus rules you can read beat a language model for printed invoices.
- Read the data terms as carefully as the benchmarks. A pre-release model in your region may come with weaker terms.
- Make confidence earned, not reported. A value is HIGH because something independent agrees with it.
- Blank is better than wrong. A warning has to be one an approver can trust.
- Pin the answer's shape with a schema, temperature 0 and validation, and design for the model being off.
Published By Nikhil Kumar, AI/ML EngineerRitwik Finance Enterprises Private Limited