
How we built VACHAN: reading 1,027 government PDFs without trusting the LLM
An AWS pipeline that finds the wet-waste promises Pune's builders made in writing, and checks every one against its page.
How we built VACHAN: reading 1,027 government PDFs without trusting the LLM
An AWS pipeline that finds the wet-waste promises Pune's builders made in writing, and checks every one against its page.
I live in Hadapsar, in Pune, right next to a waste plant.
Every day, Pune's housing societies and malls send about 300 tonnes of wet waste to plants like the one near my home. Next door, Pimpri-Chinchwad makes about 1,500 tonnes of waste a day and processes about 700. In July, a hill of unprocessed legacy waste at the Moshi depot became unstable after heavy rain and collapsed. Nine workers died.
In June, Pimpri-Chinchwad told large housing societies to process their own wet waste at source, or pay ₹5,000, then ₹15,000, then face a case.
The fine lands on residents. But the plant was never the residents' idea. It was the builder's promise, in writing.
That is the whole project. This post is how we found those promises, how we got AWS to read them without making things up, and what I would do the same way again.
Live app (no login): https://main.d36c2lel7ok0qn.amplifyapp.com
Demo video (2:59): https://youtu.be/\_HbZJ4fCaak
Code: https://github.com/SidTheKid-dotcom/wemakedevs-env-hacks-2026
The promise is already public
To get environmental clearance, every large housing project in Maharashtra goes to the State Expert Appraisal Committee (SEAC). The builder states how much wet waste the project will make once people move in, and how it will process it on site: a composting unit, an organic waste converter, a biogas plant. The committee minutes record it.
Those minutes are public on PARIVESH, the government portal. There are more than a thousand of them for our area. They are long scanned-or-typed PDFs, and nobody reads them.
So when a society gets a notice, nobody can answer the one question that matters: what did our builder promise?
VACHAN answers it. You type your area. You see every written promise within 2 km. You tap one and get this:

1,056 kg of wet waste a day, half a kilometre from Seasons Mall, promised in writing to be treated in an organic waste converter. One tap opens page 3 of the government PDF with the exact line highlighted. One more tap drafts a polite letter, in English, Marathi or Hindi, that quotes the builder's own promise and asks for the plant to run. Ward officers get the same data as a check-first list: the biggest promises in their ward, each with its builder and page, so they can go to the builder first, before a society is fined. Across Pune and Pimpri-Chinchwad, that adds up to 735.6 tonnes of wet waste a day, promised in writing, from 783 projects the committee recommended for clearance.
The architecture
Everything runs in
ap-south-1 (Mumbai) and is defined in one AWS CDK app.
The whole pipeline, one CDK stack.
Plain text
EventBridge (02:00 IST, nightly)└─ Step Functions├─ Lambda: crawl PARIVESH ──► S3 (every PDF, named by its SHA-256)├─ Distributed Map: one Lambda per PDF│ ├─ Poppler reads the text layer│ ├─ Textract reads scanned pages│ ├─ Bedrock (Nova Pro) reads each project into a strict JSON schema│ └─ plain-code checks ──► DynamoDB├─ Lambda: publish static JSON ──► S3 + CloudFront└─ Lambda: proof step, re-downloads every cited PDF and finds each quoteAmplify hosts the website · Amazon Location finds addressesLetter Lambda: Bedrock + Guardrail, capped per day
A few choices worth explaining:
- S3 keys are SHA-256 hashes of the PDF. If PARIVESH re-uploads the same file under a new name, we never read it twice. It also gives us a citation copy: PARIVESH has broken TLS and outages, so CloudFront serves our mirror and anyone can open the exact page.
- One container image, five handlers. Crawl, process, publish, prove and letter all share one Lambda image with Poppler installed. One build, one thing to debug.
- Distributed Map, not a queue. Retries and a 10% failure budget come built in, and the run graph is the clearest picture of the pipeline I could show anyone. The first full run read 1,024 PDFs in 43 minutes. 15 failed inside the budget, and the run finished.
The model reads. Plain code decides.
The hard part is one line.
Here is what a promise looks like in the minutes:
Plain text
Wet waste: 1056 kg/day Treated in OWC
The promise, on the government's own PDF.
Easy, right? A regex should do it.
It doesn't. Every project lists wet waste twice: once for the construction phase (a few labourers on site) and once for the operation phase (after hundreds of families move in). Only the second one is the promise. The tables are inconsistent, the headings move, and the phase label can sit rows away from the number. A regex cannot reliably tell which figure belongs to which phase, and picking the construction one shrinks the promise to almost nothing.
Telling the two apart needs reading. So Amazon Bedrock (Amazon Nova Pro) reads one project at a time and returns a strict JSON schema:
JSON
{ "wet_kg_day": 1056, "plant": "OWC", "phase": "operation", "page": 3,"quote": "Wet waste: 1056 kg/day Treated in OWC" }

But we never let the model decide. That was the rule we didn't break.
After Bedrock returns a record, deterministic code checks it:
- the figure must appear word for word on a "Wet waste" line in the PDF;
- it must be the operation-phase figure, never the construction one;
- wet waste per flat must be 0.5 to 3 kg a day;
- the stated plant capacity must cover the promised waste;
- the committee decision must be readable.
Code, not the model, picks the quoted line and the page. If any check fails, the record goes to review and the card says why.
On a 25-item hand-checked sample, Nova Pro matched our plain-code figure on 23 of 24 items. On a 25-item hand-checked sample, Nova Pro matched our plain-code figure on 23 of 24 items. That is a strong result. For a public page with a builder's name on it, we wanted every number confirmed, and the checks are what make each one publishable.
Then prove it again
A model reading a PDF once is a claim. We wanted proof.
After every run, a separate step downloads every cited PDF again from the government site, hashes it, and looks for each published quote on its page. The latest run: 829 checked, 829 matched. It logs the result to CloudWatch, and CI checks the published numbers on every push.

Proof, not trust: 829 checked, 829 verified.
If a number is on the screen, it traces to a page you can open.
Showing doubt is a feature
385 records in VACHAN say "A person must check this." Each one shows the reason, word for word. My favourite: the model read 12,50,000 kg a day for one project. Plain code could not find that figure in the minutes, so the card shows it in pencil with a stamp and offers no letter. No letter ever quotes an unchecked number.

385 records wait for a person, each with its reason.
I expected those 385 to feel embarrassing. They turned out to be a feature. A page that admits when it is not sure gives you a reason to believe the cards that it is sure about.
The same thinking went into the letters. A public tool that writes to builders must never accuse anyone. The data shows what was promised in writing, not whether a plant runs today. So the letter Lambda runs Bedrock through an Amazon Bedrock Guardrail that blocks 17 accusing words and phrases. We tested it with a draft saying the builder "violated the law and is guilty of fraud". The Guardrail returned GUARDRAIL_INTERVENED , and nothing went out.
One setup tip: apply the Guardrail to the model's output. Our letter prompt carries long instructions, and checking the output is what keeps every sent letter safe while the drafting stays fluent.
Measure before you claim
Measuring the full corpus changed two of our early estimates, and made the final numbers ones we can stand behind.
1. "40% of the minutes are scans." That came from a first sample of 25 PDFs. The full corpus had 12 scans in 1,027. So Poppler reads the text layer for almost everything, and Textract handles the scans as a focused side path.
2. "Over 350 tonnes a day for Pimpri-Chinchwad alone." Our first total. Once we counted each site once and kept only projects recommended for clearance, it settled at 313.7 t for PCMC and 421.8 t for PMC.
Measure the whole thing before you claim anything about it.
What it cost
About $10 of AWS for the whole event, almost all of it Bedrock.
Because the app is public and has no login, we also set a hard stop: AWS Budgets alerts at $30, $48 and $60, and at $50 a budget action attaches a deny policy that stops Bedrock, Textract and new runs. The letter Lambda is capped per day and runs two at a time. A sign-in-free demo should never be able to run up a bill. The design input that mattered more than money was the Nova Pro quota of 50 requests a minute. Step Functions made that easy: MaxConcurrency of 4 on the Distributed Map, with retries and backoff built in.
What I'd tell you if you are building something like this
- Let the model read. Let code decide. Use the LLM for the part that needs judgment (which of two numbers is the promise), then verify everything it says against the source with plain, boring, testable code.
- Make every number traceable. Store the source, hash it, cite the page, and re-check the citation after the run. "Trust me" is not a feature.
- Ship the doubt. A review queue with honest reasons builds more trust than a clean page that is sometimes wrong.
- Guard the output, not your own prompt. And test the guardrail with the exact sentence you are afraid of.
- Measure before you claim. A full count beats a sample, and a deduplicated total is the one to publish, until you deduplicate them.
- Data only helps when someone can act on it. A total of 735 tonnes changes nothing on its own. One building's promise, its page and a ready letter can.
What's next
We want to sit with a PCMC or PMC ward officer to test the check-first order on real inspections, and record what each visit finds next to the promise. We also want to hand-label about 40 records and publish a field-by-field accuracy figure. The crawler already reads every Maharashtra SEAC minutes PDF, so another city needs only its ward map.
Built by Team VACHAN, Pune, for WeMakeDevs x AWS Environmental Hacks 2026: Siddhant Vishnu (the build), Dushyant Singh (problem and design), Shreya Bableshwar (product and outcome).
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article