🟢 PurpleAir AQI Notifier

Free Telegram alerts the moment air quality crosses a threshold that matters — powered by community PurpleAir sensors, corrected the way the EPA recommends.

Open the bot on Telegram →

What it does

This bot watches PurpleAir sensors for the locations people subscribe to and sends a Telegram message the moment the air quality category changes — good → moderate, moderate → unhealthy, and so on, in either direction. No app besides Telegram, no account, no cost.

Get started in under a minute:

  1. Open the bot and tap Start.
  2. Send /subscribe leadville-co — Leadville, CO ships out of the box.
  3. Want a different town? /addlocation boulder-co finds the nearest active sensor automatically.

You can subscribe to more than one location, check /status any time, and /unsubscribe whenever you want. Full command reference (including how to point it at a specific sensor by hand, and how to flag a sensor that looks wrong) is under Using the bot below.

Why the numbers may differ from PurpleAir's own map

If you check a sensor on PurpleAir's map and compare it to what this bot reports, don't be surprised if the numbers don't match — often by a meaningful margin. That's expected, not a bug. Here's why.

PurpleAir sensors are low-cost laser particle counters. They're great at detecting relative change in real time, but on their own they systematically overestimate true PM2.5 concentration compared to EPA reference-grade monitors — and the size of that overestimation gets worse in dry air and at high altitude, which describes plenty of the mountain towns this bot was built for. PurpleAir's own map, by default, shows AQI computed straight from the raw sensor reading, with no adjustment for this known bias.

This bot instead applies the correction formula published by Barkjohn, Norris & Clements (2021) — developed specifically for PurpleAir sensors, using co-located reference monitors, and the correction the US EPA itself recommends and uses on its AirNow Fire and Smoke Map. In plain terms: it takes the average of the sensor's two PM2.5 channels plus the local humidity and temperature, and produces a value closer to what a reference-grade monitor would read at that spot.

The formula

// pm25 = average of the sensor's A/B channel readings (µg/m³) // rh = relative humidity (%) temp = temperature (°F) if (pm25 < 343) { corrected = 0.541 * pm25 - 0.0618 * rh + 0.00534 * temp + 3.634 } else { // separate high-concentration fit - relevant during heavy wildfire smoke corrected = 0.46 * pm25 + 0.000393 * pm25^2 + 2.97 }

That corrected PM2.5 value then goes through the 2024-revised EPA breakpoint table (below) to get the final AQI this bot reports — which is why every AQI value it sends is labeled (EPA-corrected).

Try it yourself

This chart plots the exact formula above: raw PurpleAir AQI (uncorrected) vs. the EPA-corrected AQI this bot actually reports, across a range of PM2.5 readings. Drag the sliders — notice how the gap between the two lines grows in drier air, which is exactly the mismatch people notice when comparing this bot to PurpleAir's map in a place like Leadville, CO.

Raw vs. EPA-corrected AQI Line chart comparing uncorrected PurpleAir AQI to EPA-corrected AQI as raw PM2.5 increases.
Raw / uncorrected AQI (PurpleAir's default map display) EPA-corrected AQI (what this bot reports)
At 38 µg/m³ raw PM2.5 (Leadville's actual reading today)
—
EPA-corrected
—

A real example: Leadville, CO

We pulled live data on a dry summer afternoon from the sensor this bot uses in Leadville, plus three others within a couple hundred meters of it (conditions varied slightly sensor to sensor, so each row uses its own actual humidity/temperature reading):

SensorRaw PM2.5 (µg/m³)RHTempRaw / uncorrected AQIEPA-corrected AQI
Sloan_flex-f877 (used by this bot)38.011%83°F10779
Sloan_flex-d54e38.110%85°F10779
East 4th35.010%80°F9976
Sloan_zen-233.812%92°F9774

All four sensors agree closely with each other once corrected. The ~20-30 point gap between each sensor's raw and corrected AQI isn't sensor disagreement — it's the same known overestimation bias, showing up consistently across every nearby sensor, in exactly the dry/high-altitude conditions where it's most pronounced.

AQI levels this bot alerts on

You get a message any time a subscribed location crosses one of these boundaries, in either direction:

LevelAQI rangeWhat it means
🟢 Good0–50Air quality is fine for everyone.
🟡 Moderate51–100Fine for most people; take it easier outside if you have asthma or another breathing condition.
🟠 Unhealthy for Sensitive Groups101–150Kids, older adults, and anyone with heart or lung conditions should limit time outdoors.
🔴 Unhealthy151–200Everyone may start to notice effects. Limit outdoor activity.
🟣 Very Unhealthy201–300Health alert — avoid outdoor activity if you can.
🟤 Hazardous301+Emergency conditions — stay indoors.

More detail on any level: airnow.gov/aqi/aqi-basics

Sensor health checks

Individual PurpleAir sensors can go wrong in more than one way, and this bot checks for three of them on every scheduled poll, automatically switching a location to a nearby healthy sensor whenever it confirms one:

Diverging channels. Each sensor has two independent PM2.5 sensors that normally agree closely. Occasionally one fails (dust, insects, moisture) and reports wildly different numbers from the other — this bot checks for that disagreement before trusting a reading.

Flatlined. A sensor can keep reporting right on schedule — so it never looks offline — while its value has effectively stopped moving for hours. Confirmed against nearby sensors' current reading first, so a genuinely calm, stable-weather day doesn't get mistaken for a stuck sensor.

Erratic. The opposite problem — a sensor's own reading swings wildly while sensors a few meters away stay calm the whole time. Confirming this pulls each nearby sensor's own recent history from PurpleAir directly (not just a single live snapshot, which an erratic sensor could pass or fail depending on the exact moment it's checked), and only acts if several of them were genuinely stable over the same window — a real, shared event that made everything swing doesn't trigger a switch.

Subscribers can also flag a location directly with /reportissue <slug> — the bot re-runs all of these checks immediately and either fixes it on the spot or logs it for review, no manual intervention required to file or triage a report.

A successful switch happens quietly in the background — the bot just keeps using the healthy sensor going forward, the same as it always has. (If you check /status or file a /reportissue right around when it happens, it'll mention the switch — but there's no proactive alert for it, since routine self-healing doesn't need anyone's attention.) If a sensor is confirmed bad and there's genuinely no healthy replacement nearby, the bot says so rather than silently reporting a broken reading.

Using the bot

Everything below happens by DMing the bot on Telegram — no account, no website, no cost. If you don't have Telegram yet, it's a free app (App Store, Google Play, or web.telegram.org with no download at all) — sign-up takes about a minute with just a phone number.

CommandWhat it does
/startWelcome message and a quick command list.
/locationsList every location currently tracked.
/subscribe <slug>Get alerts for a location, e.g. /subscribe leadville-co.
/addlocation <slug>Register a new town and subscribe to it in one step — finds the nearest active PurpleAir sensor automatically, e.g. /addlocation boulder-co.
/addlocation <slug> <sensor_index> <City, ST>Same, but pointing at a specific sensor by hand — the fallback for when auto-discovery can't find anything (small town, unusual name).
/removelocation <slug>Remove a location — only the chat that added it can.
/unsubscribe <slug>Stop alerts for a location (the location itself stays available for others).
/statusShow every location you're subscribed to and its current AQI.
/reportissue <slug>Think a location's sensor looks wrong? Flag it — see Sensor health checks above.
/documentationLinks back to this page.

A slug is always lowercase and hyphenated, ending in the 2-letter state (or Canadian province) code — boulder-co, salt-lake-city-ut. The bot reads the city and state straight from it, so there's nothing else to fill in for the common case.

You can subscribe to as many locations as you want from one chat — there's no one-location limit. Privacy-wise, the bot stores only your Telegram chat ID and which location(s) you've subscribed to — no name, phone number, or other personal information — and unsubscribing stops messages immediately.

Sources & further reading