o2.agent9.dev — live, free, no login.
Real capture from the live site: modeled smoke advancing through forecast time. Recorded during the July 2026 Canadian wildfire event.
Every air-quality tool shows you a number and a colored map. During a wildfire smoke event, that is not the question people actually have.
The questions are:
| What people ask | What most tools show | What O2 answers |
|---|---|---|
| When does it get better? | today's AQI | "Improvement expected around Tue, Jul 21, 10 PM." |
| When does it get worse? | a colored map | the deterioration hour, before it arrives |
| Is it safe to go out — for me? | one generic sentence | separate guidance for everyone and for sensitive groups |
| Is there an alert near me? | a national alert list | NWS alerts filtered to your coordinates |
| Can I watch the smoke move? | a static overlay | a 48-hour scrubber over modeled smoke |
| Can it tell me when it changes? | refresh the page | an email when your air crosses a threshold |
Smoke is a timing problem. If you know the air clears at 10 PM, you move the run, close the windows now, and open them later. O2 is built to answer the timing question first, and everything else second.
Open it and the answer is already there. No login, no account, no setup. It loads with Washington, D.C. so there is never an empty screen, and one tap switches to your location.
- The O2 Air Window — a plain-language headline: when the air improves, the cleanest window in the loaded forecast, and how confident the model is.
- Live smoke map — modeled surface smoke over a 48-hour scrubber, with active fire perimeters and incidents. Play it and watch the plume move across forecast time.
- Two smoke models, separable — the US NWS NDGD forecast (amber, on by default) and the Canadian BlueSky/FireSmoke model (cyan, opt-in). Show either alone, or both, and see where two independent models agree.
- Spoken briefing — a generated broadcast read aloud by Deepgram Aura on Workers AI, with the full transcript always available.
- Email alerts — double opt-in, per-subscriber AQI threshold, alerts when the air turns unhealthy and when it is safe again, plus optional morning and evening briefings. One-tap unsubscribe in every message.
- How to stay safe — all six AQI categories with the guidance O2 applies to each, your current band marked, plus practical steps during a smoke event.
- Answers to the confusing part — an FAQ that explains why the map can show smoke overhead while your AQI reads Moderate. (Smoke aloft has not mixed to the surface yet. It often does later in the day.)
The answer is computed, not generated. No language model writes any of the words that matter. AQI categories, the improvement time, the safest window, and every piece of health guidance come from fixed rules in the code. The same reading always produces the same advice.
That is a deliberate constraint. Air quality guidance is safety information, and a model that occasionally invents a reassuring sentence is worse than no tool at all. The one place AI runs at request time is text-to-speech — it reads a script the deterministic layer already wrote. It does not choose what the script says.
It says when it does not know. Confidence drops and O2 says why, rather than stating a precise hour it cannot support.
It only claims smoke it can see. The spoken briefing mentions wildfire smoke only when a modeled plume actually covers your coordinates, or sits within ~150 km — in which case it names the distance and direction. Ask it about a location with no plume nearby and it says nothing about smoke at all.
O2 was designed with GPT-5.6 and implemented with Codex.
GPT-5.6 (design). The product architecture was worked out in a long GPT-5.6 session before any code was written: positioning, feature scope, three Mermaid architecture diagrams, the alerts and TTS plans, deterministic safety boundaries, the implementation sequence, and the README structure. That session produced a handoff document that Codex then built from.
Codex (implementation). Codex wrote the codebase: the Cloudflare/TanStack
foundation, the smoke-intelligence layer, the forecast engine
(src/lib/forecast-intelligence.ts), the deterministic report and broadcast
generator (src/lib/local-report.ts), the Workers AI Aura integration
(src/server.ts), the AQI rules (src/lib/aq.ts), and the alerts pipeline.
Codex on GPT-5.6 later did the local-smoke-evidence work
(src/lib/local-smoke.ts) — point-in-polygon, haversine and bearing math that
gates the spoken smoke line on plumes near the user's own coordinates — plus the
collapsible answer panel and the first-run location hint.
A representative agentic loop: Codex read the plan, reconciled it against the actual repo state, corrected its own terminology when it drifted from the project's, installed Playwright, wrote the evidence-capture tooling, ran the test suite, and committed — then flagged what it could not verify itself.
flowchart TB
subgraph edge["Cloudflare Worker · edge"]
SSR["TanStack Start SSR"]
FN["Server functions<br/>air quality · fires · smoke · briefing"]
TTS["/api/o2/broadcast-audio<br/>Workers AI · Deepgram Aura"]
CRON["Cron · every 15 min<br/>alert engine"]
end
subgraph deterministic["Deterministic core · no LLM"]
AQ["aq.ts<br/>AQI categories + guidance"]
FI["forecast-intelligence.ts<br/>improvement · deterioration<br/>safest window · confidence"]
LS["local-smoke.ts<br/>plume proximity gating"]
LR["local-report.ts<br/>report + broadcast script"]
end
subgraph sources["External sources"]
OM["Open-Meteo · CAMS"]
NIFC["NIFC WFIGS"]
NDGD["NWS NDGD smoke"]
FS["BlueSky / FireSmoke"]
NOAA["NOAA HMS + NWS alerts"]
end
D1[("D1 · o2-alerts<br/>subscriptions · deliveries")]
MAIL["Resend<br/>alert@updates.agent9.dev"]
sources --> FN --> deterministic --> SSR
LR --> TTS
CRON --> FI
CRON --> D1
CRON --> MAIL
SSR --> UI["Browser · Mapbox GL"]
Stack: TanStack Start · React 19 · TypeScript · Tailwind v4 · Mapbox GL · Cloudflare Workers, D1, Workers AI · Resend · Turnstile.
Everything runs on Cloudflare's free tier. There is no origin server.
| Source | Provides | Class |
|---|---|---|
| Open-Meteo (CAMS) | PM2.5, PM10, O₃, hourly + 48h forecast | Modeled |
| NIFC WFIGS | Active fire perimeters and incidents | Measured |
| NWS NDGD | Hourly North America surface-smoke forecast | Modeled |
| BlueSky / FireSmoke (UBC) | Canadian wildfire smoke model (opt-in overlay) | Modeled |
| NOAA HMS | Smoke narrative, North America overview | Observed |
| NWS alerts | Active alerts filtered to your coordinates | Official |
| Mapbox / OpenStreetMap | Basemap | — |
Every air-quality, fire, and smoke source is listed in-app under Data sources with a link and a measured/modeled label.
How the improvement time is calculated
- Build an hourly series from the loaded forecast for the selected location.
- Convert PM2.5 to US AQI using EPA breakpoints (
src/lib/aq.ts). - Walk forward to the first hour that crosses into a better AQI category and hold it — that is the improvement time.
- Independently find the lowest-AQI hour in the loaded window — the safest window, which is often later than the moment things start improving.
- Score confidence from how much usable forecast coverage exists and how clearly it supports a timing estimate. Thin or ambiguous coverage lowers confidence and O2 states the reason.
Forecasts are estimates and get less certain further out. Wind shifts and new fires move the timing. O2 presents it as planning guidance, never a guarantee.
How the smoke line is gated to your location
src/lib/local-smoke.ts takes the user's coordinates, the modeled smoke
polygons, and current PM2.5, and returns one of three tiers:
- over — a plume at or above the significance threshold contains the point (ray-casting point-in-polygon, with interior-ring/hole handling).
- nearby — the nearest qualifying plume centroid is within ~150 km (haversine), reported with an 8-point compass bearing.
- none — nothing qualifying nearby, and O2 says nothing about smoke.
This replaced an earlier approach that keyword-matched a continental narrative and consequently implied wildfire smoke everywhere on the continent.
These are enforced in code, not by convention:
- No language model writes user-facing copy. Forecast wording, the broadcast script, and map answers are templates filled by the deterministic layer.
- Health guidance has one source. Every category label, range, and piece of
advice is read from
src/lib/aq.ts. Tests assert the on-page safety table matches the classifier at a probe value inside every band, so the two cannot drift. - Observed and forecast are always distinguished in the timeline and copy.
- Fallbacks are labeled, never hidden. If Aura is unavailable the transcript is shown; if a source is stale its timestamp says so.
- No automatic social posting, and no medical advice. O2 states plainly that it provides general information only.
Live, and verified end to end on production infrastructure.
subscribe → confirmation email → tap link → active in D1
↓
cron (*/15) evaluates each subscriber
↓
category crossing? → dedupe + cooldown → Resend → inbox
- Double opt-in, protected by Cloudflare Turnstile. Nothing is sent before you confirm.
- Per-subscriber AQI threshold; separate toggles for turns unhealthy, safe again, and morning/evening briefings.
- Boundary hysteresis and a cooldown prevent alert storms at a category edge.
- Idempotent delivery logging in D1: every send is recorded
sent,queued, orfailedwith a reason. - With no API key configured the adapter records the message as
queuedand contacts no provider — a real record, never a fake success. - One-tap unsubscribe in every message.
Stored per subscription: email, latitude, longitude, a label for the place, and its timezone. That is what makes a local alert possible. Nothing is sold.
Verified in a browser on 2026-07-20. These four checks are the full extent of what was tested — no aggregate conformance claim is made.
| Check | Result |
|---|---|
| Category conveyed as text beside color, everywhere AQI appears | Pass |
| Current reading, category, and improvement time reachable as text | Pass |
| Timeline, map questions, alert signup, and FAQ keyboard-operable with a visible focus ring | Pass (one missing focus ring found and fixed) |
prefers-reduced-motion honored |
Pass |
npm install
npm run devThe map needs a Mapbox token:
VITE_MAPBOX_PUBLIC_TOKEN=pk.your_token_hereEmail alerts use a Resend key held as a Worker secret, never in the repo:
npx wrangler secret put RESEND_API_KEY --config wrangler.jsoncWith no key configured the pipeline records deliveries as queued and contacts
no provider, so you can exercise the whole flow without sending mail.
Note on local dev: Workers AI is proxied differently under
wrangler dev, so the Aura voice falls back to browser speech onlocalhost. On the deployed Worker the real voice is used. Alert emails also require the deployed secret.
npm run test:intelligence # 60 tests
npm run build
npm run lintThe suite covers AQI boundary categories, forecast improvement/deterioration detection, safest-window selection, confidence scoring, report and broadcast generation, smoke proximity gating (over / nearby / none, plus malformed plume values), alert engine crossings and cooldowns, subscription lifecycle, email adapter behavior with and without a key, and content integrity — including tests that fail if the FAQ ever reintroduces a claim the product cannot support.
npm run deployCloudflare Worker aqi-agent9 on the custom domain o2.agent9.dev, with a D1
binding (o2-alerts), the Workers AI binding, static assets, and a 15-minute
cron trigger.
- Forecast accuracy degrades with distance in time; timing can move by hours.
- Modeled data is not a substitute for a nearby regulatory monitor. For official readings see AirNow.gov.
- Smoke proximity uses plume centroids for the "nearby" tier, so distance is approximate for very large or irregular plumes.
- Source-disagreement detection (flagging when two models conflict) is not built yet; both models are shown so the difference is at least visible.
- O2 is an independent tool, not affiliated with the EPA, NOAA, or any agency.
- Text (SMS) alerts — the same "your air just crossed a line" notifications, delivered by text as well as email.
- Global coverage — O2 already runs on the worldwide CAMS model, so forecasting air quality beyond North America is the natural next step.
- Multi-location subscriptions — watch home, work, and a loved one's town from a single account.
- Sharper confidence — per-source freshness timestamps and source-disagreement penalties, so you can see exactly how sure each forecast is.
Licensed under the Apache License 2.0. © 2026 Agent9.
o2.agent9.dev · Built by Agent9



