Case Study — Procurement Analytics
An AI assistant that can read a client's financial data, and is structurally incapable of ever writing to it.
Every large company already owns the data to catch a duplicate payment, an off-contract purchase, or a segregation-of-duties breach. Almost none of them can see it in time. The records are spread across six or seven ERP transactions, each one owned by a different team, and by the time an analyst has exported them and matched them in Excel, the invoice is paid and the finding belongs to the auditors.
Our client lived on the other side of that problem. They advise enterprises on procurement, and every engagement started the same way — pull the raw data, wrangle it in spreadsheets, hand over a static deck. They wanted to turn that into a product: upload a client's procurement extract, and get the whole governance picture back in minutes, with an AI assistant that could answer follow-up questions and generate the deliverables. We built two things that had to work as one — the analytics platform, and an AI assistant that could read the data but, this was the hard constraint, could never write to it.
Procure-to-pay is a chain: requisition → purchase order → goods receipt → invoice → payment. In a healthy process those five documents line up. The interesting stuff is where they don't.
A purchase order with no requisition behind it — someone bought outside the contracted process, usually at a worse price.
The same user who posted an invoice also releasing its payment. The authorization matrix is supposed to prevent it; in practice it leaks, and nobody computes the leak.
An invoice that doesn't reconcile against both the PO and the goods receipt — the classic route to overpayment.
An MSME supplier invoice paid past the statutory window accrues penalties under the MSMED Act. That's not a preference, it's law, and finance teams routinely miss the clock.
None of these live in a single report. That's the whole reason the product needed to exist, and why "just build some dashboards" was never going to be enough.
The core loop is deliberately simple to say and was not simple to build: one upload, every metric recomputed. A user drops in the procurement extracts — requisitions, orders, goods receipts, invoices, payments, vendor master, change logs. The system detects what each file is from its column signature rather than asking the user to label anything, validates and de-duplicates it, classifies each line, and recomputes every KPI across the platform. On a client's dataset that runs in well under two minutes.
On top of that sit role-specific views — procurement gets order activity and contract compliance, finance gets invoice aging and duplicate detection, leadership gets the risk panel at a glance, vendor management gets delivery performance and spend concentration. Each view is scoped by company, because the client onboards several end-customers into the same instance and their data must never bleed together.

Profit centers map to departments, plants and material groups, each carrying its own CAPEX/OPEX classification and budget. That mapping is what lets the platform answer "are we over budget" at the level a finance lead actually thinks in, rather than as one company-wide number that hides which department is the problem.

ERP extracts are not clean, and they are not shaped the way you'd design them. Columns are named for the source system, not for humans — a boolean like "is this maverick" comes through as an integer, not a true/false. We shipped a handful of bugs early precisely because we trusted the obvious column name instead of the actual one, and a filter that excluded deleted rows silently dropped every row where the deletion flag was simply null — which was most of them. That class of bug doesn't throw an error. The dashboard just quietly shows the wrong number, which is worse.
The fix wasn't clever, it was disciplined: a mapping and validation layer that every extract passes through before it becomes a metric, with the real column semantics pinned down once and asserted, so a surprising file fails loudly at the door instead of producing a confident, wrong KPI three screens later.
The client wanted more than dashboards — plain-language questions, grounded answers, real files back. We built an in-app assistant as a proper agentic harness: it plans, picks from a set of tools, runs read queries, analyzes results, draws charts, and assembles reports. The entire design bent around one non-negotiable rule.
The assistant can read the database. It can never create, update, or delete anything in it.
We didn't rely on the model behaving. We made writing structurally impossible, in five independent layers, on the assumption that any one of them might one day fail.
The assistant connects through a dedicated Postgres role granted SELECT and nothing else. No INSERT, UPDATE, DELETE, or DDL privilege to invoke.
Every connection opens with the transaction set to read-only, so the server aborts a write before it touches anything — independent of the role grant.
Rejects any statement carrying write verbs before it's ever sent to the database.
The model can only call the tools we registered, and not one of them mutates data. There is no capability to reach for.
Requires every generated query to be a single read — one SELECT or CTE — and rejects anything else, with a cost ceiling to stop a runaway scan.
We verified this the only way that counts: with the dedicated role, an INSERT issued directly on the assistant's own connection is thrown out by the database, no application code involved. Belt, braces, and three more belts. On something touching money, that redundancy isn't paranoia — it's the job.
Read-only keeps the data safe. It does nothing about the other failure mode that actually sinks trust in an AI analyst: making numbers up. Our stance from the start was that the model never authors a number. It plans, chooses tools, and writes the prose around results — but every figure comes from deterministic Python running against real query output. An output check scans the model's final wording, pulls out every numeric claim, and verifies each against the stored results.
That caught the numbers. It did not catch a subtler hallucination: a categorical claim that wasn't in the data — describing an invoice's status as something the query never returned. The number check had nothing to grab onto and waved it through. The fix extends grounding from "every number traces to a result" to "every stated attribute traces to a column we actually fetched." Worth naming as a limitation of the naive approach: numeric grounding is necessary and nowhere near sufficient.
The client wasn't going to pay top-tier API rates for "what were last month's payments." Because the model only plans and narrates while Python does everything numeric, a weaker model barely hurts accuracy. Cheap models handle planning and narration; a stronger one is held in reserve for the rare multi-step task and escalated to only on failure. Getting there meant losing a few rounds to the models themselves — the war stories are the useful part.
Generating a deck was a four-step conversation, and a small model would burn its tool-call budget two steps in and stall. We collapsed the whole thing into a single tool call that assembles the deck in Python — one decision from the model, a finished file out.
Dividing rupees to crores inline, and getting it wrong. Rather than trust it to do arithmetic, we pre-computed the converted value and handed it over to be copied verbatim.
The key it needed to reference a previous result wasn't visible in what we fed back to it. Prefixing each result with its own reference key fixed the chaining outright.
A rate-limited free model hung a test run for the better part of an hour before we caught it. It now caps retries and escalates to the next model instead of waiting forever.
A grounded answer on screen is good; a board-ready file is what gets forwarded. The assistant generates PowerPoint decks, Excel workbooks, and PDF reports on request, styled to a consistent brand template. The layout is deterministic code — the model supplies titles and narrative, never the figures — so a small model can produce a polished, correct deliverable it could never have laid out itself.
There's a diagnostic lesson buried in this feature. The first version of the report generator worked flawlessly in isolation and failed constantly in the running product. The instinct is to debug the generator — but the generator was fine, producing a valid deck every time we called it directly. The failure was upstream, in the model exhausting its tool budget before it reached the finalize step. Default move now: when an AI feature misbehaves, isolate the part that's just code from the part that's the model, and prove which one is actually broken before touching either.
Each stage of the procure-to-pay chain is coloured by health, so a bottleneck between goods receipt and invoicing is visible before it becomes a payment problem.

Every number the assistant states is pulled from a real query result and checked before it's shown — the chart is rendered by code, not described by the model. Ask for a deck, an Excel workbook, or a PDF, and it returns a real, downloadable file, persisted so it's still there when the conversation is reopened later.

We're glad we treated read-only as an architectural invariant, not a runtime check. It made a whole category of “what if the model does something awful” conversations short, because the answer was “the database won't let it,” and we could show that on demand.
We'd start the anti-hallucination work broader. Building the numeric verifier first was right, but it quietly encouraged us to think of grounding as a numbers problem, and the categorical gap sat there until we went looking. Grounding is a claims problem — every assertion, not just every figure.
We under-scoped the data-cleaning layer at the start, like almost everyone does. The interesting engineering is the AI; the engineering that determines whether the product is trustworthy is the boring mapping layer that stops a null flag or an oddly named column from silently poisoning a metric.
The deliverable is a working platform where a procurement extract becomes a full governance picture — spend, cycle times, maverick rate, duplicate invoices, SOD conflicts, MSME exposure — in the time it takes to get a coffee, with an AI analyst on top that answers questions and produces the decks and workbooks without ever inventing a figure or being able to touch the underlying records. It runs day to day on low-cost models and steps up to an expensive one only when it has to, which keeps the per-question cost negligible. For an advisory firm whose product is the analysis, that's the difference between selling hours and selling a system.
Book a free discovery call and let's explore how Dev can accelerate your business with AI-first solutions.
🍪 We use cookies
We use cookies to analyze site traffic and improve your experience. No personal data is sold.