AWS Builder Center
Smokeprint: A Delhi Air-Pollution Triage Tool That Knows When Not to Dispatch

Smokeprint: A Delhi Air-Pollution Triage Tool That Knows When Not to Dispatch

How I built a triage tool for Delhi air-pollution spikes that decides whether to send an inspector at all. It separates sensor faults and regional smog from local events, ranks comparable nearby sites, and runs on AWS Lambda, Step Functions, S3 and DynamoDB. I also cover what broke along the way.

The problem

When an air-quality monitor in Delhi spikes, an officer has to decide whether to send an inspection team. That decision costs time and money, and the dashboards I found only say that pollution is high. They don't say whether the reading can be trusted, whether the cause is local, or where to look first.
Smokeprint is my attempt at that decision layer for the Air track. It uses real past data from Delhi-NCR monitors and sorts each spike into one of three outcomes.
Outcome on screenWhat it meansAction
Refused: instrument faultThe data looks like a sensor or logger problemNo dispatch
Refused: regionalMost stations rose together, so there is no single local sourceNo dispatch
Inspection shortlist (comparable candidates)A local event: several nearby sites could plausibly contributeCheck the list
The idea fits in one line: knowing when not to dispatch is as valuable as knowing where to go.

What it does on real data

Gurugram, June 2026. PM2.5 is above 300 µg/m³ at times. But the PM2.5-to-PM10 ratio sits at 0.600 across the whole morning window, then the readings drop out. Real air doesn't hold a ratio that steady. Detectors for a constant-ratio lock and a dropout after a peak flag it, and Smokeprint says no dispatch.
Delhi, October 2025. Most monitors across the region rose together. Comparing a station to the median of all the others shows there's no local excess, so this is regional smog. No dispatch.
Anand Vihar, November 2025. PM2.5 climbs overnight from about 180 to 400 µg/m³, with PM10 around 536 at the peak. This is a local event, so Smokeprint builds a shortlist of nearby OpenStreetMap sites. Each has a distance, a wind alignment and a confidence tier. It's explicitly a triage list, not an accusation. One site, a thermal power station, lines up with the wind, but OpenStreetMap tags it as disused. Smokeprint says inspection there is unlikely to be useful, so a team doesn't drive to a closed plant.
Inspectors get the same case as plain-language cards that fit a phone screen.

How it works

  1. Data quality first. Hourly readings are checked for physically impossible values, constant ratios and dropouts. Units are read from sensor metadata and never guessed.
  2. Regional or local. Each station is compared with the median of the others.
  3. Plume footprint. For local events, 500 simulated air parcels are traced backwards along ERA5 wind with boundary-layer height.
  4. Scoring. Candidate sites from OpenStreetMap get a forward Gaussian plume likelihood, gated by the particle signature. In weak wind (below 1.5 m/s), direction is unreliable, so every candidate is capped at low confidence.
  5. Review. Two agents built with the Strands Agents SDK, an investigator and a critic, review each case on a small local model. A validator rejects any output containing an ID, number or date that the tools didn't return.

The AWS architecture

  • S3: the raw OpenAQ archive is on AWS Open Data, and S3 also holds the case bundles and the static website.
  • DynamoDB: case records and run records, on-demand.
  • Lambda and an HTTP API: serve the cases to the web app.
  • Step Functions: a four-state workflow (load the case, run the checks, review, store) with a catch path for failures. "Replay on AWS" in the app starts it.
  • CloudWatch: a dashboard and logs for the runs.
The stack is serverless, defined with AWS SAM, and designed to stay inside the free tier. The front end is Next.js with MapLibre.

Testing it honestly

I evaluated the shortlist with 500 simulated plume injections. The injections used different dispersion physics from the scorer, plus perturbed winds. The shortlist's first pick was right 54.4% of the time (95% interval 50.0 to 58.6), against 34.4% for a nearest-upwind baseline. For the top three, it was 72.2% against 60.0%.
This is a simulation. It shows the method is consistent, not that it would find real polluters, and the site says so on its Evidence page.

What fought back

This is the part I learned the most from.
My first model lost to a baseline. My original scoring, with hand-set priors for each source type, was beaten by "pick the nearest site upwind." Removing the priors and scoring candidates by forward plume likelihood fixed it.
The numbers on screen didn't match the raw data. This was the biggest lesson. A chart fell back to hard-coded placeholder values for the regional case. Several cases had been cloned from my first case, so they carried the wrong wind. Timestamps were mislabelled between UTC and IST. A Lambda reimplementation had its wind direction inverted and showed a confident shortlist on the wrong side of the city. I fixed each one, then added tests: every concentration shown in the app must equal a row in the raw archive, and every case's wind must match the ERA5 hour of its own peak.
A language model invented facts. An early critic output said a power station had closed in a specific year because of pollution. None of my tools had returned that. Now the validator rejects any date or reason that isn't in a tool result, and the critic's text is built from templates filled with verified values.
Weak wind made some cases meaningless. One case turned out to have a wind of 0.19 m/s, where direction means nothing. I hid it from the demo instead of showing a confident-looking shortlist.
Platform limits. Calls to Amazon Bedrock returned an "Operation not allowed" error on my account, so the agents run locally on a small model instead. The scientific Python stack was too large for a zip-packaged Lambda, so the Step Functions workflow replays a stored, validated result. I labelled it as replayed everywhere and never described it as a live recompute.
I built this with an AI coding agent, which is also why the tests matter. Its summaries sometimes reported results that weren't in the data, and the only dependable check was comparing the app against the raw files myself.

Limits and what's next

  • It's a triage tool. It doesn't prove who emitted anything.
  • The evaluation is simulated, and the cases are a handful of real events.
  • Next steps: live ingestion, a real inspector outcome log to calibrate the confidence tiers, and an agent step that runs inside AWS.

Try it

  • Demo: http://smokeprint-demo-k9f2a1.s3-website-us-east-1.amazonaws.com
  • Video: https://www.youtube.com/watch?v=X9mkMpE6tb8
  • Code: https://github.com/Unknownx-x1/SmokePrint_EnvHacksAWS
Built for the AWS x WeMakeDevs hackathon, Air track.
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