{"id":"385b83d441c7587462d535754aea2605aeb4e7a609f3f5f404bb3f4ba7754188","pubkey":"daa41bedb68591363bf4407f687cb9789cc543ed024bb77c22d2c84d88f54153","created_at":1785640805,"kind":30817,"tags":[["d","service-announcements"],["title","Service Announcements"],["k","10200","Service Roster"],["k","30200","Service Announcement"],["client","better-nips"]],"content":"Services describe themselves to their owner, so that an inventory of what someone runs is a side effect of running it rather than a list kept by hand.\n\nEach service publishes an encrypted, self-expiring record of what it is, where it can be reached and how it is doing. A reader holding the owner's key collects those records. There is no server to run and no prober: a reader need not reach a service to know whether it is alive.\n\n## Kinds\n\n| kind    | name                 | type        | author  | purpose                                          |\n|---------|----------------------|-------------|---------|--------------------------------------------------|\n| `30200` | Service Announcement | addressable | service | What a service is, where it is, how it is doing. |\n| `10200` | Service Roster       | replaceable | owner   | Which service keys the owner accepts.            |\n\nBoth are provisional and unassigned at the time of writing; `200` is a nod to HTTP's `200 OK`.\n\n## Motivation\n\nSelf-hosted inventories are a hand-maintained list plus a prober that must reach every host. The list drifts from reality as things change, and the prober cannot see behind NAT without being inside the network, which is exactly when one wants to look from elsewhere. Inverting it fixes both: services announce, nothing polls, and because announcements are ordinary events the inventory can be read from anywhere, while *using* a service still requires whatever network it sits behind.\n\n## Roles\n\n- **owner**: the person. One pubkey, SHOULD be dedicated to this purpose rather than a social identity.\n- **service**: a process that announces itself, holding its own keypair generated on first run. A host may run many.\n- **reader**: anything that displays the inventory. Needs the owner's key to decrypt, and SHOULD obtain decryption through a remote signer ([NIP-46](46.md)) rather than holding the secret.\n\n## Service Announcement\n\n```json\n{\n  \"kind\": 30200,\n  \"pubkey\": \"<service pubkey>\",\n  \"created_at\": 1785400000,\n  \"tags\": [\n    [\"d\", \"7f3c1a9e-4b02-4c31-9a77-2e5d8c10b4aa\"],\n    [\"p\", \"<owner pubkey>\"],\n    [\"expiration\", \"1785400900\"]\n  ],\n  \"content\": \"<NIP-44 ciphertext of the payload below>\"\n}\n```\n\n| tag          | presence | meaning                                                |\n|--------------|----------|--------------------------------------------------------|\n| `d`          | MUST     | Stable per service instance, and opaque (see Privacy). |\n| `p`          | MUST     | The owner. Readers subscribe by this.                  |\n| `expiration` | SHOULD   | [NIP-40](40.md). See Liveness.                         |\n\nNo other tags are defined and implementations SHOULD NOT add descriptive ones: tags are public, and anything descriptive belongs in the payload.\n\n`content` is the payload below, encrypted with [NIP-44](44.md) between the service key and the owner key.\n\n```json\n{\n  \"v\": 1,\n  \"name\": \"hanami\",\n  \"type\": \"blossom\",\n  \"version\": \"0.14.0\",\n  \"host\": \"box1\",\n  \"node\": \"<pubkey identifying the host, if not this key>\",\n  \"started_at\": 1784800000,\n  \"interval\": 300,\n  \"urls\": [\n    { \"role\": \"admin\", \"url\": \"https://box1.example/admin\" },\n    { \"role\": \"api\",   \"url\": \"https://box1.example/upload\" }\n  ],\n  \"metrics\": { \"storage_used\": 44023414784, \"storage_total\": 64424509440 },\n  \"note\": \"3 peers, last backup 2h ago\"\n}\n```\n\n| field        | presence | meaning                                                              |\n|--------------|----------|----------------------------------------------------------------------|\n| `v`          | MUST     | Payload version. `1` for this document.                              |\n| `name`       | MUST     | Human name, usually the software.                                    |\n| `interval`   | MUST     | Seconds between republications. Defines staleness (see Liveness).    |\n| `type`       | SHOULD   | Broad category (`relay`, `blossom`, `signer`, …), for grouping.      |\n| `version`    | SHOULD   | Version of the running software.                                     |\n| `host`       | SHOULD   | Human label for the machine. A display hint (see Security).          |\n| `node`       | SHOULD   | Pubkey identifying the host, where services and hosts differ.        |\n| `started_at` | MAY      | Unix seconds the service started, so uptime can be shown.            |\n| `urls`       | MAY      | Addresses, each with a `role`. Reachability depends on the reader.   |\n| `metrics`    | MAY      | A small map of *current* numeric gauges. Never a series (see Scope). |\n| `note`       | MAY      | Short free text for whatever does not fit.                           |\n\nReaders MUST ignore unrecognised fields, and SHOULD render a payload with a higher `v` on a best-effort basis rather than discarding it.\n\n## Liveness\n\nA service MUST republish every `interval` seconds and SHOULD set `expiration` to about three times `interval`. On clean shutdown it MAY republish with `expiration` set to the present, so a deliberate stop disappears at once rather than appearing stale.\n\nReaders MUST ignore announcements dated meaningfully in the future, allowing for clock skew; `created_at` is publisher-chosen, so otherwise a compromised service could post-date one and appear alive indefinitely.\n\nReaders MUST derive state from the age of the newest announcement, measured against that announcement's own `interval`:\n\n- age < 2 × `interval`: running\n- age ≥ 2 × `interval`: not known to be running\n- no event: unknown, or expired\n\nNo status field is defined, and a self-reported one MUST NOT be trusted if present. A service that has crashed, lost power or been disconnected cannot publish its own failure, so the only reliable indicator of an outage is the absence of one.\n\n## Service Roster\n\nAnyone can address an event to the owner, so a reader MUST NOT trust an announcement merely because it is addressed correctly.\n\n```json\n{\n  \"kind\": 10200,\n  \"pubkey\": \"<owner pubkey>\",\n  \"created_at\": 1785399000,\n  \"tags\": [],\n  \"content\": \"<NIP-44 ciphertext, owner to owner>\"\n}\n```\n\nThe plaintext is a JSON array of accepted service pubkeys:\n\n```json\n[\"<service pubkey>\", \"<service pubkey>\"]\n```\n\nReaders MUST NOT trust announcements from keys absent from the roster, MUST consult it before trusting a key seen for the first time, and SHOULD surface absent keys as pending, so adding a service is an approval rather than an edit. Revocation is removal from the roster; because each service holds an independent key, one can be dropped without touching any other.\n\n## Reader behaviour\n\n1. Fetch the roster: `{\"kinds\": [10200], \"authors\": [\"<owner pubkey>\"]}`.\n2. Subscribe: `{\"kinds\": [30200], \"#p\": [\"<owner pubkey>\"]}`.\n3. Decrypt each `content` with NIP-44, using the owner key and the event's `pubkey`; set aside events whose author is absent from the roster.\n4. Keep the newest event per author and `d`, and derive state as in Liveness.\n\n## Deployment\n\nNothing here depends on a particular network. A service need only open an outbound connection to a relay, and the reader reach the same relay, which is why no prober is needed and why services behind NAT require no special arrangement. The choice of network affects privacy and availability, not correctness.\n\nWhere services sit on a private network, announcements SHOULD go to a relay on it, which removes the third party entirely. Such a network need not be a LAN: an overlay that traverses NAT reaches a phone on a mobile network, so a private relay can still be read from anywhere. Owners MAY additionally mirror to a public relay so the inventory survives that network being degraded: the one case a private relay cannot cover.\n\nThe layering must not invert: the inventory is never how a reader *finds* that relay. An inventory that bootstraps itself cannot be read when it is most needed.\n\n## Privacy considerations\n\n`content` is encrypted; the envelope is not. On a relay the owner does not control:\n\n- The `p` tag links every service to one owner, exposing the *shape* of a deployment: how many services, how often each reports.\n- The kind is itself a marker: anyone may subscribe to kind `30200` and enumerate who uses this scheme without decrypting anything. No further marker tag is defined and none SHOULD be added, since it would only narrow that search.\n- The `d` tag is public and MUST be opaque. A random identifier generated on first run suffices; `hanami@box3` would leak exactly what the encryption was for.\n\nAnnouncements MAY instead be published as [NIP-59](59.md) gift wraps, so a relay cannot tell how many services exist or correlate one over time. Two rules change: the roster check applies to the **seal's** pubkey, since the wrapper's author is a throwaway key; and the future-timestamp check applies to the **rumor's** `created_at`, since NIP-59 randomises the wrapper's. The cost is that gift wraps are not addressable, so relays retain every announcement until expiry rather than only the latest, and owners choosing this SHOULD lengthen `interval`. It does not conceal that a recipient exists, as the `p` tag remains.\n\n## Security considerations\n\nAnnouncements are **hints, not authentication**. A reader learns where a service claims to be; whether the thing answering is that service is decided by the transport (TLS, a key exchange, an onion address), never by this event. Implementations MUST NOT treat `urls`, `host` or `name` as grounds for trusting what they connect to. Where a network derives addresses from keys, the reachability half authenticates itself and readers SHOULD prefer it.\n\nThe roster is the trust anchor. Compromising one service key permits false statements about that service alone; introducing a new service requires the owner's key. `metrics` is a claim by a service about itself, and is exactly as trustworthy as that service.\n\nA hostile or faulty relay fails safe: withholding announcements makes a service appear stale, and replaying an old one makes it appear staler still. Neither can make a stopped service appear to be running.\n\n## Scope\n\nAn inventory with liveness, deliberately not monitoring: no history, no aggregation, no alerting. `metrics` carries current gauges so a reader can show \"94% full\", and nothing further. Retention and alerting belong to monitoring software, which can announce itself here like anything else.","sig":"e3e9566363eaa388011d9a80961273a785835f908f32d18a93baa3e72b078db84182cb8be0096329b72f640216fe0cdcbcef3fdd4942649d0041f27f8b424d79"}