Sep 23, 2026 · @Justin
ATsense applies ATproto's architecture to every kind of open sensor: each station owns a portable, signed repo describing what it measures, and open relays, feeds and labelers make seismometers, air monitors, weather stations, cameras and microphones discoverable, streamable and searchable through one protocol.
Today each community runs its own silo. PurpleAir, Sensor.Community, CWOP, Safecast, BirdWeather, OpenSky and the FDSN each have their own identity scheme, API, archive and map. The data shapes differ, but the problems are the same:
- Identity is bound to a platform. A sensor's identity lives in one company's or project's database, and leaves with it.
- Participation is gated by the aggregator. A backyard station needs registration, API keys or a vendor's cloud to be counted.
- No cross-domain discovery. You cannot ask for "every sensor of any kind within 5 km of this wildfire" and subscribe to all of them live.
- Provenance rests on trust in the aggregator, not on signatures and hashes.
- Derived data is siloed. Smoke detections, bird IDs, earthquake picks and QC flags live in separate systems and cannot reference each other.
- One small core vocabulary for systems, deployments, datastreams and observations, with domain modules on top.
- Every data shape (scalar readings, waveforms, images, audio, detections, mobile tracks) fits the same record-plus-blob pattern.
- Any operator, from a national network to one person with a kit, can publish with a portable identity.
- Live data at the latency each domain needs, and verifiable historical access from any mirror.
- Open, pluggable discovery that works across domains.
- Bridges so existing platforms and tools work on day one.
Nine representative open networks span five data shapes, and each one exposes a requirement the seismic-only draft missed.
| Network | Domain | Scale (as reported) | Cadence and shape | Requirement it adds |
|---|---|---|---|---|
| PurpleAir | Air quality | Global, consumer hardware | Scalar readings every 2 min; dual A/B particle counters; indoor/outdoor flag | Redundant sub-sensors, siting context, private sensors, paid central API (pricing) |
| Sensor.Community | Air, noise, weather | >14,000 sensors in >70 countries (2021); ~12,000 active in 82 countries (2024) | Scalar readings every 2.5 min from DIY kits | Heterogeneous hardware: sensor model must be first-class |
| OpenAQ | Air quality aggregator | Reference monitors plus low-cost sensors worldwide | Raw, hourly and daily aggregates | Provider is not owner; licenses change over time; mobile vs stationary; QC flags |
| Raspberry Shake (FDSN AM) | Seismic, infrasound | Thousands of stations since 2016 (data center) | Continuous waveforms, 50–100 sps | High-rate waveforms; already bridged to FDSN; users already "follow" stations |
| CWOP | Weather | 3,000–4,000 routinely reporting (2009) | Scalar readings every 5–15 min via APRS-IS to NOAA MADIS; manual snow reports via CoCoRaHS | Human-entered observations; QC results attached as metadata |
| Safecast | Radiation, air | >120 million observations by 2020, CC0 | bGeigie Nano logs every 5 s with GPS from cars and bikes | Mobile platforms: location per observation, not per station |
| ALERTCalifornia | Wildfire cameras | >1,200 cameras (2026); ~8 million images/day (2023) | Frames every 10–15 s from rotating PTZ cameras; AI smoke detections | Images with a changing view per frame; derived detections; very high blob volume |
| BirdWeather | Bioacoustics | >1,000 active stations | On-device BirdNET detections plus short audio clips; fixed, mobile, livestream and retrieved recorders | Late-arriving batch uploads; edge ML output as primary data; audio privacy |
| OpenSky Network | Aircraft (ADS-B, Mode S) | >7,000 receivers; >40 trillion Mode S replies (2025) | Messages received from aircraft broadcasting up to 2 Hz (ESSD) | Observed object is far from the sensor; many receivers hear the same target |
The closest existing standard is OGC API – Connected Systems, whose Part 2 was approved in June 2025. It models Systems, Deployments, Procedures, Sampling Features, Datastreams and Observations on the W3C SOSA/SSN ontology, and supports live and historical access over HTTP, WebSocket and MQTT. ATsense should adopt that vocabulary rather than invent one: Connected Systems standardizes the API a server exposes, and ATsense adds the identity, signing, federation and discovery layers it lacks.
Six data shapes cover every network above; the lexicon handles them all with one segment record whose payload is either inline values or blob chunks.
| Shape | Examples | Typical rate | Payload in a segment |
|---|---|---|---|
| Scalar series | PurpleAir PM2.5, weather, fixed radiation | 1 per s to 1 per 15 min | Inline columnar arrays (time offsets + values) |
| Waveform | Seismic, infrasound, magnetometer, raw acoustic | 10–48,000 samples/s | Blob chunks (miniSEED, FLAC, or generic compressed array) |
| Media frames | Wildfire cameras, all-sky cameras, timelapse | 1 frame per 10 s to 30 fps | Blob per frame or per video segment, with per-frame view metadata |
| Track | Safecast drives, drones, buoys, balloons | 0.2–10 per s | Inline arrays with a position per sample |
| Detection | Smoke alert, bird ID, lightning stroke, earthquake pick, decoded aircraft position | Sparse | One record per detection, referencing the evidence segment |
| Human observation | CoCoRaHS snow depth, manual gauge reading, field note | Sparse | One record per observation |
- Feature of interest vs sensor location. An ADS-B receiver hears aircraft 300 km away; a camera views a ridge 20 km away. Records separate where the sensor is from what it observes, as SOSA/SSN does.
- Observed properties and units. Properties use a shared vocabulary (CF standard names or QUDT), and units use UCUM codes, so "PM2.5 in µg/m³" means the same thing from every vendor.
- Raw vs derived vs aggregated. A segment says whether values are raw, corrected (e.g. EPA-corrected PurpleAir) or averaged, and links to its inputs.
- Late and batch arrival. Recorders retrieved months later, or stations that were offline, publish segments with past timestamps. Observation time and publish time are separate fields.
- Redundant sub-sensors. PurpleAir's A and B channels are two datastreams on one system, so QC can compare them.
- Privacy class. Audio and images can capture people. Each datastream declares a privacy class that hosts and relays must respect.
The core split holds for every domain: small signed records (descriptions, manifests, detections) travel through repos and the firehose, while bulk payloads (waveform chunks, images, audio) travel as content-addressed blobs and live streams that records point to.
| ATproto concept | ATsense role | Example |
|---|---|---|
| DID + handle | Identity for a system (sensor or station), an operator, or a fleet | did:plc:…, handle pa-1234.purpleair.sense.net |
| PDS | Data host: holds repos, serves blobs and live streams | A university server, a vendor cloud, a home Raspberry Pi |
| Repo | Signed tree of a system's records | System description, deployments, datastreams, segment manifests |
| Blob | Bulk payload addressed by CID | One minute of seismic data, one camera frame, a 3 s audio clip |
| Relay / firehose | Global stream of commits | "New segment", "new detection", "deployment moved" |
| AppView | Aggregating service | Air quality map, earthquake catalog, smoke detector, bird tracker |
| Feed generator | Virtual network across domains | "Every sensor of any kind within 5 km of the Park Fire" |
| Labeler | QC and trust | pm-ab-disagree, indoor-sited, verified-reference-monitor |
| Lexicon | Shared schemas | sense.core.* plus sense.seismic.*, sense.air.*, sense.imagery.* |
flowchart LR
S[Sensors<br/>any domain] -->|readings, frames| H[Data host PDS]
H -->|signed commits| R[Relay firehose]
H -->|live streams| L[Live subscribers]
H -->|blobs by CID| A[Archives / mirrors]
R --> V[AppViews]
R --> F[Feed generators]
R --> B[Labelers]
V -->|detections, QC| H2[Their own repos]
Nothing in the relay or host layer knows about domains. Domain logic lives in lexicon modules and in the AppViews, feeds and labelers built on them.
Every system (a sensor, station or camera) gets its own DID, and operators and fleets get DIDs too; the system's DID is what data, detections and citations point to, so it survives changes of host, owner or network.
- System DID. Resolves to the current data host and signing keys. Rotation keys stay with the owner, so neither a host nor a vendor cloud can capture the system.
- Operator DID. A person or organization. Owns systems via
sense.core.operatesrecords, verified two-way as with ATproto domain handles. - Fleet DID. A network, project or vendor fleet (FDSN network
AM, Sensor.Community, a city's air network). Publishes membership records; a system can belong to several fleets at once. - Existing identifiers. FDSN codes, PurpleAir sensor indexes, CWOP IDs and OpenAQ location IDs are stored as
externalIdson the system record. Registry labelers (FDSN, CWOP) mark which DID legitimately holds each code. - Device keys. Firmware can hold a delegated key that signs segment manifests, revocable without touching history. Vendors can ship devices that generate their DID on first boot and let the owner claim it.
- Mobile and ephemeral systems. A Safecast unit keeps one DID across drives; each drive is a deployment. A drone or balloon flight is a deployment of a persistent system.
- Human observers. A person's ordinary ATproto account can publish human observations directly, so a CoCoRaHS volunteer or birder needs no separate system identity.
A small sense.core.* namespace, mapped one-to-one onto OGC Connected Systems concepts, carries everything relays, feeds and archives need; domain modules plug in through open unions and never change the core.
| Lexicon | Connected Systems equivalent | Purpose |
|---|---|---|
sense.core.system |
System | What the device is: type, maker, model, external IDs, default license and privacy class |
sense.core.deployment |
Deployment | Where and when: location or "mobile", siting (indoor/outdoor, height), feature of interest |
sense.core.datastream |
DataStream | One observed property: unit, shape, nominal rate, encoding, live stream URI, domain extension |
sense.core.segment |
Batch of Observations | A time window of data: inline values or blob chunks, processing level, gaps |
sense.core.detection |
Observation (classification result) | A derived event pointing to its evidence segments |
sense.core.observation |
Observation | A single human-entered reading |
sense.core.annotation |
none | Free-form note on a time window of any datastream |
sense.fleet.member |
none | Fleet membership, confirmed by a matching system record |
sense.feed.generator |
none | Declares a feed service |
sense.graph.follow, sense.graph.endorse |
none | Social layer |
| Module | Extends | Adds |
|---|---|---|
sense.seismic.channel |
datastream | FDSN NET.STA.LOC.CHA, orientation, instrument response ref |
sense.seismic.pick, sense.seismic.origin |
detection | Phase, polarity, hypocenter, magnitude |
sense.air.particle |
datastream | Sensor channel (A/B), size bin, correction applied |
sense.weather.station |
system | Siting class, exposure, shielding |
sense.imagery.frameView |
segment | Per-frame pan, tilt, zoom, heading, field of view |
sense.imagery.smoke |
detection | Bounding box, estimated bearing and range |
sense.acoustic.species |
detection | Taxon ID, clip reference, model and threshold |
sense.radiation.doseRate |
datastream | Detector tube type, energy calibration |
sense.aviation.target |
detection | ICAO24 address, target position and altitude, receivers that heard it |
ATproto Lexicon has no float type. ATsense uses one fixed-point rule everywhere: values are integers plus a scale exponent, so value × 10^scale is the reading. PM2.5 of 12.4 µg/m³ is 124 with scale: -1; a latitude is 37774900 with scale: -6.
{
"lexicon": 1,
"id": "sense.core.datastream",
"defs": {
"main": {
"type": "record",
"key": "any",
"record": {
"type": "object",
"required": ["system", "observedProperty", "unit", "shape"],
"properties": {
"system": { "type": "string", "format": "did" },
"name": { "type": "string", "maxLength": 200 },
"observedProperty": { "type": "string", "format": "uri", "description": "CF standard name or QUDT quantity kind URI" },
"unit": { "type": "string", "description": "UCUM code, e.g. ug/m3, m/s, Cel" },
"shape": { "type": "string", "knownValues": ["scalar", "waveform", "media", "track"] },
"nominalRate": { "type": "ref", "ref": "#scaledInt" },
"encoding": { "type": "string", "knownValues": ["inline", "miniseed3", "flac", "jpeg", "mp4", "parquet"] },
"liveStream": { "type": "string", "format": "uri" },
"privacyClass": { "type": "string", "knownValues": ["public", "public-redacted", "restricted"] },
"deployment": { "type": "ref", "ref": "com.atproto.repo.strongRef" },
"ext": {
"type": "union",
"refs": ["sense.seismic.channel", "sense.air.particle", "sense.radiation.doseRate"]
}
}
}
},
"scaledInt": {
"type": "object",
"required": ["value", "scale"],
"properties": {
"value": { "type": "integer" },
"scale": { "type": "integer", "minimum": -12, "maximum": 12 }
}
}
}
}The ext union is open, so new domain modules can appear without a core release; clients ignore extensions they do not know.
Record key convention: <datastream key>.<window start>, e.g. pm25-a.2026-09-23T14.
{
"lexicon": 1,
"id": "sense.core.segment",
"defs": {
"main": {
"type": "record",
"key": "any",
"record": {
"type": "object",
"required": ["datastream", "start", "end", "level", "payload"],
"properties": {
"datastream": { "type": "ref", "ref": "com.atproto.repo.strongRef" },
"deployment": { "type": "ref", "ref": "com.atproto.repo.strongRef" },
"start": { "type": "string", "format": "datetime" },
"end": { "type": "string", "format": "datetime" },
"level": { "type": "string", "knownValues": ["raw", "corrected", "aggregated"] },
"aggregation": { "type": "string", "knownValues": ["mean", "median", "max", "min", "sum"] },
"derivedFrom": { "type": "array", "items": { "type": "ref", "ref": "com.atproto.repo.strongRef" } },
"payload": { "type": "union", "refs": ["#inlineValues", "#chunks"], "closed": true },
"gaps": { "type": "array", "items": { "type": "ref", "ref": "#timeRange" } },
"clockQuality": { "type": "integer", "minimum": 0, "maximum": 100 },
"ext": { "type": "union", "refs": ["sense.imagery.frameView"] }
}
}
},
"inlineValues": {
"type": "object",
"description": "Columnar arrays for scalar and track data",
"required": ["offsetsMs", "values", "scale"],
"properties": {
"offsetsMs": { "type": "array", "items": { "type": "integer" }, "maxLength": 4000 },
"values": { "type": "array", "items": { "type": "integer" }, "maxLength": 4000 },
"scale": { "type": "integer" },
"latMicroDeg": { "type": "array", "items": { "type": "integer" } },
"lonMicroDeg": { "type": "array", "items": { "type": "integer" } }
}
},
"chunks": {
"type": "object",
"required": ["items"],
"properties": {
"items": { "type": "array", "items": { "type": "ref", "ref": "#chunk" } }
}
},
"chunk": {
"type": "object",
"required": ["blob", "start", "end"],
"properties": {
"blob": { "type": "blob" },
"start": { "type": "string", "format": "datetime" },
"end": { "type": "string", "format": "datetime" }
}
},
"timeRange": {
"type": "object",
"required": ["start", "end"],
"properties": {
"start": { "type": "string", "format": "datetime" },
"end": { "type": "string", "format": "datetime" }
}
}
}
}A PurpleAir hour is one segment with 30 inline values per channel. A seismic hour is one segment listing 60 miniSEED chunks. A Safecast drive is one segment with inline dose rates plus latitude and longitude arrays. Each record's commit time is its publish time, so late uploads are visible as such.
{
"lexicon": 1,
"id": "sense.core.detection",
"defs": {
"main": {
"type": "record",
"key": "tid",
"record": {
"type": "object",
"required": ["evidence", "time", "class", "method"],
"properties": {
"evidence": { "type": "array", "items": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, "minLength": 1 },
"time": { "type": "string", "format": "datetime" },
"class": { "type": "string", "description": "Namespaced label, e.g. seismic:P, smoke, taxon:gbif:2490384" },
"confidencePct": { "type": "integer", "minimum": 0, "maximum": 100 },
"method": { "type": "string", "knownValues": ["manual", "automatic"] },
"model": { "type": "string" },
"ext": {
"type": "union",
"refs": ["sense.seismic.pick", "sense.imagery.smoke", "sense.acoustic.species", "sense.aviation.target"]
}
}
}
}
}
}Because evidence accepts several segments from several systems, one detection can cite a camera frame, a nearby PM2.5 spike and a weather station's wind shift together. That cross-domain link is the thing no existing network can express.
Each data shape gets its own commit cadence and live transport, but all share one rule: the firehose carries signed records only, and bulk payloads move over live streams and blob fetches.
| Shape | Live stream | Commit cadence to firehose | Fast path |
|---|---|---|---|
| Scalar series | Each reading pushed as it is taken | Segment appended every 10 min | Not needed |
| Waveform | Packets every 0.5–1 s | Chunk appended every 60 s | < 1 s for earthquake early warning |
| Media frames | Latest frame or video segment URI | Frames batched every 60 s | Latest-frame endpoint for responders |
| Track | Positions pushed as taken | Segment appended every 1–10 min | Not needed |
| Detection | Not applicable | Committed immediately | Detections are small, so the firehose is the fast path |
Live packets carry a sequence number and the previous packet's hash. When the host commits the segment update, it signs over the chunk CIDs or inline values. Subscribers verify their buffered live data against the commit, so tampering shows up within one commit interval.
| Source | Assumed count | Commits per second |
|---|---|---|
| Air and weather datastreams | 300,000 | ~500 |
| Seismic channels | 50,000 | ~830 |
| Cameras | 5,000 | ~80 |
| Detections | varies | tens to hundreds |
About 1,500 commits per second in total, in the same range as Bluesky's firehose. Blobs stay off it: at ALERTCalifornia's ~8 million images a day and an assumed 500 KB per image, one camera network alone produces about 370 Mbit/s.
- Per datastream: connect to its
liveStreamURI. - Per feed: a live-stream relay resolves a feed to its datastreams and multiplexes them over one connection, tracking changes as systems join or leave the feed.
- Replay: every subscription takes a cursor, so clients resume after disconnects.
History for every shape is the same: signed segment records are the index, and content-addressed blobs are the bulk, so any mirror can serve data and any client can verify it.
- Segments are the index. One segment per datastream per hour gives a compact, signed map of coverage and gaps. Scalar data is fully contained in its segments, so an air-quality archive is just the repos.
- Blobs are immutable. Corrections produce new segments with
level: correctedandderivedFrompointing to the raw ones; raw data is never overwritten. - Tiered retention. Hosts can drop old raw imagery or audio after publishing aggregated or detection records, as long as an archive has pinned it. A segment whose blobs are no longer hosted anywhere is marked by an archive labeler, not silently lost.
- Domain archives. EarthScope for seismic, OpenAQ for air quality, NOAA for weather: each becomes a full-history archive node for its domain, serving the ATproto sync API plus blob-by-CID.
- Backfill and late data. Archives replay repos from cursors. Late uploads (a retrieved acoustic recorder, an offline station) land as normal commits with past observation times.
sequenceDiagram
participant C as Client
participant I as Index AppView
participant M as Mirror or archive
C->>I: area + time + properties
I-->>C: datastreams + segments
C->>M: fetch blobs by CID
M-->>C: chunks, frames, clips
C->>C: verify hashes + signatures
A query such as "PM2.5, wind and camera frames within 10 km of this fire, 6 hours before ignition" returns scalar segments directly from the index and fetches only the frames the client asks for.
Discovery works across domains because feeds, labelers and follows all operate on core records; a feed can mix seismometers, cameras and air monitors as easily as it lists one kind.
A feed service returns datastreams, detections or segments on request, declared by a sense.feed.generator record. Its core call, sense.feed.getSkeleton, returns record refs plus a cursor, like Bluesky's getFeedSkeleton.
- Incident: every sensor of any kind within 10 km of an active wildfire, updating as the perimeter moves.
- Property: all outdoor PM2.5 datastreams in Los Angeles County passing the A/B agreement check.
- Event: every seismic channel and every building-mounted camera that recorded a given earthquake.
- Species: acoustic stations that detected a given owl this week.
- Community: all sensors in a school district, all Raspberry Shakes in schools.
A feed plus a time range plus repo revisions is a reproducible dataset, citable with a DOI.
| Label | Applies to | Likely labeler |
|---|---|---|
pm-ab-disagree |
Air datastream | Air QC service comparing PurpleAir A and B channels |
indoor-sited |
Deployment | Community or vendor labeler |
verified-reference-monitor |
System | Environmental agency |
clock-drift |
Segment | Timing QC service |
verified-fdsn-code |
System | FDSN registry |
faces-blurred |
Media segment | Privacy labeler |
ml-false-positive |
Detection | Human review service |
Users stack the labelers they trust. A researcher can ask for "outdoor PM2.5 with no pm-ab-disagree label from my two chosen QC labelers", across every vendor at once.
- Follows: follow a system, a fleet, a feed, a person or a model. Your home view shows new detections, annotations and notable readings.
- Threads: detections and annotations cite segments, so each wildfire, earthquake or smoke event becomes a thread of machine and human evidence from many domains.
- Endorsements: "I used this data" signals roll up into usage metrics that operators can show funders.
Adoption depends on bridges in both directions: ingest bridges bring existing networks in with their consent, and facades let existing tools read ATsense without changes.
| Bridge | Direction | Notes |
|---|---|---|
| OGC Connected Systems facade | ATsense → CS API | The main standards-facing view; core lexicons map one-to-one |
| OGC SensorThings facade | ATsense → STA | For existing SensorThings clients and dashboards |
| FDSN web services + SeedLink | Both ways | Seismic: ObsPy, SeisComP and Earthworm work unchanged |
| PurpleAir | PurpleAir → ATsense | Owner-authorized ingest; PurpleAir's per-call API pricing makes owner opt-in the practical route |
| Sensor.Community | Sensor.Community → ATsense | Open data; kits could also push to a native host directly |
| OpenAQ | Both ways | OpenAQ becomes an air-quality AppView and archive; its provider/owner/license model maps onto operator, fleet and license fields |
| CWOP / APRS-IS / MADIS | Both ways | Weather stations publish once and still reach NOAA |
| Safecast API | Safecast → ATsense | CC0 data; drives become track segments |
| BirdWeather | BirdWeather → ATsense | Detections become sense.acoustic.species records |
| MQTT | Device → host | Most hardware already speaks MQTT; the reference host accepts it |
- Core lexicons and reference host, with MQTT, SeedLink and Connected Systems support.
- Mirror open data from Sensor.Community, Safecast, Raspberry Shake and OpenAQ, so discovery works on real data immediately.
- Native publishing for kits: firmware for Sensor.Community-style kits and Raspberry Shake that publishes straight to a host.
- Operators claim keys from bridges and optionally self-host.
- Cross-domain AppViews and feeds, starting with a wildfire incident feed that combines cameras, air quality and weather.
The biggest open questions are privacy for cameras and microphones, restricted data, archive funding and vocabulary governance; the rest are engineering.
- Restricted and embargoed data. ATproto repos are public by design. Options: publish manifests publicly but encrypt chunks with per-network keys, or add a private record namespace served only to authorized DIDs.
- Who pays for archives. Pinning every chunk forever costs real money. Likely answer: institutional archive nodes as today, plus pinning agreements tied to citations and endorsements.
- Fast-path trust. Sub-second early warning cannot wait for a commit. Should EEW stay on dedicated regional systems, bridged in, rather than on this protocol at all?
- Numeric types. The draft picks integer-plus-scale everywhere. Open: whether waveform chunks that store floats natively (some digitizers, audio) should be exempt, since blobs are opaque to Lexicon anyway.
- Manifest granularity. Hourly segments are a guess. Slow scalar streams may want daily segments to cut record count; video may want per-minute segments.
- Metadata and calibration quality. Signing wrong metadata makes it verifiably wrong, and low-cost air sensors need correction factors to be comparable. Labelers help, but validation tools and published corrections should ship with the reference host.
- Spam and junk sensors. Open participation invites fake or broken stations. Labelers and feed curation handle this the way Bluesky does, but default labelers need committed operators.
- Privacy for cameras and microphones. Images and audio can capture people and homes. Options: on-device redaction before publishing, a
public-redactedclass enforced by hosts, and detections-only publishing with raw media kept private. - Vocabulary governance. Someone must steward observed-property and class vocabularies. Leaning on CF, QUDT, UCUM and GBIF taxon IDs avoids inventing new ones, but a registry of
sense.*modules still needs an owner. - Vendor incentives. PurpleAir and similar vendors earn money from their APIs. Owner-controlled publishing may conflict with that model, so bridges should be owner opt-in.
- Detection floods. Always-on ML models can emit millions of low-confidence detections. Hosts may need rate limits or confidence floors for the firehose.
- Finalize the
sense.core.*lexicons and publish the Connected Systems mapping - Write the first three domain modules: seismic, air particle, imagery
- Build a reference host with MQTT, SeedLink and Connected Systems support
- Stand up one relay and one index AppView
- Pilot with Sensor.Community, Raspberry Shake and one camera network in the same region
- Ship a cross-domain wildfire feed and an air A/B QC labeler to prove discovery