
“Aao” is not a pickup request: building KabadiSetu on AWS in four days
How I built a Hindi + English waste-sorting and kabadiwala pickup app on AWS in four days, and measured the AI honestly.
I built KabadiSetu in four days for WeMakeDevs × AWS Environmental Hacks. It's a Hindi and English web app that helps a household sort its waste under India's new rules, then send a real pickup request to the local kabadiwala. The AI names the item. Code decides where it goes. People decide what happens.
Demo video (2:56): https://youtu.be/f0JD8LPav94 · Live app: https://main.d364mryjpwg6jn.amplifyapp.com · Code: https://github.com/Anish033-coder/kabadi-setu
One word, no information
In most Jaipur homes, the scrap leaves the house after a single WhatsApp message to the kabadiwala: aao. Come.
That one word carries no items, no quantity and no address detail. He has to guess whether it's two newspapers or a cupboard of old cartons. And whether the trip across the colony is worth his morning. A few weeks before Diwali, when every home is clearing out at once, that guess happens a lot.

Caption: The whole pickup request, as most kabadiwalas receive it.
There's a second problem behind the first. Since 1 April 2026, India's Solid Waste Management Rules require every household to separate waste into four streams: wet, dry, sanitary and special care. I tried it at home and didn't know where half of it went. Is a chips packet dry waste? Where does a dead phone charger go? What about a cracked bulb?
KabadiSetu tries to fix both: sort an item correctly, then replace aao with a message that actually says something.
From a photo to a pickup
A household identifies an item by photo or by search, confirms it, and answers what it can see: is it clean and dry? Versioned rules give the stream and the reason. A battery goes to special care and can never enter the scrap basket. Sellable items build a basket, and WhatsApp opens with a clear message listing what's there and how much, plus a private link. The household decides whether to send it.
The kabadiwala opens that link on his phone: a Hindi page, no app, no account. He accepts, and later reports the pickup. Both sides see the same history. Items a household has sorted before become one-tap shortcuts that never call the AI at all.

Caption: Recorded on my phone: sorting newspapers, an item that can't join the basket, the photo short list, and the kabadiwala's Hindi page.
The rule I kept coming back to was simple: the AI names the item, code decides where it goes, and people decide what happens. The model has no tools. It can't add anything to a basket, choose a stream or touch a pickup.
Day 1: a pickup is a state machine, not a message
The first thing I built wasn't the AI. It was the pickup itself, because that's where things break quietly.
A request moves through awaiting response → accepted → collection reported, with side exits for rejected, cancelled and expired. Two very different people change it: a signed-in household, and a kabadiwala who only has a link. So the first question was who is allowed to try what.
Who may try is not the same as what's valid right now. I split those apart. Cedar, AWS's open-source policy language, answers only the first question:
Plain text
permit(principal is Household, action in [Action::"view", Action::"cancel", Action::"collect"], resource is Request)when { resource.owner == principal };permit(principal is PickupLink, action in [Action::"view", Action::"accept", Action::"reject", Action::"collect"], resource is Request)when { principal.requestId == resource.requestId };
A household can cancel its own request; only the collector's link can accept it. Whether the request is still in a state where that action makes sense is a separate check in code.
Every tap might arrive twice. On a patchy connection a kabadiwala might tap Accept, see nothing happen, and tap again. At that same moment, the household taps Cancel. Each action carries the version it expects and a one-time receipt, and both are written in a single DynamoDB transaction:
Python
self.write([{'Put': {'TableName': self.table, 'Item': self.item('REQUEST#' + doc['id'], doc),'ConditionExpression': '#v = :expected','ExpressionAttributeNames': {'#v': 'version'},'ExpressionAttributeValues': {':expected': {'N': str(version)}}}},{'Put': {'TableName': self.table, 'Item': self.item(receipt_key, receipt),'ConditionExpression': 'attribute_not_exists(pk)'}}])
A repeated tap gets the original answer back. A conflicting one fails cleanly. One wins, never both. The tests run exactly these races against DynamoDB Local.
The 48-hour bug. My first version gave every request 48 hours to live. That sounds fine until a kabadiwala accepts on Monday for a Wednesday pickup. By Wednesday the request had expired. The pickup could never be marked collected. None of my first tests waited longer than 48 hours, so none of them noticed. Now acceptance opens its own seven-day pickup window, and there's a test that waits past the 48 hours.
Day 2: Bedrock said no
My plan for recognising items was Amazon Nova Lite on Bedrock. Every call failed with
ValidationException: Operation not allowed , in Mumbai and in us-east-1. Every model I checked reported NOT_AUTHORIZED , even with an administrator role and no restrictive policies. CloudTrail showed the exact error, and it matched a re:Post thread about India-billed (AISPL) accounts. I opened a support case. As I write this, it's still blocked.What saved the day was a decision I'd made earlier for a different reason: recognition sits behind one small
Recognizer interface. Switching to Amazon Rekognition was one setting. Nothing else in the app changed, and the Bedrock adapter is still there, tested, one setting away.
Caption: Everything runs in ap-south-1 (Mumbai), defined in one AWS SAM template. Bedrock is the dashed box.
Day 3: recognition is a suggestion, not a decision
Before testing anything, I froze a small benchmark: 30 MIT-licensed public photos from TrashNet, 20 for development and 10 held out. On the held-out set, Rekognition named only 2 of the 7 items it should have known. Then I tried it on things lying around my own home, and it struggled there too.
So I stopped asking what it got wrong and looked at what it actually said. The labels were right in kind, just not specific. A notebook came back as "Book, Text, Document". A newspaper was "Advertisement, Poster". My steel water bottle was "Aluminium, Bottle". And an old axe head was, with total confidence, a "Grenade".
That changed the design. Instead of waiting for an exact name, code now groups labels into families. Each family becomes a short list of up to four catalogue items. The person picks one, or taps "None of these":
Python
FAMILIES = (('paper', {'paper', 'text', 'document', 'book', 'publication', 'poster', ...},('newspaper', 'notebooks', 'office-paper', 'cardboard')),('bottle', {'bottle', 'beer bottle', 'water bottle', ...}, ('pet-bottle', 'glass-bottle')),('metal', {'aluminium', 'metal', 'steel', 'cookware', ...}, ('steel-utensil', 'aluminium-can')),...)MAX_CHOICES = 4
The AI still never decides. It narrows the choice, and a person finishes it.
Day 3, evening: measuring it honestly
A short list designed on the photos you test it on proves nothing. So I built it only on development photos, froze it, and then took 30 fresh public photos nobody had used. I wrote down the correct label for every one of them, committed those labels to Git, and only then ran the test, once.
Test | Photos | Result |
|---|---|---|
Held-out public set, exact names only | 10 (7 catalogue items) | Named 2 of 7 |
Fresh public set, short list frozen before the run | 30 (19 catalogue items) | Right item offered for 12 of 19; named 3 outright, none wrong |
It's not a great number, and I didn't try to make it look like one. The misses are instructive. Three were snack wrappers, which the short list never offers. A plastic milk jug and a food tub were offered as bottles. There's an honest catch, too. Nine of the 11 items the app doesn't support still got a list. That's why "None of these" is always on screen. Manual choice and the one-tap shortcuts are still the main path. The photo now saves a search instead of ending in "unknown".
Day 4: a video made by code
For the demo video I wanted every AWS service to light up at the exact moment it was mentioned. Amazon Polly reads the script in an Indian English neural voice, and says the Hindi words natively. Polly's speech marks give the timestamp of every single word. The animations are timed to those marks, so Cognito appears when the voice says "Cognito". The phone footage is my own, with the kabadiwala's number and name blurred before anything else touched it.
Eleven AWS services, and what one photo costs
- AWS Amplify hosts the React app; Amazon Cognito handles household sign-in.
- Amazon API Gateway (HTTP API) checks the Cognito token before any code runs.
- One AWS Lambda function in Python 3.14 on arm64 (Graviton) runs the rules, Cedar checks and the pickup lifecycle.
- Amazon DynamoDB holds baskets, requests and daily quotas, with conditional transactions.
- Amazon S3 takes photos through a size-limited upload straight from the phone; a lifecycle rule deletes them after a day.
- Amazon Rekognition makes exactly one call per photo, and only after a daily quota has been reserved, so a bug can't run up a big bill.
- AWS Systems Manager Parameter Store holds the secret that signs collector links.
- Amazon CloudWatch keeps logs for 14 days; AWS Budgets alerts at $1, $5 and $10.
- AWS SAM defines all of it in one template.

Caption: Not a mock-up: the stack as CloudFormation reports it.
I measured one complete photo flow on the live stack: five API calls, one image request and a few DynamoDB writes. At Mumbai list prices that's about $0.00129, or roughly 13 US cents per 100 scans. 97% of it is the Rekognition call, so choosing an item by hand, or from the shortcuts, costs almost nothing. That's why the app only reaches for AI when it actually helps. The whole thing is covered by 116 automated tests that run on every push.
Built for two kinds of people
Households in Indian cities trying to follow the 2026 rules without memorising them. And small kabadiwalas who already run their business on WhatsApp, and won't install another app. And, I hope, developers looking for a pattern where AI is useful but never in charge.
Three rules I'm keeping
Put the AI behind an interface before you need to. I didn't know Bedrock would be blocked. The interface turned a dead end into a one-line change.
Write the labels down before you look at the results. I committed the correct answers first. That's the only reason I trust the 12 of 19, disappointing as it is.
Assume every tap arrives twice. Phones on slow networks retry. If a double tap can corrupt your data, it eventually will.
What's still unproven
No kabadiwala has used it for a real pickup or confirmed rates yet. So the app asks for a quote instead of promising a price. The pickups in my demo are tests, and a "collected" report isn't proof of recycling. My accuracy numbers come from public photos, not from Jaipur homes. Next: a pilot with one real collector, and the Bedrock comparison once access comes through.
See it for yourself
- Open https://main.d364mryjpwg6jn.amplifyapp.com and create an account with any email.
- Tap Old notebooks, answer "clean and dry?", and add it to the basket.
- Tap Old electronics to see an item that can never join the scrap basket.
- Prepare a pickup request and open the recipient test page to see the kabadiwala's Hindi view.
The code, the SAM template, the tests and the full evaluation are on GitHub: https://github.com/Anish033-coder/kabadi-setu
A question for you: how would you make a kabadiwala's "collected" report trustworthy, without asking him to install an app or log in?
Built solo for WeMakeDevs × AWS Environmental Hacks (Waste and Energy track). I used Codex and Claude while building.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article