Ten Seconds on WhatsApp: Turning a Hindi Voice Note Into a Tamper-Evident Custody Record
How ChainTrack captures the first handover of a dead e-rickshaw battery using WhatsApp, Transcribe and Bedrock, then stores it in a KMS-signed hash chain with nightly Object Lock digests. No webhook server, no QLDB.
The user is a battery dealer with wet hands
The most important person in our system never creates an account. A battery dealer in Delhi buys four dead e-rickshaw batteries from a driver for cash, exactly as dealers do today. Then, instead of filling a form, the dealer sends one voice note on WhatsApp:
"chaar e-rickshaw battery, lagbhag 80 kilo, driver se li"
That is the entire data entry. ChainTrack's job is to turn those ten seconds into a trustworthy, tamper-evident record. Here is how we did it on AWS without running a single server.
Step 1: Receive the message without a webhook server
AWS End User Messaging Social receives the WhatsApp message and publishes it to an SNS topic. An ingest Lambda subscribes. That removes the usual chore of hosting and securing a public webhook endpoint.
WhatsApp can deliver the same message twice. We use Powertools for AWS Lambda idempotency so a repeated delivery produces exactly one ledger event, not two.
Step 2: Voice to structured data
The ingest Lambda downloads the voice file into S3 and starts an Amazon Transcribe job that handles Hindi, English and Hinglish. The transcript goes to an extract Lambda that calls Amazon Bedrock with Amazon Nova Lite and a strict JSON schema:
-
material
-
count
-
weight_kg
-
source_category
A small, cheap model is the right choice here. The task is narrow, and the output is checked against a schema rather than trusted.
Two privacy decisions matter. Raw audio is deleted after 24 hours and only the transcript is kept. Transcripts are passed to the model as quoted data, never as instructions, which blunts prompt injection.
Step 3: Read the numbers back
The system replies with a card showing the extracted numbers and two buttons, confirm and edit. We never silently accept a model's reading of a number. Weight also stays marked as "stated" unless someone actually weighed the batch, because a spoken "about 80 kilo" is not a scale reading.
When the dealer confirms, those fields are tagged
BUYER_REPORTED . Only the buyer said it, and the record says so.Step 4: The driver gets to say "yes, I sold this"
The dealer receives a receipt link and a QR code. The QR holds only
https://<site>/r/<receipt_token> , an opaque token and never data. The driver can open the link on their own phone and confirm. If WhatsApp is the channel, a wa.me link sends a short pre-filled confirmation message from the driver's own phone.Confirmation strength depends on the channel, and we label it honestly:
Channel | Weight | Note |
|---|---|---|
Driver's own phone (WhatsApp, SMS or link) | High | A different device from the buyer. Still not proof of identity. |
The buyer's own phone | Low | Labelled SAME_DEVICE , never counted as witnessed |
No confirmation | None | Stays BUYER_REPORTED |
Status is derived, never stored. An event is
BOTH_PARTIES_CONFIRMED only when every field the handler was asked about is confirmed on their own device.Handlers get no name, Aadhaar, GSTIN or address in our system. Phone numbers are stored only as a keyed HMAC, so deleting the key erases the link (crypto-shredding).
Step 5: The ledger
Each event becomes one DynamoDB item. The trick is small and old:
Python
import hashlibimport jsondef event_hash(prev_hash: str, event: dict) -> str:canonical = json.dumps(event, sort_keys=True, separators=(",", ":"))return hashlib.sha256((prev_hash + canonical).encode()).hexdigest()
Every event stores its hash and a KMS signature (ECDSA P-256). Because each hash includes the previous one, altering any old event breaks every later hash. Our
verify_chain function reports the exact event number where the break happens.A hash chain alone is not enough, since an operator could rewrite the whole chain. So each night we compute a Merkle root of the day's events and write it to S3 with Object Lock in compliance mode, which nobody can delete early, including the root user. The root is also published on a public page. Even the operators cannot silently rewrite history.
Corrections never overwrite anything. A correction is a new
CORRECTION event that points at the old one, and handler confirmations arrive the same way and are applied when history is read.This design replaces what many teams would reach for first, Amazon QLDB, which AWS has retired.
Step 6: Only the handler can speak for the handler
A buyer must not be able to fake a handler's confirmation, even by calling the API directly. We encode that as a Cedar policy in Amazon Verified Permissions: only the handler, or a handler token, may write
confirmed_by: HANDLER . We tested the policies locally with the Cedar engine before touching AWS, and we test that the Cedar policy, our Python mirror of it and the Verified Permissions adapter all agree on every case.Offline first
Connectivity is not guaranteed at a scrap yard. The React PWA on Amplify queues events in IndexedDB and syncs later, with an idempotency key on every event so reconnecting never duplicates one. The in-app camera hashes each photo on the device and gets a server receipt time on upload, which fixes back-dated photos for new evidence.
If WhatsApp linking stalls, a WhatsApp-style chat inside the web app runs the same pipeline.
What this does not prove
This is the part we most want builders to take seriously. A hash chain proves a record was not changed. It does not prove the record was true. Someone with many SIM cards could fake confirmations. We raise the cost with channel labels, device clustering and Cedar rules, and we keep evidence quality as a separate score. We never call it proof.
Test it like a user, not like an engineer
Our acceptance test is not a unit test. It is at least three people outside the team, some not at ease in English, recording "3 old batteries bought" with no instructions. The target is 100 percent completion with a median under 60 seconds. We have designed this test but have not yet run it with real dealers, and we say so.
The ledger tests are more mechanical: edit one stored event and
verify_chain must fail at that number, delete an event and a gap must be found, and deleting a locked digest must be denied.Takeaways
- Use managed messaging so you do not run a webhook server.
- Use the model for extraction, then make a human confirm the numbers.
- Hash chain plus KMS signatures plus Object Lock plus a public digest is a pragmatic tamper-evidence stack.
- Put authorization in policy, not in the UI.
- Label how strong each confirmation is, and never call one-sided data witnessed.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article