An air quality intelligence construct for the Echelon prediction market framework, built on constructs by Soju. Ridden by Loa.
BREATH ingests real-time air quality data from PurpleAir's 30,000+ sensor network and EPA AirNow federal monitoring stations, runs prediction markets on AQI outcomes across configurable time windows, and exports Brier-scored RLMF training certificates. It is the third construct in the TREMOR → CORONA lineage, adapting the proven seismic prediction market architecture to the air quality domain.
- Ground truth oracle — EPA-reviewed AQI values are published hourly. No interpretation required:
category_number >= threshold_category_numbercloses the market. - Binary structure — EPA AQI category breakpoints (51, 101, 151, 201, 301) create natural threshold gates. Every question has a crisp YES/NO resolution.
- Fast cycles — AQI updates hourly. Theatres resolve in 4–72 hours. High-frequency RLMF data generation.
- Exogenous — Predictions do not affect air quality. No reflexivity. Clean RLMF signal.
- Dense network — 30,000+ PurpleAir sensors enable sensor-to-sensor corroboration. Localized phenomena (divergence, dropout) are detectable before EPA stations report.
import { BreathConstruct } from './src/index.js';
const bc = new BreathConstruct({
apiKeys: {
purpleair: process.env.PURPLEAIR_API_KEY,
airnow: process.env.AIRNOW_API_KEY,
},
purpleair: {
bboxes: [{ nwlng: -123.0, nwlat: 38.0, selng: -122.0, selat: 37.5, label: 'sf-bay' }],
},
airNow: {
regions: [{ lat: 37.77, lon: -122.41, radius_miles: 25, label: 'sf-bay' }],
},
});
// Open a prediction market
bc.openAqiThresholdGate({
region_name: 'San Francisco Bay',
region_bbox: [-123.0, 37.5, -122.0, 38.0],
aqi_threshold: 151, // Will AQI reach Unhealthy (>=151)?
window_hours: 24,
});
bc.start(); // Begin polling (PurpleAir every 2m, AirNow every 60m)
const state = bc.getState(); // { construct, running, stats, theatres }
const certs = bc.getCertificates(); // RLMF training certificates
bc.stop();src/
index.js BreathConstruct, SensorRegistry
processor/
aqi.js NowCast, breakpoints, AQI categories
quality.js Channel consistency, freshness, density scoring
uncertainty.js Doubt pricing, threshold crossing probability
settlement.js Evidence class: provisional -> ground_truth
bundles.js EvidenceBundle assembly
oracles/
purpleair.js PurpleAir API v1 (120s cadence)
epa-airnow.js EPA AirNow API (60m cadence)
theatres/
aqi-gate.js T1: AQI Threshold Gate
sensor-divergence.js T2: Sensor Divergence
wildfire-cascade.js T3: Wildfire Cascade
rlmf/
certificates.js Brier scoring, certificate export
spec/
construct.json Constructs Network spec
BREATH uses a two-tier oracle model:
| Tier | Source | Cadence | Role |
|---|---|---|---|
| Signal layer | PurpleAir | 120s | Early warning — updates Theatre position probability |
| Settlement authority | EPA AirNow | 60m | Ground truth — resolves Theatre YES/NO |
PurpleAir bundles never resolve a Theatre directly; they blend position toward thresholdCrossingProbability. EPA AirNow bundles resolve immediately based on category_number >= threshold_category_number.
AirNow bundles are always processed before PurpleAir bundles in each poll cycle to ensure settlement signals resolve markets before signal-layer updates can affect post-resolution positions.
| Template | Type | Question format | Window | Resolution |
|---|---|---|---|---|
aqi_threshold_gate |
Binary | Will AQI reach category X in region R? | 4–72h | EPA AirNow confirmation or expiry |
sensor_divergence |
Binary | Will sensors A and B diverge by >N AQI for K consecutive readings? | 4–24h | Self-resolving (consecutive counter) or expiry |
wildfire_cascade |
Multi-class (5 buckets) | What fraction of sensors will exceed AQI 200? | Up to 72h | resolveWildfireCascade at window close |
Wildfire cascade buckets: 0–10% | 10–30% | 30–50% | 50–70% | 70%+
EPA AirNow hourly delay — AirNow publishes observations on the hour, not in real-time. A theatre that should resolve at 14:00 may not see the confirming bundle until 14:05–14:15. closes_at should account for this lag; using window_hours >= 1 is always safe.
PurpleAir A/B divergence — PurpleAir sensors have two independent measurement channels. When |pm25_a - pm25_b| / avg > 0.7, the bundle is classified channel_inconsistent and does not update Theatre position. This prevents degraded sensors from corrupting market prices.
Sensor dropout — Sensors absent from the PurpleAir API response for > 2× poll cadence are flagged dropout in SensorRegistry. Dropout bundles have evidence_class: 'sensor_dropout' and are not forwarded to Theatres.
AQI breakpoint discontinuities — EPA breakpoint tables have a gap at category boundaries (e.g., PM2.5 12.0 = AQI 50, PM2.5 12.1 = AQI 51). BREATH uses <= on Chigh per the official formula. A PM2.5 of exactly 12.0 maps to AQI 50 (Good), not 51 (Moderate). Values are truncated, not rounded, per EPA specification.
Wildfire smoke transport lag — Smoke plumes may take 2–12 hours to reach downwind sensors after ignition. T3 window_hours should be set generously (24–72h) to capture the full cascade arrival. The uniform prior [0.2, 0.2, 0.2, 0.2, 0.2] correctly reflects initial ignorance; bucket probabilities converge as sensors report.
Zero. Node.js 20+ only.
AGPL-3.0