Adoption Lighthouse

The Trust Console answers “is the system safe?”. The Adoption Lighthouse answers a different question: is anyone actually using it — and is that growing? It aggregates enrollments, evidence, receipts, settlements, and enabled rails across time windows, reports the trajectory, and signs the result so anyone can verify the numbers.

It refuses to flatter us

Our own smoke harnesses and the adoption proof loop create agents, evidence, receipts, rails, and settlements. Counting those as “adoption” would be self-congratulatory. Any row whose identity / evidence / reference carries one of the documented markers is excluded, and the marker list is returned in the response under excluded_markers:

  • adopt- — the adoption proof loop and its canary rail
  • adopt-canary- — the settlement canary rail
  • smoke: — smoke-harness artifacts

Buckets and trend

Four buckets — 24h, 7d, 30d, all — each report enrolled_agents, evidence_events, receipts, settlements, enabled_rails, and distinct_operator_prefixes. For the 24h and 7d windows the lighthouse also compares against the equal preceding window and labels each metric growing, flat, or falling.

Operator ids are masked to their first four characters — a count of distinct operators, never an identity.

GET /api/v1/raillab/lighthouse   (ISSUER API key)
{
  "lighthouse": {
    "organic_only": true,
    "excluded_markers": ["adopt-", "adopt-canary-", "smoke:"],
    "buckets": { "24h": { "enrolled_agents": 3, "settlements": 41, ... }, "7d": {...}, "30d": {...}, "all": {...} },
    "trend":   { "24h": { "settlements": "growing", ... }, "7d": {...} },
    "persistence": {
      "cohorts": [{ "week": "2026-W24", "size": 12, "w1_retention": 0.5, "w2_retention": 0.33, "w3_retention": 0.25, "mature": true }],
      "funnel": { "7d": { "enrolled": 40, "evidenced": 18, "receipted": 9, "settled": 6, "returned": 4 } },
      "integrity": { "suspicious": false, "reasons": [] }
    },
    "degraded": false,
    "degraded_reasons": []
  },
  "snapshot": { "content_hash": "…", "signature": "…", "public_key": "…", "algorithm": "ed25519" }
}

Proof of Persistence — durable, not vanity

A raw count answers “how many showed up?” — a number a Sybil can inflate for free. The persistence block answers the only question that compounds: did they come back and keep doing costly work?

  • Cohorts — each ISO week (UTC) of newly-seen operators, with w1/w2/w3_retention: the fraction still active one, two, three weeks later. A cohort is the week an operator was first seen via a first Agent or Receipt. mature is false until the week-1 window has fully elapsed — immature cohorts read w1_retention: 0 by construction and are never used to flag inflation.
  • Funnelenrolled → evidenced → receipted → settled → returned, deduped by operator and nested(each stage requires the prior), so it is monotonic non-increasing by construction. “Returned” means active in a week after their cohort.
  • Integrity — flags manufactured adoption: an enroll-and-vanishcohort (mature, size ≥ 10, with < 10% week-1 retention), or a > 80% never-settled ratio over 7d among operators past a 3-day grace (so a network is not libelled the day it grows). When integrity.suspicious is true the reading is never cached.

A settlement counts as activity only when SETTLED — it requires a valid rail signer signature — so the strongest persistence signal cannot be manufactured by spamming bad-signature attempts at /settle.

Verify the numbers offline

The response is signed with the same key as /api/v1/receipts/monetary. Remove the snapshot object, recompute content_hash = sha256(canonicalJson(body, keys sorted)), then verify the Ed25519 signature over utf8(content_hash). If it checks out, the adoption numbers were not tampered with in transit or at rest.

# pseudocode
body      = response minus "snapshot"
hash      = sha256(canonicalJson(body))
assert hash == snapshot.content_hash
assert ed25519_verify(snapshot.signature, utf8(hash), snapshot.public_key)

Degrades, never lies

If a datastore read fails, the lighthouse returns HTTP 200 with degraded: true, the buckets it could compute, and a degraded_reasons list — a barometer that hides all data on a partial outage is worse than one that reports what it sees.

Agent autarky

const p = new PassportClient({ apiKey: "pp_…", baseUrl: "https://passport.metis.gold" });
const lh = await p.getLighthouse();              // ISSUER key

if (lh.lighthouse.buckets["7d"].settlements === 0 || lh.lighthouse.trend["7d"].settlements === "falling") {
  // an autonomous operator can notice the network is cooling and adjust.
}