Every air quality app tells you the air is bad. Saans tells you what to do about it today — for your lungs, your routine, your neighbourhood.
A personal air-exposure agent on Telegram. Free, open-source, works anywhere in India — including the towns with no monitoring station.
Built for Code for a Billion — Bharat Agentic-AI Hackathon 2026, Track 04 Clean Air.
1.67 million deaths a year in India are attributable to air pollution — 17.8% of all deaths in the country (Lancet Planetary Health, GBD 2019). 100% of India's 1.4 billion people live where PM2.5 exceeds the WHO guideline. The average Indian loses 3.5 years of life expectancy; the average Delhi resident loses 8.2 — the largest of any major city on earth (AQLI 2025).
The forecast data needed to reduce personal exposure is already public and free. It reaches almost nobody in a form they can act on.
Every air quality app in India reports the same thing — a number — to a marathon runner, an asthmatic child, and a construction worker alike. A number is not a decision.
Harm is not concentration. It's dose:
dose = concentration × duration × ventilation
Ventilation — how hard you're breathing — varies about 7× across a normal day, and no consumer app in India models it.
Real Open-Meteo forecast, Delhi, 26 Aug 2026. Same 45 minutes outdoors at 187 µg/m³:
| Activity | Ventilation | Inhaled |
|---|---|---|
| Sitting | 6 L/min | 51 µg |
| Running | 45 L/min | 379 µg |
A run puts 7.5× more particulate into your lungs than sitting in identical air. So the useful advice is almost never "stay inside" — people go out anyway and stop reading you. It's move it to 4pm.
That day ran 79 µg/m³ at 9am and 187 at midnight — a 2.4× swing. Shifting a morning run from 6am to 9am cuts intake by half, at zero cost to the runner.
The cleanest hour is very often the hottest one — pollution disperses as the ground heats, which is the same process that makes exertion dangerous. Optimising for air alone tells people to run at 10am in an Indian summer.
So heat is a constraint, not a term in the score. Blending µg/m³ and °C into one number is unexplainable and easy to get quietly wrong. A veto can be said out loud:
🏃 Morning run — best at 7 AM (80 µg/m³, 34°C) You usually go at 6 AM (95, 32°C) — shifting is 16% less in your lungs. Cleaner hours later are too hot to push in — this is the balance.
The threshold depends on what you're doing: 36°C blocks a run but not a school walk. Apparent temperature is used throughout, since humidity is what decides whether 30°C is pleasant or dangerous. Cold matters too — North India's smog season is its cold season, and cold air triggers bronchospasm in asthma.
Dedicated tracks and footpaths are the exception in India, not the rule. That matters because the forecast is a coarse-grid ambient background value — kerbside air beside moving traffic is materially worse than the grid cell it sits in, and the gap is widest at the commute peaks.
So a "clean" 10am can be a dirty 10am at the roadside, and the forecast cannot see it. Saans keeps road activities out of peak traffic (8–11, 17–21) and says why. Nobody with a park or a track has to accept that — set off_road and the constraint lifts.
We do not invent a kerbside multiplier. No honest one exists without knowing the road, and a fabricated number in a health product is worse than no number. Both heat and traffic are constraints with stated reasons, never hidden adjustments to a figure.
आज की हवा — Rohini
औसत 122 µg/m³ · AQI 302 (बहुत खराब)
🏃 दौड़ — सबसे अच्छा समय सुबह 9 बजे (79 µg/m³)
आप सुबह 6 बजे जाते हैं (112) — समय बदलने से 29% कम प्रदूषण फेफड़ों में।
🌆 सैर — सबसे अच्छा समय शाम 4 बजे (80 µg/m³)
आप रात 9 बजे जाते हैं (156) — समय बदलने से 49% कम।
🪟 खिड़कियाँ: सुबह 8 बजे से शाम 6 बजे तक खोल सकते हैं
😷 मास्क: सुबह 8 बजे से पहले और शाम 6 बजे के बाद
One message each morning, Hindi or English. Then you can just ask it things:
you: I'm on a construction site 8 to 6, I can't change that Saans: Then the fix isn't timing, it's the two worst hours. Today 8–10am sits around 83 and the site stays under 100 until 5pm. It's after 6 that jumps: 125 by 7pm, 156 by 9. Head straight home rather than staying out, and if you have an N95 keep it for the evening, not the workday.
Telegram → @SaansIndiaBot → /start. Ninety seconds to set up.
/when any time for today's windows.
Open-Meteo ─► cpcb.py ─► dose.py ─► advisor.py ─► { facts } ─┬─► render.py templates ─► daily push
└─► agent.py LLM + tools ─► chat reply
───────── deterministic ───────── language only, never arithmetic
Compute, then narrate. Every number is calculated in Python. The message layer only renders facts it was handed. The LLM never sees a raw forecast array and is never asked "what's the AQI" — it receives a fact block that already contains the answer.
This is a health product. A hallucinated µg/m³ is not a cosmetic bug, and "the model said so" is not a defence. We don't stop it inventing numbers — we never ask it to produce one.
| File | |
|---|---|
ingest.py |
Open-Meteo client, 3h cache, refuses partial data |
cpcb.py |
CPCB National AQI breakpoints, sub-index, categories (en/hi) |
dose.py |
ventilation table, dose maths, window scan |
heat.py |
heat-safety veto by exertion level, cold-air trigger |
traffic.py |
roadside exposure veto during commute peaks |
advisor.py |
forecast + profile → structured advisory facts |
render.py |
facts → Hindi/English message templates |
agent.py |
conversational agent: 6 tools, prompt, loop |
bot.py |
onboarding and command routing |
telegram.py |
Bot API + geocoding over stdlib |
db.py |
SQLite: users, advisories, events |
main.py |
daily scheduler, polling, /metrics |
No dependencies. Stock Python 3.11+, nothing to pip install.
git clone https://github.com/avinashnegi1999/saans && cd saans
python3 check.py # 12 module self-checks
TELEGRAM_TOKEN=<from @BotFather> python3 main.pyOptional — enables the conversational agent. Without it the daily push works exactly the same:
export ANTHROPIC_API_KEY=<key> # needs: pip install anthropicSee today's windows for anywhere in India, no bot needed:
python3 demo.py # Delhi
python3 demo.py 29.75 78.52 KotdwarDocker:
docker build -t saans . && docker run -e TELEGRAM_TOKEN=... -v saans:/data saans- The forecast isn't ours. Saans is a translation layer over Open-Meteo (CAMS model output). We interpret air quality — we don't measure it. Accuracy is theirs.
- CPCB AQI is defined on 24-hour averages, so hourly AQI does not exist. Most apps present it anyway. We schedule on raw hourly µg/m³ and compute a true CPCB AQI only for the rolling 24h figure. Category names on hourly values are a readability aid, and the real number always travels with the label.
- Ventilation rates are typical adult values from exercise physiology, not per-user measurements. They live in one table in
dose.pyand are meant to be tuned. - Roadside air is worse than what we show. The forecast is ambient background; we can flag peak traffic but cannot quantify your kerb. Traffic peak hours are a heuristic, tunable per city in
traffic.py. - Not medical advice. Saans schedules exposure. It does not diagnose, interpret symptoms, or advise on medication — and it refuses to, in both languages, before the model is ever called.
- CPCB's top AQI band is published as "250+" with no ceiling; we interpolate 250→380 for 401→500, the conventional implementation. Commented in the code rather than left as a magic number.
Apache-2.0. Built for Code for India — take it, fork it, run it for your city.