Kabadi Khata: a ledger that breaks on tamper, a radar that catches a 41× over-claim
Kabadi Khata turns a scrap collector's scale photo and Hindi voice note, sent via Telegram, into a tamper-proof, hash-chained receipt. A Strands agent checks that the photo and voice agree, and asks one question in Hindi instead of guessing. An auditor console runs a deterministic fraud radar to catch fake recycling (EPR) certificates, and Cedar policies decide who sees what. Built with Strands Agents, Cedar, SAM CLI and OpenSearch.
Part of the Kabadi Khata series, built for Environmental Hacks (WeMakeDevs × AWS Bharat Builds Tour), Waste & Energy track, Build It mode.
In 2024, the NGT took up CPCB's finding of 6 lakh fake recycling (EPR) certificates from four plastic recyclers.
That one line is why this layer exists. A recycling certificate is only as honest as the records under it, and in India's informal scrap trade there usually are no records. Kabadi Khata gives a kabadiwala a receipt for every collection and gives an auditor a way to check the certificates sold on top of that work. This post is the data layer I built: the hash-chained ledger the receipts live in, and the deterministic fraud radar that reads it.
The series:
- The problem, and the product
- Perception: reading a photo and a Hindi voice note
- The agent and the policy layer
- The ledger and the fraud radar (this post)
Everything shown here runs on synthetic demo data, and is labelled that way on every screen.
The ledger: a hash chain, not a database row
Every receipt carries the fingerprint of the one before it, so nobody can quietly change history. Each kabadiwala has their own chain, numbered from 0.
When the agent writes receipt n, the ledger:
- Builds the receipt's canonical JSON: sorted keys, a fixed float format, UTC ISO timestamps. The same receipt always serialises to the same bytes.
- Sets prev_hash to the hash of receipt n − 1, or "GENESIS" for receipt 0.
- Stores hash = sha256(canonical JSON, including prev_hash). The receipt ID is derived from that hash.
Change any field of any past receipt, even one kilo, and its recomputed hash stops matching what the next receipt recorded. Corrections are never edits: a fix is a new receipt, appended to the end of the chain.
The agent can only add to this chain through one tool, create_receipt, and that tool refuses unless the photo and the stated weight agree and Cedar allows the agent to write (Part 3 covers that).
Verify points at the exact break
The verify endpoint recomputes every hash in a kabadiwala's chain and returns the first broken sequence number. You don't have to trust that nobody touched the database; anyone allowed to read the chain can check it, and the answer names the receipt.
In the demo, someone edits receipt 27 in Sunita's chain directly in SQLite, setting its weight to 95 kg. Verification walks the chain: receipts 0 to 26 still check out, 27 is marked Edited, and everything after it is greyed out as unverifiable, because each of those receipts was built on a hash that no longer exists.
The radar uses the same check. Receipts after a break stop counting as verified supply, so a tampered chain can't be used to prop up a recycler's certificates.
SQLite decides, OpenSearch only searches
We had two build days, so the ledger runs on FastAPI + SQLite, one file, no services to start. That was a deliberate decision, recorded as an ADR, not a fallback: the four demo beats had to run end to end on the real stack before anything else got built.
OpenSearch came back later as a read-only search copy. make reindex rebuilds it from SQLite, and the API mirrors new receipts into it when it's up. It powers search and a tonnage chart for the auditor, but nothing in the receipt path or the radar ever reads from it. If Docker isn't running, the copy is simply skipped and the demo still works.
The same rule holds for authorisation: Cedar builds its entities from SQLite, never from the search copy. One source of truth, and everything else is a convenience.
The fraud radar: code decides, the LLM explains
The radar's verdicts come from three deterministic rules in the radar's rules module. No model ever decides whether a recycler is flagged.
Only after a rule fires does a language model get involved, and only to write the explanation, in English and Hindi. Even then, code checks its work: every number in the text must come from the flag's facts, the synthetic-data label must be there, and certain words are banned. If the model's text fails any check, the flag shows a template explanation instead. Then a human auditor reads it and decides: request an inspection, or dismiss.
In our demo world, Recycler C certifies 38 times the PET it could have recycled. That 38× is our own simulation, built to make the radar fire hard, not a CPCB finding. The console shows 41.3×, and that gap is the ledger at work: some of Recycler C's supply came from Sunita's chain after the break at receipt 27, and the radar refuses to count it. A tampered chain makes the fraud look worse, not better.
Not a circular 100%
A radar that catches fraud it planted itself proves nothing, so we graded it differently. Our radar evaluation script builds 20 random three-month worlds. Each has 6 honest recyclers whose certificates drift ±15% month to month, plus one fraudulent recycler each at 1.2×, 1.5×, 3× and 38×. Then it asks one question: what is the smallest over-claim the radar catches every time, with zero false flags on honest recyclers?
The answer is 1.5×, with 0 of 1,440 honest recycler-material-months flagged. The same sweep is how I chose R1's threshold:
Below 1.15, noise starts flagging honest recyclers (17.7% at 1.1). Above 1.2, real fraud starts slipping through. 1.17 sits in the zero-false-flag band and still catches the 1.2× cheat 62% of the time.
Two caveats we report rather than hide. When a chain is tampered, an honest recycler that bought from that kabadiwala can be flagged that month (5 of 30 such cases): the rule working as designed, counted separately. And R2's perfect score says nothing about real look-alike photos, because the sweep's photo hashes are random apart from the planted ring.
Closing the loop for the buyer
The radar has an honest limit: it only sees supply that flows through the system. A real recycler also buys scrap that never got a receipt, which pushes its ratio up for reasons that aren't fraud. Lowering the threshold would hide that, so we surface it instead.
Every certificate carries a verified-backed %:
verified receipt tonnes linked to the recycler ÷ tonnes claimed on the certificate × 100
The provenance endpoint returns it, and the brand's console sorts certificates by it with a badge at 80%. In the demo, 11 of a brand's 12 certificates hold up. Recycler C's PET certificate is 2.4% backed and marked for review: a certificate you don't buy.
The brand also sees where the scrap came from, as tonnes per locality. It never sees who collected it. Ask for the kabadiwalas' names and Cedar refuses (Part 3 covers that policy).
What we learned, and what comes next
Build the spine first. We had two build days. Nothing new started until all four demo beats (receipt or refusal, fraud caught, tamper detected, Cedar denial) ran end to end on the real stack. SQLite over a cluster was part of that call, and it held.
Make the model optional. Every model call has a timeout, a disk cache and a scripted replay mode (MOCK_LLM=1). The agent and the radar can also run on Groq, with several API keys tried in turn when one hits its rate limit. If every model fails, code still finishes the collection and the radar still flags.
Report the number that can embarrass you. "1.5× with zero false flags" is less impressive than "100% detection", and far more useful to an auditor.
Next on the list:
- Look-alike photos. A teammate added an experimental DINOv2 similarity check for re-shot photos of the same load. It only measures today; no threshold is validated until we collect real same-load photo pairs.
- Real supply data. The radar's honest-recycler noise model is synthetic. Real recycler intake, even a month of it, would test the 1.17 threshold properly.
- WhatsApp. WhatsApp in production; Telegram for the prototype. The ledger and radar don't change; only the front door does.
Try it
Everything runs locally on open-source AWS tools, with no AWS account. You need uv, Python 3.12 and make. Clone the Kabadi Khata repo from GitHub and run make demo.
Then open the console in your browser on port 8600, run the radar as the auditor, and verify a kabadiwala's chain. make demo needs no API keys: it starts in scripted-replay mode.
Built with Strands Agents for the agent, Cedar for the policy layer, SAM CLI to package the same API as an arm64 Lambda, OpenSearch as the read-only search copy, FastAPI and SQLite for the ledger, Groq and Gemini for the models, and Sarvam for Hindi speech-to-text.
Thanks to my teammates Vanshika, Aarushi and Sparsh.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article