# Voidly Probe Protocol

Self-onboarding spec for community probe operators. No email, no key request,
no human in the loop — register, get an ingest key, start submitting.

---

## Why run a probe

Every probe adds one more vantage point to Voidly's censorship-detection
network. We aggregate the data with measurements from OONI, CensoredPlanet,
and IODA into the global Censorship Index. Operators of healthy probes are
automatically promoted from `trial` to `full` after 7 days, get a 6× higher
rate limit, and become part of the public probe network display on
[voidly.ai/probes](https://voidly.ai/probes).

We don't store probe IPs (we keep ASN only, for ISP-level analysis). The
ingest key is the only identity material we hold.

---

## Quickstart

### 1. Register

```bash
curl -X POST https://api.voidly.ai/v1/probe/register \
  -H "Content-Type: application/json" \
  -d '{
    "node_id": "my-probe-de-1",
    "version": "1.0.0",
    "geo": { "country": "DE", "asn": "AS24940" },
    "domains_supported": ["x.com", "wikipedia.org"]
  }'
```

Response:

```json
{
  "ok": true,
  "node_id": "my-probe-de-1",
  "ingest_key": "<save this — never shown again>",
  "ingest_key_header": "X-Probe-Ingest-Key",
  "status": "trial",
  "trial_ends_at": 1715184000,
  "promote_threshold": 0.9,
  "rate_limit_per_min": 100,
  "submit_endpoint": "https://api.voidly.ai/v1/probe/results",
  "heartbeat_interval_s": 300
}
```

**Save the `ingest_key`.** It's hashed at rest and cannot be recovered. If
you lose it, register a fresh `node_id` — duplicates of an existing
`node_id` are idempotent and won't issue a new key.

### 2. Submit results

POST your probe results to `submit_endpoint` with the key in the
`X-Probe-Ingest-Key` header:

```bash
curl -X POST https://api.voidly.ai/v1/probe/results \
  -H "Content-Type: application/json" \
  -H "X-Probe-Ingest-Key: <your-key>" \
  -d '{
    "nodeId": "my-probe-de-1",
    "country": "DE",
    "results": [
      { "domain": "x.com", "blocked": false, "confidence": 0.98, "latencyMs": 142 }
    ]
  }'
```

The server records `last_seen_at` on every successful POST. Stop submitting
for >24h and you'll be auto-suspended. Stop for >14d and you're deactivated.

### 3. Stay healthy

Aim for one submission every 5 minutes. The protocol expects probes to be
roughly that responsive — silent gaps longer than 10 minutes will mark you
as "silent" in the public heartbeat feed.

---

## Required fields

### `POST /v1/probe/register`

| Field | Required | Description |
|-------|----------|-------------|
| `node_id` | yes | Stable 4–64 char identifier you control. Letters, digits, `._-/` only. |
| `version` | optional | Probe software version (≤ 32 chars). |
| `geo.country` | optional | ISO 3166-1 alpha-2. We never store IP, so this hints regional coverage. |
| `geo.asn` | optional | `AS12345` or `12345`. Used for ISP-level analysis only. |
| `domains_supported` | optional | Informational. We accept the full APPROVED_DOMAINS set regardless. |

### `POST /v1/probe/results`

Same shape as the existing community probe payload. `results` is an array of
domain checks. Approved domains only — see the registry response for the full
list. Off-list domains are silently dropped.

---

## Quality scoring

Once an hour the Worker evaluates every registered probe:

```
quality = success_rate × 0.7  +  freshness × 0.3

success_rate = (total - failed) / max(1, total)
freshness    = 1.0 if seen ≤ 1h ago,
               linearly decaying to 0 at 24h
```

- **Promote** (`trial → full`): quality ≥ 0.9 AND ≥ 7 days of data
- **Suspend** (`full → suspended`): quality < 0.5 OR silent > 24h
- **Suspend** (`trial → suspended`): silent > 24h
- **Deactivate** (`suspended → deactivated`): silent > 14d

Suspended probes can still submit but the data is held in trial-tier rate
limits and not surfaced on the public network display until quality recovers.

---

## Rate limits

| Tier | Limit |
|------|-------|
| `trial` | 100 result submissions / minute |
| `full` | 600 result submissions / minute |

If you exceed the cap you get `429 Rate limit exceeded` with a
`Retry-After: 60` header. Over a sustained tier excess, your `failed_probes`
counter grows and your quality score drops.

---

## Privacy

- **No IP storage.** We log ASN if you provide it, never the IP itself.
- **No PII.** Don't include user identifiers, email addresses, or session
  tokens in your `nodeId` or any submission.
- **Public node_id.** The `node_id` is visible via
  `GET /v1/probe/network/heartbeats`. Pick one that doesn't expose internal
  hostnames you'd rather not publish.

---

## Self-healing

The Voidly self-healer monitors core nodes via the same heartbeat feed. It
will auto-restart core nodes it owns; community probes get a public
notification (visible in the operator's dashboard) when they go silent so
the operator can investigate, but the healer does not touch remote machines
it has no SSH access to.

---

## Endpoints reference

| Method | Path | Auth | Purpose |
|--------|------|------|---------|
| `POST` | `/v1/probe/register` | none | Self-onboard a new probe |
| `POST` | `/v1/probe/results` | `X-Probe-Ingest-Key` | Submit measurements |
| `GET`  | `/v1/probe/network/heartbeats` | none | Live liveness feed |
| `GET`  | `/v1/probe/priority-targets` | none | Domains × countries we want covered |
| `GET`  | `/v1/probe/notifications` | none | Public network announcements |

---

## Versioning

This protocol is `v1`. Breaking changes will be versioned at the path level
(`/v2/probe/...`); existing v1 probes will continue to function until at
least 90 days after a v2 promotion to general availability.
