Back to Blog
The fires moved to 5 pm

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.

17 min read
  • aws
  • hackathon
  • whatsapp
  • 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.

The clearsky home page. An aerial film of green Punjab fields fills a rounded frame, with a small chip reading "Paddy season · Punjab" and the headline "Straw, Not Smoke." in large white type. The nav bar above has How it works, Who it's for, Field data, Impact, Open dashboard, and a lime button "Join as baler or buyer".

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:

The "One Platform, Everyone Gains" carousel on the home page, on a dark teal panel. The centre card shows a farmer in a white turban speaking into a phone in a harvested field, labelled 01 / 04, "Farmers", with the line "One message · Hindi, Punjabi, voice" and the text "Send name, village, acres and harvest date on WhatsApp, typed or spoken. The agent books the pickup and confirms when a baler accepts. No app, no account." District officers and Baler operators are previewed on either side.

  • 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.)

The "From One Message To A Clear Field" section of the home page: four numbered steps on the left (01 Farmer sends one message, 02 The nearest free baler accepts, 03 Field cleared, straw sold, 04 Risk drops on the radar) and on the right a photo of a farmer speaking into his phone, tagged "Step 01".

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

The Burn Risk Radar. KPI strip: 581 acres registered (63 fields · 61 farmers), 215 acres booked (37%), 0 offers waiting, 55 acres cleared, 0 red fields (10 amber), 538 t straw routed (138 t delivered), 2 saved after alert. Below, a map of Sangrur district with green and amber field pins around Sangrur and Bhawanigarh, and a "Villages by risk" list: Chhajli, Jakhepal, Sheron and Ghabdan amber, Mehlan, Nadampur and Sunam green, each with an Alert button.

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:

The radar with a field drawer open on the right for field F-SEED-005: "Amber 41", "Harvested", "Davinder Brar · Chhajli", reason chips "harvested today", "14 days to sowing", "high fire history", "not booked". Details: farmer phone masked (demo), 9 acres of paddy, harvest 12 Oct confirmed, sowing deadline 25 Oct with 14 days left, village fire history 92/100, and a black "Alert Chhajli" button.

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:

The same radar with the "Fire history" toggle on, zoomed out to show Barnala, Patiala, Bathinda, Ambala and Karnal. The FIRMS heatmap is a solid pinkish-red rectangle covering the whole Sangrur district box, with the field pins sitting on top of it.

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.

The public impact page at /impact: "Straw picked up, not burnt." Six tiles: 215 acres booked for pickup, 55 acres cleared, 538 tonnes of straw routed to industry, 61 farmers on WhatsApp, 2 fields saved after an officer alert, ₹8,29,760 estimated farmer payouts (demo prices, not market rates). Below, a grey note: "Add emission factors (with sources) to show pollution avoided: set EMISSION_FACTORS to published values for open burning of rice straw, each with its citation. Nothing is shown until then, so no unsourced number ever appears here."

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:

The admin Impact page: 55 acres cleared (2 pickups done), 138 t straw kept from fire, ₹2,45,520 paid to farmers (demo prices), 2 saved after alert (5 alerts sent). A grey note says pollution prevented appears once emission factors are set. Below, a season progress bar for 581 registered acres: cleared 55 ac (9%), booked with pickup pending 160 ac (28%), offered 0 ac, not booked yet 366 ac (63%), and an "Acres cleared by village" bar chart.

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.

The admin Demo controls page: a demo clock (real today Sun, 11 Oct, with previous/next day buttons and "Use real date"), season events (Harvest wave in Auto, 5 fields, buttons for Harvest wave, Run risk now, Send tomorrow's reminders, Expire unanswered offers, Reset), and a numbered demo runbook starting "Reset demo data (clock goes to 20 Oct)" and "Open the farmer simulator and send the Hinglish sample".

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

Comments

Sign in with GitHub to leave one — every thread is a public issue on the site's repo.

Loading comments…