AWS Builder Center

The Sprinkler Nobody Measured

Delhi runs 733 anti-pollution machines and can’t prove any of them work. We built an AI command centre on AWS that dispatches the fleet where people breathe and verifies every spray against a counterfactual.


Picture 8 a.m. on a November weekday. A water sprinkler drives its fixed route past an empty landfill yard reading AQI 450. Two kilometres away, three schools sit at AQI 350 as children line up for assembly.
The sprinkler's GPS log will say it did its job. Nobody will ever check whether the air got cleaner.
We started this project trying to make Delhi's fleet faster. We ended up building something narrower: a system that has to show its evidence before it is allowed to say "this worked."
This post covers what we built for Environmental Hacks 2026 (Air track), the model that lost to a one-line baseline, the rule our AI agents are not allowed to override, and what actually runs on AWS today.
The short version* What it does: maps Delhi's air on a 500 m grid (10,600 cells), tags each hotspot with a likely cause, forecasts 72 hours ahead, drafts a dispatch plan for the existing fleet, and verifies each intervention against similar untreated cells.* What is real: 24 live CPCB/DPCC stations, NASA FIRMS fire detections, weather data, the models, the optimiser, the agents, the officer console and a five-role Flutter app.
  • What is simulated: the fleet, the vehicle-mounted sensors and the interventions. The verifier is tested on effects we injected ourselves.
  • What runs on AWS: one t3.medium EC2 instance in Mumbai serves the whole stack; S3 holds citizen evidence photos through pre-signed uploads; an IAM role means no keys live on the box.
  • Live: [LIVE LINK] · Code: [REPO LINK]

Part one: the problem is not data, it is proof

Delhi is not short of air-quality data. It publishes station readings every few minutes, and an AQI number reaches every phone.
What it is short of is evidence. The city deploys 376 anti-smog guns, 266 water sprinklers and 91 mechanised road sweepers. GPS proves where each one drove. Nothing proves the air got cleaner afterwards.
The spending follows what is visible, too. Road dust is about 15% of Delhi's winter PM2.5, yet it draws about 64% of available NCAP funds (CEEW, Feb 2026, [CEEW LINK]). Water settles dust. It does nothing for smoke from burning waste or stubble, and a truck sent to a smoke hotspot is a wasted shift.
So we stopped asking "how do we route the fleet faster?" and asked a narrower question:
What evidence should a city need before it says a spray run cleaned the air?
Everything else in HawaCommand hangs off that question. The loop runs every three hours: sense, map, attribute, forecast, decide, act, verify. The last step is the one nobody else builds.

Part two: rank by people, and the twin that lost to a baseline

The highest AQI is not where the most harm happens. We rank hotspots by person-hours of excess exposure: PM2.5 above the safe level, times the people present that hour, times a vulnerability weight for schools and hospitals, times duration. A landfill at AQI 450 with nobody around ranks below a school street at 350.
To know PM2.5 on every street, not just at stations, we fit a Gaussian process (kriging) across 10,600 cells of 500 m, from 24 live stations. Every cell carries a confidence score that falls with distance from the nearest monitor.
Then we evaluated it on held-out stations, and it lost badly.
Method
Error on held-out stations (MAE, µg/m³)
Copy the nearest station
7.40
Inverse distance weighting
7.05
Our Gaussian process twin
37.82
Our model was five times worse than copying the closest station. The reasons are mundane: the training history is synthetic, 24 points across 1,484 km² are too few for the kernel to learn a sensible length scale, and we evaluated on only 3 held-out stations.
We kept the twin for one reason: it says how unsure it is, and a baseline does not. The console shows that confidence on every cell and never quotes twin accuracy as a feature. Beating inverse distance weighting is the first job before we claim the twin adds anything.
The forecaster held up better. On the same synthetic history, it beats persistence (assuming tomorrow equals today) at every horizon: +14.3% at 1 h, +27.3% at 6 h and +22.5% at 72 h.

That shows the pipeline works end to end. It does not yet show that it predicts real Delhi air; retraining on multi-year CPCB history is next.

Part three: the optimiser proposes, the code decides, the model explains

We did not let a language model plan the fleet. A Google OR-Tools vehicle-routing solver drafts the plan from exposure scores, water capacity and shift limits. Four agents (Forecaster, Planner, Auditor, Communicator) then explain and critique it in words an officer can read.
The plan is shown like a pull request. Each task is a line in a diff against the usual routes, the Planner writes its reasoning, and the Auditor can leave a review comment that blocks approval.

The important design choice is where the verdict comes from. The Auditor's rules are plain Python; the model only writes the explanation. This is the check that rejects water on smoke, trimmed from  auditor_agent.py :
Plain text
if h.cause == SourceClass.OPEN_BURNING and "sprinkler" in t.asset_id.lower():
violations.append(
f"Sprinkler {t.asset_id} dispatched to open burning at {t.cell_id}. "
"Water is ineffective on smoke; an inspection team is required.")

if violations:
verdict = "revise" # decided here, not by the LLM
explanation = llm_client.generate(prompt, system_prompt=self.system_prompt)
if not explanation:
explanation = f"Plan rejected: {violations[0]}"
A model can be talked out of a rule. An  if  statement cannot. The model's job is to say why in plain language, and if it returns nothing, the rejection still stands.
Not every check is code yet. Our prompt also asks the Auditor to flag "gaming": cleaning operations clustered around an official CPCB monitor, which improve the reading without cleaning anyone's air. Today that guard is an instruction to the model, not a rule. Making it a distance check against station coordinates is on the list.
Nothing dispatches without an officer clicking Approve. Routes then reach drivers in a Flutter app that serves five roles from one codebase: drivers see the day's route and log each stop, inspectors get targets to visit with photo evidence, schools get a safe outdoor window, citizens see the air near them and can report burning, and site managers see their site's score. All of it works in Hindi and English.
[IMAGE: Flutter app, driver route + citizen "Air near me" side by side]

Part four: proving a spray worked

Air quality changes on its own. Wind shifts, traffic thins, the sun lifts the inversion layer. If PM2.5 drops after a sprinkler passes, that proves nothing by itself.
So we ask a counterfactual question: what would this street's air have done if the truck had never come? We answer it with a synthetic control. Before the spray, we find a weighted blend of similar untreated cells whose PM2.5 tracked the treated cell as closely as possible. After the spray, that blend is our "no truck" street. The gap between the two is the effect.
The weights solve a small constrained least-squares problem with SLSQP in SciPy: minimise the squared gap between the treated cell and the weighted donors before the spray, with every weight between 0 and 1 and the weights summing to 1.

We have no real fleet to test on, so we tested the verifier the only honest way available: we injected an effect we chose, then checked whether it could find it.
Check
Result
Effect we injected
−18.5% PM2.5
Effect the verifier recovered
−18.8% PM2.5
Error
0.32 percentage points
95% confidence interval
−22.2% to −15.4%
Duration of effect
120 minutes
This proves the method recovers a known answer. It does not prove sprinklers reduce PM2.5 by 18% in Delhi; that number needs real HawaTag spray logs on real streets.
Each verified result is written to an effectiveness library keyed by cause and action (dust × sprinkler, burning × inspection), so the next plan is drafted from what actually worked last time. That is the loop closing: verify feeds decide.

Part five: what runs on AWS, and why it belongs there

We deployed less than our first architecture diagram promised, on purpose. That diagram had fifteen AWS logos. Most of them would have been boxes with nothing calling them.

The backend calls the AI-ML engine in-process, and the app uploads evidence photos straight to S3 with a URL the backend signs
Loop stage
AWS service
What it does in HawaCommand
Status
Sense
AWS IoT Core
HawaPod and HawaTag packets arrive over MQTT/TLS; an IoT rule routes them on. Today a simulator publishes the exact packets a real pod would
[Coded ]
Sense
Amazon Kinesis Data Streams
 hawa-sensor-data  buffers streaming telemetry so a burst of readings never blocks the API
[Coded]
Map
Amazon ElastiCache (Redis)
Caches the 500 m PM2.5 grid snapshot ( grid:pm25:500m:* ) and tracks live fleet positions in a geo index ( geo:fleet:active )
[Coded]
Act
Amazon DynamoDB
 hawa-citizen-grievances  (with a  WardStatusIndex  GSI so each ward sees its open reports) and  hawa-fleet-telemetry  (TTL expires old pings)
[Coded]
Act
Amazon S3
 hawa-citizen-evidence-prod  holds report photos; public access blocked; the app uploads with a pre-signed PUT from  POST /v1/uploads/presign 
Live
Decide
Amazon Bedrock
Runs the Planner, Auditor, Forecaster and Communicator agents
[Coded]
Act
AWS Step Functions
Approve → record dispatch → notify; every dispatch gets an execution ARN
[ Coded]
Act
Amazon SNS (+ FCM)
Fans alerts out to SMS/email and push notifications on the app
[Coded]
Run
Amazon EC2 / ECR + Fargate
EC2  t3.medium  in Mumbai serves the live demo; the backend image is built to ECR for Fargate
EC2 Live · Fargate [Coded]
Secure
IAM + AWS Secrets Manager
Instance role with IMDSv2 required, so no access keys on disk; WAQI, OpenAQ, FIRMS and FCM credentials live in Secrets Manager
IAM Live · Secrets [Live ]
The role can do exactly one thing to S3:
Plain text
{
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::hawa-citizen-evidence-prod/*"
}
No  GetObject , because nothing in the code reads photos back yet. We added permissions only when a line of code needed them.
Second, AWS stays optional at runtime. Every integration sits behind a feature flag and falls back to an in-process path, so a demo survives a network failure or a cold service. We test each one against  moto  mocks before it touches the real account.


Part six: what fought back

The agents went quiet, and the demo still looked fine. Our Bedrock client required a static  AWS_ACCESS_KEY_ID . On an instance with an IAM role there is no static key, so the client switched itself off without an error. The agents fell back to template text, which read plausibly enough that nobody noticed. We fixed it by using the default boto3 credential chain, and now we check for the  Initializing Amazon Bedrock client  log line instead of trusting the output. A fallback that looks like success is the most expensive failure in the system.
We deleted our first AWS layer and rebuilt it. The first pass produced wide scaffolding that had never run against anything. We replaced it with small integrations, one service at a time, each with a caller in the code and a test behind it. It was slower, but every line of it has been run.
Two screens disagreed about the same plan. The landing page said Plan #214 protected 21% more people; the console said 42%. Both were "right", computed from different snapshots. A judge, or a ward officer, sees only that one of them is wrong. Both now read from the same source.
Plans live in memory. It was the fastest way to a working approve flow, and it is why the backend runs as a single task. Moving plan state into DynamoDB is what lets it scale out.

What we are not claiming

  • HawaCommand has not been used by MCD or DPCC. The fleet, the vehicle sensors and the interventions in the demo are simulated, and the console says so on every screen that shows them.
  • The IoT pipeline is real, but the devices feeding it are a simulator. No physical HawaPod has been deployed on a bus yet.
  • The models are trained on synthetic station history. The forecast skill above is evidence that the pipeline works, not that it predicts real Delhi air.
  • The −18.8% is the verifier recovering an effect we injected. It is not a measured effect of sprinklers in Delhi.
  • Our twin is currently worse than a nearest-station baseline.

Three things we would tell ourselves on day one

  1. Write down what would count as proof before you build the feature. "Verified" meant nothing until we wrote the injected-effect test.
  1. Put the rules in code and the words in the model. Verdicts that matter should not depend on a prompt.
  1. Add an AWS service only when a line of code calls it. Our first diagram had fifteen logos and no callers. The second has the same services, and every one of them does a job you can point to.

Try it

Built for Environmental Hacks 2026, Air track, part of the WeMakeDevs × AWS Bharat Builds Tour. [TEAM: Straw Hat Pirates] . AI coding assistants helped with implementation and drafting; the team is responsible for every claim and line of code.
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