
Building a farm to city food router on AWS with SAM, Lambda and Amazon Location
India loses about 19 million tonnes of fruit and vegetables a year, much of it because the price crashes before the food spoils. Here’s how I built AnnaSetu on AWS to keep produce out of landfill: send each load where it still sells, rescue what’s left unsold at a city mandi, and turn the rest into feed, biogas or compost.
In September 2023, Kolar's tomato market took in 4.21 lakh quintals against 2.31 lakh the year before, and officials blamed the crash on "the arrival of a large quantity of tomatoes" ( Deccan Herald ). In the 2025 glut, harvest and transport cost about Rs 70 per 15 kg box while the sale price fell as low as Rs 30 ( Outlook Business ).
The food was fine. Its price wasn't.
The data that could have prevented that is public: every Indian mandi reports prices and arrivals daily. But nobody turns it into an answer for one truck before it leaves. So in a 72-hour hackathon I built AnnaSetu, a router that answers one question: where should this load go today, and what will it actually earn after costs?
What I wanted
- Advice for a single truckload, by voice, in the farmer's language, in under 15 seconds.
- Net value per kg after freight, fees and spoilage, as a range, never a point.
- Spread a day's loads so the app never causes the glut it is avoiding.
- A path for stock that didn't sell at a city mandi: another mandi, a food bank, then feed, biogas or compost.
- No AWS credentials on the phone, and no data I couldn't cite.
The first constraint shaped everything: the output had to stay a decision, not a dashboard.
The Stack
Layer | What I used |
|---|---|
App | React Native with Expo, Android APK via EAS |
API | Amazon API Gateway (HTTP API) + a Lambda authorizer |
Compute | Three Python Lambdas: advisor, voice, ingest |
Data | DynamoDB (4 tables), S3, SQS with a dead-letter queue, EventBridge Scheduler |
Maps | Amazon Location Service: place search and truck routes |
Voice | Amazon Transcribe in, Amazon Polly out |
Weather | NOAA GHCN-Daily on the AWS Registry of Open Data, NASA POWER, Open-Meteo |
Infra as code | AWS SAM CLI |

Three paths, kept apart on purpose: recommendations run synchronously through API Gateway and the advisor Lambda; voice uploads to S3 through a presigned URL and is parsed by its own function; ingest runs nightly, fanned out over SQS. A failed ingest never blocks a farmer's advice, and a slow transcription never sits inside a normal request.
Step 1: get the data, honestly
My first surprise was infrastructure, not algorithms: agmarknet.gov.in and the data.gov.in API refuse connections from cloud IPs. So I used the same AGMARKNET data as bulk files from the India Data Portal (about 400 commodities, 4,142 markets, 2021 to May 2026), split into one gzipped CSV per crop in S3.
For today's prices, Agmarknet's newer site answers from ap-south-1 but rate-limits after about 60 calls, so the advisor caches each market's prices for the day in DynamoDB. History became a predictable S3 workload; the fragile external call shrank to a small, cached part of Live mode.
Step 2: keep the model boring
No machine learning. Every rule is a few lines of Python in
backend/core/ , with no AWS imports, so it runs the same in a test and in Lambda. Spoilage comes from shelf life adjusted for temperature (Q10) and the hours since harvest, on the road and at the mandi:Python
def spoilage_share(hours, temp_c, crop):def s(sl_ref):return min(1.0, crop["alpha"] * hours / shelf_life_hours(temp_c, sl_ref, crop["q10"], crop["t_ref_c"]))short, long_ = crop["sl_ref_hours_range"]return {"low": s(long_), "mid": s(crop["sl_ref_hours"]), "high": s(short)}
The net price per kg would align with the assumption that the price is negative with the assumption of negative spoilage. I believed that it was the sensible thing to do to go with the forecast. But the backtest proved me wrong since the prices were not driven by supply for most of the crashes. Therefore, AnnaSetu doesn't serve as an early warning but rather as proof that you shouldn't ship to glut.
Step 3: anti-herding
Since all parties get the same instructions, the ideal market in this case is the glut market. Load assignments are done according to decreasing load sizes, and the tonnage of each load is added to the estimated load of the market in question. In case the market is overloaded, then the market is not selected. The algorithm is not an optimizer, but greedy by design.
Step 4: routes and places with Amazon Location
They are a village and not a coordinate. The advisor is going to evaluate the markets and the districts based on what they know, and then go back to an Amazon Location places search, storing the drive time of that route. Spoilage and freight costs are dependent on distance; hence, a wrong route translates into a wrong profit figure. No route, no assumptions, but just a clear 422 API.
Step 5: one SAM template
Everything lives in
infra/template.yaml : three functions, four tables, two buckets, the queue, the schedule, alarms, and one IAM role per function with only what it needs:YAML
AdvisorFunction:Type: AWS::Serverless::FunctionProperties:Handler: backend/handlers/advisor.handlerMemorySize: 512Timeout: 29 # under the HTTP API 30 s limit
sam build , sam local start-api , then sam deploy with a reviewed change set. Ingest fans out one SQS message per crop, so each crop retries on its own and failures land in a dead-letter queue with a CloudWatch alarm.Step 6: voice and the phone
The phone holds no AWS credentials: requests go through API Gateway, and recordings go to S3 through a presigned URL, deleted after a day. Transcribe covers 12 Indian languages; Polly replies in Hindi or Indian English. Bedrock was meant to parse speech and explain the advice, but our account was refused model access, so a rule parser and templates run instead. The lesson: AI can improve the interface, but it shouldn't own the decision.

Did it work?
I replayed the deployed engine on every day from 1 March to 31 May 2026, with simulated loads from Kolar and Azadpur running on real prices, weather and routes. Across 1,380 loads, it ran with 0 errors and estimated Rs 2.20 crore saved (likely Rs 1.73–2.96 crore). But since the loads themselves were simulated, I wanted something more concrete to validate the recommendations. So for every load AnnaSetu sent away from its nearest mandi, I checked what actually happened to the prices next. In 1,248 of 1,279 cases, the recommended market really did pay more after freight. It wasn't consistent everywhere though, Delhi cauliflower was the clear weak spot, where it held only 14 out of 35 times.


Waste avoided is smaller on ordinary days, when a longer trip adds spoilage; the gains come on glut days. Sometimes the nearest mandi really is the right answer.
Reflecting on my learnings so far
- Public data isn't usable infrastructure. Split bulk history from cached live data.
- A recommender changes the system it observes. Hence anti-herding.
- Ranges beat points. One confident number implies a precision the data doesn't have.
- Fail gracefully, and say what's simulated. Loads are simulated; prices, weather and routes are real; money and waste are estimates.
Coding agent: Claude Code
Great time building AnnaSetu along with @utkarshdugar.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article