AWS Builder Center
93% fewer fires, and the same smoke

93% fewer fires, and the same smoke

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.

I am a design engineer
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.
Hero section - clearsky


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:
Home roles - clearsky

  • 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.)
How it works

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:
text
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):
text
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:
text
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 admin Bookings table, filtered to All dates, with declined and expired offers shown. Five rows for one farmer in Sangrur: a 50-acre field offered to Kamran on 11 Oct (Expired), then to Sanskar Vinaykumar Joshi (stop 1, Done); and a 5-acre field offered to Jaswinder Gill on 13 Oct (Expired), then Kamran (Expired), then Sanskar Vinaykumar Joshi (stop 1, Done). Columns: Date, Baler, Stop, Farmer, Village, Acres, Tonnes, Straw to, Payout (demo), Status. Header shows 21 confirmed · 537.5 t.

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.
Burn risk radar

Every hour, a scheduled Lambda scores every unbooked field:
text
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:
Field drawer

text
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:
Fire history layer

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.
Public impact

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:
Admin impact

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.
Demo controls

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 .
  • Screenshots are from the live deployment on 11 October 2026.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article