
Fined for a fire he never lit: checking stubble burning field by field with Sentinel-2 on AWS
Satellite fire alerts help decide which farmers in Punjab and Haryana are penalised for stubble burning, and a fire 98 metres away can be enough to blame the wrong one. How I built Saakshi to check each field from Sentinel-2 on AWS, why an officer and not a model confirms every burn, and what fought back along the way.

The field in the yellow box was harvested in October and never burnt. It stays tan in every clear picture until December. A neighbouring field went black in early November, and a NASA fire alert landed 98 metres from the box. Under the current enforcement process, that alert can put a red entry against the farmer in the box, which can mean a fine, an FIR, and losing the incentive he was promised for not burning.
The satellite pictures settle the question in about ten seconds, but they are not part of the process. Over four days for Environmental Hacks 2026, by WeMakeDevs and AWS, I built Saakshi ("witness" in Hindi) to make them part of it: it checks every farmer's no-burn claim from Sentinel-2 pictures, pays clean fields quickly, and sends every possible burn to a village officer with the evidence.
On 593 labelled fields along the Punjab–Haryana border, fire alerts wrongly flagged 9.0% of clean fields. Saakshi flagged 0.6%, and approved none of the 111 burnt fields without an officer. The demo is live at d5gpae5oilzws.cloudfront.net (officer PIN 2468) and the code is on GitHub . If you would rather watch first, here is the three-minute demo:
Why fire alerts are the wrong tool, and why timing matters
Every November, paddy straw burnt in Punjab and Haryana adds to the smoke over Delhi. Enforcement relies on fire alerts, hot pixels about 375 m across, seen at the moment a satellite passes. In my labelled sample they caught only 19% of burns, because a field can be burnt and ploughed between two passes. They also flagged 9% of clean fields, because a fire next door lands on your record.
The incentive, ₹1,000 an acre in Haryana in 2024, is paid months after the season, once a committee has checked photos. A randomised trial in 171 Punjab villages found that paying only after verification did not reduce burning, while paying part of the incentive upfront roughly doubled compliance.
Paying upfront is only defensible if every field can be checked afterwards, cheaply, in a way the farmer can see and contest:
- Before harvest, a field that was clean last season gets half the incentive as an advance.
- After harvest, every clear Sentinel-2 picture of the field is read. Fixed rules approve clean fields, and the balance goes to the farmer's bank.
- Unclear cases and every possible burn go to the village officer. Only the officer can confirm a burn.
- A confirmed burn on an advanced field produces a recovery notice and a payment link. The farmer can appeal, by text or voice.

How a burn shows up in Sentinel-2
Sentinel-2 photographs the same field every few days at 10–20 m resolution, fine enough to see a single field. A fresh burn leaves three signs: near-infrared reflectance drops sharply, because standing stubble reflects a lot of it and ash does not; the field turns dark in the normalised burn ratio and in plain brightness; and the darkness persists, where a cloud shadow is gone by the next picture and wet soil dries.
The rules require all three: a sharp near-infrared drop, most of the field dark, and the darkness holding through the following clear pictures. Clean fields are approved and everything else goes to a person.
Reading one field out of a 100 MB tile
The imagery is the Sentinel-2 Level-2A collection in the AWS Open Data registry, stored as Cloud-Optimised GeoTIFFs. A COG is tiled internally with an index at the start, so a reader can fetch one small window with HTTP range requests instead of downloading the whole tile. Most of the architecture follows from that property.
For each scene, the check reads only the pixels around the field. This is trimmed from
src/parali/imagery.py :Python
with rasterio.open(scene.hrefs["scl"]) as src: # scene classification: clouds and shadowsb = transform_bounds("EPSG:4326", src.crs, *field_bbox)win = from_bounds(*b, transform=src.transform).round_offsets().round_lengths()scl = src.read(1, window=win) # a few kilobytes, not the whole tilefor band in ("red", "green", "blue", "nir08", "swir16", "swir22"):with rasterio.open(scene.hrefs[band]) as src:ratio = round(20 / src.res[0]) # 10 m bands averaged onto the 20 m gridw = Window(win.col_off * ratio, win.row_off * ratio, win.width * ratio, win.height * ratio)bands[band] = src.read(1, window=w, out_shape=scl.shape, resampling=Resampling.average)
The scene list comes from the Earth Search STAC API, clouds and shadows are masked with the classification layer, and the pixels inside the field outline become one row per date. The stack runs in us-west-2, next to the imagery. A check takes about ten seconds of Lambda time, a fraction of a rupee per field per season, which is what makes it realistic to check every registered field rather than a sample.
Who decides what

I decided early that no model and no rule would confirm a burn. A possible burn reaches the officer desk with the pictures for every clear date, the burn ratio through the season, nearby fire alerts and last season's result. The officer notes what they saw on the ground and decides, and the steps that follow appear on screen as each one completes.
Money follows the same principle. An early design sent the farmer a link to collect his balance. That puts the work on the wrong person: releasing a payment is the officer's job, so the balance now goes out from the officer's approval, or automatically for a clean field. I wanted RazorpayX for payouts, but it needs a registered business, so payouts run through Cashfree, while Razorpay handles repayment links.
The language model has a deliberately narrow role. When a farmer asks something, Amazon Nova 2 Lite on Bedrock, through the Strands Agents SDK, picks one of seven tools (status, explanation, money, appeal and so on), none of which can approve, reject, pay or penalise. What the farmer is told about decisions or money comes from fixed templates filled from the record, and a guard discards any model reply that states a decision on its own.
How it fits together

AWS Lambda runs four functions: the satellite check and decision rules, the API for the map and both desks, the farmer assistant and WhatsApp webhook, and map tiles.
Amazon DynamoDB holds fields, claims and an append-only event log for each claim. The partition key is the claim and the sort key is the time, so the officer's audit trail is a single query.
Amazon S3 and CloudFront serve the web app, evidence images, recovery notices and payment receipts.
Amazon EventBridge runs a daily sweep that follows up payouts still pending at the bank.
Amazon Polly and Transcribe handle Hindi and English voice, and Sarvam AI handles Punjabi.
The AWS open-source pieces are the Strands Agents SDK, the AWS SAM CLI that builds and deploys the single template, and boto3. rasterio and GDAL read the Open Data COGs.
What fought back
The labels that hid burns. On day one I did not know what a burnt field looked like from space. To measure anything I labelled 749 plots by hand from strips of satellite pictures, and I was never sure of my own calls, so I asked for a second check. Going through the plots where Saakshi and my labels disagreed, the problem turned out to be the strips: they showed only a sample of dates, and on some plots the burn fell on a date the strip skipped. A second read of 249 plots, on strips showing every clear date, found 42 burns the first pass had missed, and the precision of Saakshi's flags rose from 38% to 75%. I report both, because the second read was guided by the burn signal and leans toward agreeing with the rules. On my first-pass labels alone the gap still holds: 1.6% against 8.7%.
A message that contradicted itself. On one Kaithal field, the farmer's explanation read: "69% of the field looked dark; below the 30% burn level." Both numbers were correct. The field was dark enough for a burn but showed no sharp drop in near-infrared, so the rules rightly declined to call it a burn, then fell through to a message written for a different case. There is now an explicit outcome for a field that is dark without the drop a fresh burn causes, usually straw burnt on stubble that was already bare, or wet soil. It goes to the officer with an accurate explanation.
The phone line that became WhatsApp. The first plan for reaching farmers was a call-in phone line. Twilio's trial account would not enable voice calls for India without upgrading, and that dead end turned out to be useful: farmers in Punjab already live on WhatsApp. Saakshi now answers there in the farmer's language, with voice notes, and accepts questions by voice. Those voice notes arrive as Ogg Opus at varying sample rates, and Amazon Transcribe's streaming API needs the rate up front, so it is read from the Opus header before streaming.
"IP not whitelisted." Cashfree's payout API rejected every call from Lambda, because Lambda has no fixed IP address to allowlist. The usual fix is a NAT gateway with an Elastic IP, billed every hour whether or not anyone is paid. Cashfree also accepts a signature instead: the client ID and a Unix timestamp, encrypted with their RSA public key using OAEP and sent as a header, written in plain Python to keep the Lambda package free of a cryptography dependency. The next call failed with
transfer_remarks_invalid , because the remark contained a hyphen; remarks may only contain letters, digits and spaces. Sandbox transfers then settled anywhere from 3 to 19 seconds later, so payouts are followed up in the background, with an EventBridge sweep for anything still pending the next day.The demo I almost got wrong. Before scripting the demo video, I checked each farmer's claim against the data. One I had planned to show the officer approving, on the grounds that his dark patch was wet soil, was a clear burn: 100% of the field black on 6 November. A different farmer's claim, a fire alert 98 metres away and no scar on his own field, was the true version of that story, so the video follows him instead. His is the field in the yellow box at the top of this post.
Thirty payment links. During the final takes of the video, confirming a burn stopped producing a payment link, and the log said only
HTTPError . Once the response body was logged, Razorpay's reason was plain: test mode allows 30 payment links per account, in total, and the test runs had used them all. A new test account fixed it in ten minutes. Every external call now logs the provider's error body, not just the exception type.What the farmer sees

The farmer sees the same pictures the officer saw, in Punjabi, Hindi, Haryanvi or English (a friend who speaks Punjabi and Haryanvi checked the wording), with a voice note that reads the message aloud. An appeal cancels any open payment link and returns the case to the officer.

On WhatsApp the farmer can pick or say their name, tap Why for the pictures, tap My money for the balance, or ask by voice. To record that last part for the video, I played a Punjabi voice clip from my laptop next to my phone.
Results
On 593 labelled fields in Patran (Punjab) and Kaithal (Haryana), 111 of them burnt:
| Fire alerts | Saakshi |
|---|---|---|
Clean fields wrongly flagged | 9.0% | 0.6% |
Flags that were real burns | 6% | 75% |
Burnt fields approved without an officer | n/a | 0 |
Fields that were clean in 2024 burnt in 2025 at 1.8%, compared with 8.6% for the rest, which supports using "clean last season" as the basis for the advance. By published emission factors, each acre kept clean keeps roughly 19 kg of PM2.5 out of the air.
What this does and does not show
- Payments run in test mode. Cashfree simulates the bank transfer and Razorpay the repayment, and no real money moves.
- The farmers in the demo are fictional, and the field outlines are samples, since there is no public cadastral map.
- The labels are readings of satellite pictures, not site visits, and the second read leans toward agreeing with the rules, which is why the first-pass numbers are reported next to the headline.
- The PM2.5 figure is an estimate from published emission factors, not a measurement.
What I would do differently
Build the evaluation set first. The most important bug in the project was in the label strips, not the code, and it only surfaced because there was an evaluation set and I looked into the disagreements instead of adjusting thresholds until they disappeared.
Keep the model's job small. Routing a question to a tool is something a small model does well and cheaply. Deciding whether a farmer gets paid is not, and settling that boundary early made later decisions simpler.
What comes next
Running against a district's real field registry, moving payouts from test mode to the state's direct-benefit-transfer rail, and giving officers a batch view of the season's payouts.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article