
The fires moved to 5 pm
Satellite fire counts in Punjab's Sangrur district fell 93% in three years, and the smoke didn't. A walkthrough of clearsky, the WhatsApp-first system we built in four days to book a baler before a farmer reaches for a match.
- aws
- hackathon
- dynamodb
- serverless
- stubble-burning
- ai-agents
- climate
Our own fire data says the problem is nearly solved.
We pulled four seasons of NASA FIRMS satellite fire detections for Sangrur, one of Punjab's worst stubble-burning districts, October and November, 2022 to 2025. The count goes 9,518, then 7,655, then 2,068, then 685. A 93% drop in three years. Stop reading there and you'd write a press release.
Then I looked at the timestamps. Of 19,926 detections, 19,886 were recorded between 11:30 am and 3:30 pm IST. That's 99.8%, and not because farmers only burn at lunch. That's when the satellites fly over.
The farmers noticed too. iFOREST found over 90% of large farm fires in Punjab in 2024–25 happened after 3 pm, against 3% in 2021. Punjab's burnt area in 2025 was still around 20,000 square kilometres, down from 31,447 at the 2022 peak. A 36% drop, not 93%.
The fires didn't stop. They moved to 5 pm, when the satellite isn't looking.
So for the WeMakeDevs × AWS Environmental Hacks (Air track, October 8 to 11), my team, Atherion, didn't build another fire detector. We built clearsky, which tries to remove the reason a farmer reaches for the match. It's live at clearsky.akkki.tech, and this post walks through it: one farmer, one field, from a voice note to a cleared field, and everything that broke on the way.

It's a logistics problem, not an awareness problem
Every farmer in Punjab knows burning is bad. Telling them again doesn't help. Here's what they're up against:
| The constraint | The number |
|---|---|
| Time between paddy harvest and wheat sowing | 20–25 days |
| Crop-residue machines already in Punjab | about 1,48,000 |
| Industrial boiler demand for straw, 2022 → 2025 | 8.8 → 41 lakh tonnes |
| Pellet plants, 2022 → 2025 | 18 → 37 |
The machines exist. The buyers exist and want more every year. What's missing is someone to put the right baler in the right field on the right day, and send the bales to someone who pays. For most small farmers, moving straw costs more than it earns. The match costs nothing.
That gap is what clearsky fills. Four kinds of people use it, and only one has to learn anything new:

- Farmers use WhatsApp. No app, no login. A voice note in Hindi or Punjabi is enough.
- Baler operators get a phone-first dashboard: requests to accept, a day's route, a Done button.
- Buyers (pellet plants, boilers, biogas plants) post tonnes wanted, price, and radius.
- District officers get the Burn Risk Radar.
One rule we set early and kept: WhatsApp is for farmers only. One audience, one job.
Step one: a voice note
Our running example all week: Gurpreet has 8 acres in Bhawanigarh. He sends a voice note:
"Mera 8 acre dhaan 24 tareekh ko katega." (My 8 acres of paddy will be harvested on the 24th.)

Meta's webhook hits API Gateway, then a tiny Lambda that checks the signature, drops duplicates, pushes to SQS and returns 200 fast, because Meta retries if you're slow and voice is slow. A second Lambda sends the audio to Amazon Transcribe (hi-IN) and hands the text to the agent. How slow? We'd allowed Transcribe 60 seconds. The first real Hindi voice note took 61.6. It's 90 now.
Strands Agents — AWS's open-source SDK for tool-using agents. You give the model a system prompt and some Python functions, and it decides which to call.
The agent's job is narrow: collect name, village, acres and harvest date, call the booking tools, and reply in the farmer's own script (Devanagari gets Hindi, Gurmukhi gets Punjabi, Roman Hindi gets Hinglish). Its prompt has one rule I'd put on any agent that talks about money or dates:
Only state dates, payouts and names that a tool returned. Never invent or guess them.
The LLM is swappable (OpenAI, Anthropic, Gemini or Bedrock), which wasn't foresight: Bedrock wasn't usable on our account. The better decision was the fallback, a deterministic rules bot over the same tools, in four languages. It runs with no API key and takes over if the LLM fails. "The AI provider had an outage" isn't an answer for a farmer three days from his sowing deadline.
Step two: the matcher, and a race nobody sees
The agent calls book_pickup. The matcher finds balers in range with capacity before Gurpreet's deadline, clusters nearby fields, and picks the buyer with the best net price: what they pay minus ₹8 per tonne per km of transport. With our demo rates (labelled demo everywhere, never shown as market prices):
8 acres × 2.5 t/acre = 20 t of straw
Pellet plant 40 km away, ₹1,800/t = ₹36,000 gross
Baling 8 acres × ₹600 = −₹4,800
Transport 20 t × 40 km × ₹8 = −₹6,400
Platform 20 t × ₹50 = −₹1,000
─────────
Farmer payout ₹23,800
The interesting part isn't the arithmetic. A baler-day is a shared resource: two farmers in one village message in the same minute, two Lambdas both read "B03 has 12 acres free on the 25th", and both book it. So a booking is one DynamoDB transaction with four conditional writes:
1. BalerDays booked_acres + 8 only if booked_acres <= capacity − 8
2. Bookings new row only if booking_id doesn't exist
3. Fields status = BOOKED only if status is still REGISTERED or HARVESTED
4. Buyers reserved + 20 t only if demand is unchanged and reserved <= demand − 20
All four succeed or none do. DynamoDB says which condition failed, and the matcher reacts to each: capacity gone, try the next baler; field already booked, return that booking; buyer changed, reload and re-choose. One gotcha: conditions can't do arithmetic across two attributes, so you can't write reserved + 20 <= demand. We pin demand to the value we read and compute demand − 20 in Python.
Buyers aren't fixed at booking time either. Midweek, a new Sangrur buyer registered at ₹1,850/t and got nothing, because every open booking was already matched. Now open bookings are re-checked whenever a buyer is approved or changes price. Approving that one moved nine bookings to it. Delivered straw never moves, and the farmer's promised payout never changes.
Step three: never say "confirmed"
Our first version auto-confirmed. Matcher picks a baler, Gurpreet gets a ✅, done.
Then we asked what happens when the baler doesn't show. He never agreed to anything. He's busy, the machine broke, he didn't open the dashboard. Gurpreet waits, the deadline arrives, and he does what he was always going to do. The issue we wrote had one line I keep coming back to: a silent no-show is a burnt field.
So a booking is now an offer. Capacity is reserved at once, but the baler has 120 minutes to accept. A decline or silence releases the slot and offers the field to the next baler. After three, the field goes red on the radar with "no baler accepted", and the farmer is told someone will follow up. He hears 📨 when the offer goes out, and ✅ only when a baler taps Accept.
Here it is on the live system, from a test run with a teammate playing the farmer:

The 5-acre field went to three balers. Two let it expire; the third cleared it. The auto-confirm version would have told that farmer "confirmed" on behalf of a baler who never answered.
Step four: the radar
Not every farmer messages us. The officer's job is to find the ones who didn't, before 5 pm.

Every hour, a scheduled Lambda scores every unbooked field:
score = 100 × (0.45·urgency + 0.25·no_free_baler + 0.30·fire_history)
× (1 if harvested, else 0.35)
urgency = 1 − days_to_sowing / 20
RED ≥ 60 AMBER ≥ 40 GREEN otherwise
Click a pin and the score explains itself, in words, so an officer can check it without trusting the formula. Davinder Brar's 9 acres in Chhajli:

urgency 1 − 14/20 = 0.30 × 0.45 = 0.135
baler free yes × 0.25 = 0
history 0.92 × 0.30 = 0.276
─────
harvested (×1) 0.411 → 41, AMBER
The red line started at 70, and it was impossible to hit. A field must be at least 3 days from sowing to still be bookable. At 3 days urgency is 0.85, and even the worst fire history scores 100 × (0.45 × 0.85 + 0.30 × 1.0) = 68. At 70, a field went red only once it was too late to save, which broke the whole "alert, book, turn green" flow. It's 60 now.
The officer's move is Alert village: every farmer there gets a one-tap WhatsApp offer, and nearby balers see the village flagged. A farmer taps "HAAN, book karo" and the pin goes green.
The fire map that was just a red square
My job on the last two days was production, and the FIRMS layer was the part I was most excited about. Real data, 19,926 points. Then I turned it on:

A red rectangle. Four seasons of fires cover Sangrur so completely that a heatmap can only say "here". A grim finding in itself, and the reason the per-village fire-history score, not the heatmap, feeds the risk formula.
Real data broke the seed, too. Ten seed fields were designed as red candidates, in random villages. With made-up history they went red. With real history, the scorer returned zero red: only four villages could cross 60 at all. The fix put the candidates where red is reachable and stopped make seed wiping real fire history. The live radar shows 0 red and 10 amber as I write, which is honest for a demo district before most harvests.
The number we refused to make up
Every climate project wants a big pollution number. "X kg of PM2.5 prevented" is the first thing a judge looks for.

clearsky ships with no emission factor. The calculation exists (unburnt tonnes × a published kg-per-tonne factor for open burning of rice straw, snapshotted on every Done), but a factor without a citation is ignored. Until the team picks published values and a second person checks them against the paper, the page shows that grey box. Pasting a figure from a blog post would have taken five minutes, and it would have been the one number on the page nobody could defend. The admin side follows the same rule:

One more honesty note, since it's my name on this: most of the live data is a demo season. 60 fields are synthetic, from a fixed seed, with masked numbers the system never messages. The district, villages and fire history are real. The farmers and prices aren't.
What production actually cost us
All of it is serverless, so nothing runs between messages:
| Service | What it does in clearsky |
|---|---|
| API Gateway (HTTP API) | WhatsApp webhook and dashboard API, with a Cognito JWT authorizer |
| Lambda (8 functions, arm64) | webhook, processor, API, hourly risk scorer, 6 pm reminders, offer expiry, FIRMS ingest, health |
| SQS + dead-letter queue | lets the webhook answer Meta in milliseconds while voice takes a minute |
| DynamoDB (12 tables) | farmers, fields, balers, bookings, capacity; transactions stop double booking |
| Transcribe + Polly | Hindi voice in, Hindi voice (Kajal) out |
| EventBridge Scheduler | hourly risk, daily reminders in IST, offer expiry every 15 minutes |
| Cognito | separate sign-in for admin, baler and buyer; self sign-up with officer approval |
| S3 + CloudFront | voice media, FIRMS layer, and the dashboard itself |
| SSM Parameter Store | WhatsApp and LLM secrets, never in the repo |
It's one AWS SAM template, deployed by GitHub Actions through an OIDC role (no AWS keys in GitHub), with a $10 monthly budget alarm. Getting there was a day of small failures, each invisible until it happened:
| Symptom | Cause | Fix |
|---|---|---|
| HTTPS certificate for clearsky.akkki.tech wouldn't issue | the domain's existing CAA records only allowed other certificate authorities | add a CAA record for amazon.com |
| Fire map and Polly voice links failed in the browser | the global S3 endpoint 307-redirects for a regional bucket, which breaks CORS and the signed URL | presign against the regional endpoint |
| Dashboard requests got 401 before they started | the browser's CORS preflight OPTIONS hit the JWT authorizer |
an OPTIONS route with no authorizer |
| WhatsApp replies silently stopped | Meta's temporary access token expired (error 190) | a permanent system-user token, plus secrets that refresh in warm Lambdas after 5 minutes |
| First CI deploy couldn't assume its role | GitHub sent the immutable OIDC subject format, not the one the trust policy expected | trust both forms, still only for main |
| CI deploy failed once, then passed | a test picked "the first confirmed booking" in DynamoDB scan order, which is random | pick the earliest one |
The last is my favourite: the test was right about the code and wrong about the database, and passed for days because the order happened to work out.
One feature mattered more than I expected: demo controls. A season is six weeks. A demo is three minutes.

A simulated clock, a "harvest wave" button for the highest-risk village, and a farmer simulator that records WhatsApp messages instead of sending them. With those, 223 backend tests and 13 Playwright end-to-end runs passed before we had a Meta account or an AWS deploy.
Three things I'd tell myself on day one
Look at the timestamps before you trust the count. The most important fact here came from grouping 19,926 rows by hour. A 93% drop and a 36% drop are both true, depending on whether you ask the satellite or the ground. Starting from the count, we'd have built a better detector for fires that already learned to hide.
Never let the system say something a human hasn't agreed to. The agent only repeats what a tool returned. The farmer only hears ✅ after a baler accepts. No pollution number appears until someone cites one. Each rule cost us a feature that would have demoed better, and each is why I'd trust this with a real farmer.
Real data breaks your seed, not just your code. Every test passed with fake fire history. Real history gave zero red fields and a red rectangle. Get the real file on day one, especially if it's ugly.
If you keep one thing
Stubble fires don't need to be seen, they need to be unnecessary, and that turns out to be a booking problem with a deadline.
clearsky was built in four days by Team Atherion: Sanskar Joshi (the WhatsApp agent and the AWS backend), Kamran Pathan (the dashboards and approvals for every role), Anushka (the pollution-impact view), and me (the AI model, testing, and the production deploy). It's live at clearsky.akkki.tech and open source at github.com/sanskarjoshiii/clearsky.
Sources
- NASA FIRMS, VIIRS S-NPP standard-processing fire detections for the Sangrur district box, October–November 2022–2025. The yearly counts and the hour-of-day split are my own counts over the 19,926 points in the repo's
data/layers/firms_2022_2025.geojson. - Tribune / iFOREST: Satellites miss majority of stubble fires (over 90% of large fires after 3 pm in 2024–25).
- Outlook Business: the evening burning detection gap.
- Tribune: Lok Sabha reply on farm fires and farm fires as the season ends (burnt area and official counts).
- ICC: The last straw: India's burning fields turn into an energy opportunity (boiler demand, pellet plants).
- The clearsky repository: README,
CONTEXT.mddecision log and changelog,domain/risk.py,domain/matching.py,agent/prompts.py. Payouts use the project's demo rates, not market prices. - Screenshots are from the live deployment on 11 October 2026.