ATProto Observability Facts API

Public, anonymous, read-only aggregate facts about the ATProto network, published with the coverage, freshness and observation adequacy that qualify them.

publicanonymous read-onlyno API key CORS enabled

What this is

Several independent instruments watch the ATProto network and reduce what they see into aggregate facts. This API publishes those facts. It is a thin publication layer: it reads artifacts the instruments have already generated and serves them under a versioned contract. It does not run queries against any instrument's operational database.

The distinguishing property is that every fact carries the limits of the observation that produced it. How much of the estate was actually watched, how fresh the reading is, and whether the observation had enough standing to support a negative claim all travel with the data rather than being lost at the boundary. If an instrument could not see something, you are told that, instead of receiving a confident zero.

Quick start

curl -sS -i https://api.neutral.zone/atproto/v1/instruments
curl -sS -i https://api.neutral.zone/atproto/v1/labelwatch/overview
curl -sS -i https://api.neutral.zone/atproto/v1/weatherwatch/conditions
curl -sS -i https://api.neutral.zone/atproto/v1/weatherwatch/metrics

Inspect the HTTP status before using the JSON. No key or query parameters are needed. The instrument index reports readability; PRESENT does not guarantee that Labelwatch can serve a current reading. Check its freshness_state, then fetch the endpoint.

Artifact-backed fact responses carry ETag and Last-Modified. Copy the ETag exactly, including its quotes, into If-None-Match on a later request. A matching validator returns 304 with no body; changed age buckets can produce a new validator even with unchanged artifact bytes. The index has no validators. If-Modified-Since is not evaluated by this service.

curl -sS -i -H 'If-None-Match: "COPY_ETAG_FROM_RESPONSE"' \
  https://api.neutral.zone/atproto/v1/labelwatch/overview

Contributing instruments

InstrumentObservesPublished here
Weatherwatch Aggregate network activity from a Jetstream vantage: event rates by collection, and the health of the observation itself. Yes
Labelwatch Observed label application and removal events in a rolling epoch, with acquisition time, source coverage and collection freshness. Yes
Driftwatch Claim propagation, per subject. No. Its observations are per-subject and are deliberately private. See what is not published.

Endpoints

PathReturns
GET /atproto/v1/instruments Which instruments contribute and whether each is readable right now.
GET /atproto/v1/labelwatch/overview Labeler activity in the current rolling 30-day epoch: acquisition time, window coverage (warm-up and recorded gaps with reasons), event and labeler counts, and the most active labelers over 7 days. Served only while fresh or warming up; otherwise 503.
GET /atproto/v1/weatherwatch/conditions Plain-language reading of current aggregate activity, with freshness and interval coverage.
GET /atproto/v1/weatherwatch/metrics Per-collection event totals and mean rates, with observed and unobserved window counts.
GET /atproto/openapi.json Machine-readable contract (OpenAPI 3.1).
GET /atproto/health Liveness of this publication service. Says nothing about the instruments.

GET and HEAD retrieve resources; HEAD omits the body. OPTIONS returns 204 for CORS preflight. POST, PUT, PATCH and DELETE return 405. There are no write, admin, or per-subject routes, and there is nothing to authenticate as.

The response envelope

The three instrument fact endpoints share this outer shape. The instruments index instead contains contract, retrieved_at and instruments.

FieldMeaning
contract Stable identifier for the fact contract and its major version, e.g. atproto.facts.labelwatch.overview/v1. Pin against this.
instrumentWhich instrument produced the facts.
generated_at When the instrument produced the underlying artifact. This is the timestamp that matters for how current the facts are.
retrieved_at When this service read that artifact to answer you. Always recent; not a measure of data freshness.
age_seconds retrieved_at minus generated_at. How stale the facts are, in one number.
factsThe observations themselves.
caveats Limits that are part of the fact, not legal boilerplate. They state what the instrument structurally cannot see.
provenance How the facts were obtained, and an explicit does_not_measure list where the instrument publishes one.
freshness Present on Weatherwatch and Labelwatch responses: the freshness state and the threshold it was judged against. Labelwatch states are fresh, warming_up (served), and stale, held, unavailable (refused with 503).

Time and window semantics

All timestamps are UTC, ISO 8601. Three different times appear and they are not interchangeable:

Windows are preserved exactly as the instrument produced them and are never re-cut at serialization. Weatherwatch rates divide by observed duration, not nominal window width; unobserved windows are omitted from rates rather than interpolated across.

Freshness, coverage and caveats

For Labelwatch, both artifact publication and accepted collection must be current. freshness.age_seconds measures artifact age; collection_age_seconds measures age of facts.coverage.observed_through. The default bounds are 7200 seconds, but use the response's max_age_seconds and collection_max_age_seconds. basis identifies the clock responsible for a refusal. A future timestamp more than 300 seconds ahead or an unusable collection timestamp is unavailable.

fresh and warming_up permit a response; neither establishes complete coverage. Read facts.coverage.status, observed_days, warmup, gaps and sources. No recorded gaps does not prove complete source coverage. Rolling-epoch alert counts are null because no alert pipeline runs there, and current_label_state is unknown. Weatherwatch freshness is copied from its artifact; this API does not apply Labelwatch's refusal policy to Weatherwatch.

This is the part worth reading before you use the numbers.

Weatherwatch coverage

facts.window.coverage_ratio is observed seconds divided by the span of the window. A ratio below 1 means the instrument was not watching for part of the interval. Counts are of what was observed, never an estimate of what occurred.

Observation adequacy (legacy Labelwatch schema 23 only)

The rolling epoch does not publish the legacy observation or network_weather fields.

facts.observation.adequacy is one of adequate, partial, or unobserved, and it qualifies the entire reading. It exists because a window that saw nothing cannot report that nothing happened.

Negative claims are gated. When adequacy is partial or unobserved, network_weather.signals will not contain calm. It reports under-observed or unobserved instead, and network_weather.supports_negative_claim is false. Positive findings still stand — an observed spike was observed — but the absence of a finding is not published as evidence of absence.

Practically: if supports_negative_claim is false, do not read a quiet result as "nothing is happening". Read it as "not enough was seen to say".

Caveats

caveats and provenance.does_not_measure describe structural blind spots. Weatherwatch, for instance, counts events and not accounts, so participant turnover is unavailable by construction rather than merely unimplemented.

What is not published

Labeler DIDs are published, deliberately. A labeler is a public moderation service with a discoverable endpoint, not a person, and the instruments already publish these identifiers. That is the one identity-shaped thing here, and it is a service identity.

Publication is an allow-list: each field is selected by name. A new field appearing inside an instrument cannot reach this API without someone adding it on purpose.

Versioning and compatibility

Deliberately modest.

/atproto/ is one namespace on a host that may carry unrelated APIs. Nothing here claims the root, and paths outside /atproto/ are not part of this contract.

Corrections and supersession

Instruments restate facts. A reducer gets fixed, or a window is recomputed as late data arrives. There is no separate correction feed: a re-read of the same endpoint returns the current best reading, and generated_at tells you which computation you have.

For two responses describing the same window, the one with the later generated_at supersedes the earlier. ETag changes whenever the underlying artifact changes, so a 304 means the current artifact-and-age-bucket validator matched. Freshness refusal is checked before a conditional response. Facts are not immutable and should not be cached indefinitely as though they were.

Errors

Errors use {"error":{"status":503,"code":"source_unavailable","message":"…"}}. Labelwatch freshness refusals also include error.freshness; missing or unreadable artifacts may omit it. Preserve the error instead of substituting zero counts or a prior successful reading.

StatusMeaning
304Your If-None-Match matched; reuse your cached body within its stated limits.
404No such path.
405POST, PUT, PATCH or DELETE is unsupported; Allow lists GET, HEAD, OPTIONS.
503 The instrument's artifact could not be read (source_unavailable), or, for Labelwatch, it is older than its freshness bound (source_stale) or the instrument is held (source_held). Sent with Cache-Control: no-store. This is absence of a current reading, not evidence that the instrument observed nothing.
500internal_error: the service could not build the fact.

A missing documentation asset uses 503 asset_unavailable; it does not carry the fact source no-store policy. Unknown paths use 404 not_found.

Complete example response

GET https://api.neutral.zone/atproto/v1/labelwatch/overview

Illustrative fixture at a fixed retrieval time, not a live reading.

{
  "contract": "atproto.facts.labelwatch.overview/v1",
  "instrument": "labelwatch",
  "generated_at": "2026-10-09T12:00:00.000000Z",
  "retrieved_at": "2026-10-09T12:01:00Z",
  "age_seconds": 60.0,
  "freshness": {
    "state": "warming_up",
    "age_seconds": 60.0,
    "basis": "artifact_generated_at",
    "max_age_seconds": 7200,
    "collection_age_seconds": 180.0,
    "collection_max_age_seconds": 7200
  },
  "facts": {
    "labeler_count": 97,
    "alert_count_total": null,
    "alert_count_window": null,
    "window_days": 30,
    "acquisition": {
      "generation_id": "0f3c2a8e-5b7d-4c1e-9a6f-2d8b4e7c1a90",
      "started_at": "2026-10-08T18:00:00.000000Z",
      "basis": "local_acceptance_clock"
    },
    "coverage": {
      "status": "unknown",
      "observed_days": 0,
      "observed_through": "2026-10-09T11:58:00.000000Z",
      "gap_overflow": false,
      "warmup": {
        "start": "2026-09-09T12:00:00.000000Z",
        "end": "2026-10-08T18:00:00.000000Z",
        "reason": "not_observed"
      },
      "gaps": [
        {
          "start": "2026-10-09T03:00:00.000000Z",
          "end": "2026-10-09T03:20:00.000000Z",
          "reason": "source_unavailable"
        }
      ],
      "sources": {
        "enrolled": 580,
        "seeded": 24,
        "discovered": 556,
        "unstarted": 120,
        "probing": 31,
        "established": 212,
        "refused": 217,
        "pre_epoch_skipped": 1834201,
        "undated_skipped": 12,
        "late_skipped": 4410,
        "cursor_order_unverified": 3,
        "undated_only": 2,
        "provider_rejects": 5,
        "unavailable": 7,
        "future_skipped": 9
      }
    },
    "counts": {
      "events_24h": 41234,
      "events_7d": 41234,
      "events_window": 41234,
      "labelers_active_7d": 97,
      "labelers_active_window": 97
    },
    "top_labelers_7d": [
      {
        "labeler_did": "did:plc:ar7c4by46qjdydhdevvrndac",
        "count": 30211
      },
      {
        "labeler_did": "did:plc:e4elbtctnfqocyfcml6h2lf7",
        "count": 5120
      }
    ],
    "current_label_state": "unknown"
  },
  "caveats": [
    "Counts cover only the current epoch's window; intervals before acquisition are not observed.",
    "Counts are of Labelwatch's own observations, not of everything that happened. A labeler Labelwatch could not reach contributes nothing.",
    "Recorded gaps are disclosed with a reason. No recorded gaps does not prove complete source coverage.",
    "Labelwatch records observed application and removal events; current label state is unknown.",
    "labeler_count counts labelers with at least one observation in the current window of this epoch.",
    "Labeler DIDs identify public moderation services, not people. No subject identities are published."
  ],
  "provenance": {
    "instrument": "labelwatch",
    "method": "Keeps a rolling window of label application and removal events observed from known labeler endpoints, stamped with the local acceptance clock; older observations expire.",
    "artifact_schema_version": "labelwatch.recent-overview.v1"
  }
}

Machine-readable contract

OpenAPI 3.1 at https://api.neutral.zone/atproto/openapi.json.