• 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.
Two reading pipelines, one decision-maker Top row, vendor bills in Finance: the bill goes to OCR on Document AI in Mumbai, which keeps nothing; our own rules-based parser turns the text into fields with a confidence; the form fills only blank fields and only high-confidence values are compared with what was typed. Bottom row, odometer photos in Travel: the photo goes to a Gemini model on Vertex AI in Mumbai at temperature 0; it answers in a fixed JSON shape with whether it is an odometer, the reading, a confidence and a reason; the reading is pre-filled for the officer to confirm and differences become flags. Both rows end at the same box: a person decides. Vendor bills (Finance) BillPDF or photofrom the vendor OCRDocument AI,Mumbai, not kept Our parserrules, no model;HIGH, MEDIUM or blank Fill · comparefills blanks only;differences flagged Odometer photos (Travel) Photoat trip startand end GeminiVertex AI, Mumbai,temperature 0 JSON answerodometer? reading,confidence, reason Pre-fill · flagofficer confirms;differences flagged A persondecides approves, pays,sets the distance
Figure 1. Two pipelines, one decision-maker. Bills never meet a language model; odometer photos do, because the task is visual. Both end with a person.

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:

FieldReadConfidenceWhy
Taxable value₹4,200HIGHprinted, and the taxes and total agree with it
GST₹756HIGHexactly 18% of the printed taxable value
Total₹4,956HIGHtaxable value plus taxes add up to it
Invoice date(blank)nonetwo 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

Written by

Nikhil Kumar

AI/ML Engineer

Digital Transformation and Alliances Department, Ritwik Finance Enterprises Private Limited