Fleetmesh fleetmesh
meshstr .org

federation spec · alpha draft · 2026-10-02

The fleetmesh Protocol

The technical specification for fleetmesh: an open, permissionless relay network. Nodes deploy across datacenter servers, single-board computers, and mobile devices without centralized dependencies or proprietary infrastructure. The architecture provides coordinated spam defense based on verified operator observations. Nodes identify via W3C DIDs and negotiate over DIDComm v2. Capacity is enforced through declared bilateral budgets and signed accounting receipts.

0-draftdeclared protocol version
3node classes
0required bootstrap hosts
11event kinds, unregistered
0private data in the mesh
3transports in alpha
4open questions
S0–S4severity classes

Scope

Protocol Scope & Architecture Principles

fleetmesh is an alpha, permissionless relay network open to arbitrary hardware. Participants can run nodes on dedicated servers, virtual machines, Raspberry Pis, laptops, or mobile phones. It maintains independent governance, distinct branding, and its own protocol namespace at fleetmesh.org. The system is engineered as an open standard with zero dependence on any single commercial entity or hosted provider.

Four requirements shape everything below.

  • Two networks, one boundary. The mesh and any private or commercial relay backends share no database, no message bus, no secrets and no identity. They meet at a single gateway that speaks ordinary relay protocol. Either side can be switched off without the other noticing. § Two networks is that boundary in detail.
  • Uses, does not rely on. relay.example.org appears in this specification solely as an illustrative example. No bootstrap host, DNS domain, or single operator is a hardwired constant. The conformance suite includes an automated test that removes all default bootstrap nodes and verifies that the mesh continues operating.
  • A phone is a full node. It holds an independent cryptographic identity, retains a local copy of subscribed data, and earns standing in the reputation system. It operates with intermittent uptime and no inbound public connectivity. A design requiring public IP addresses and TLS certificates is a server federation rather than a mesh.
  • Standards where they exist. Node identity is a W3C DID. Node-to-node negotiation is DIDComm v2, using registered protocols wherever one already covers the job. Transport is iroh and libp2p. Content addressing is multihash/CID. New wire formats appear only where nothing suitable exists, and each is called out as such.
Alpha

The mesh is currently in alpha status. Kind numbers, wire formats, and scoring constants will evolve. Reputation findings gathered during alpha are discarded at general availability rather than carried forward. The core promises in § Alpha contract remain invariant: joining the mesh never risks existing local data, and leaving requires only a single configuration flag.

Terminology

MUST, SHOULD and MAY are used per RFC 2119. A node is a mesh participant identified by a DID. A peer is a node another node holds an active grant with. The subject of a grant is the node it constrains; the issuer published it and will enforce it. The platform means any private backend or hosted cluster; fleetmesh, or the mesh, means this network. An anchor node is a public gateway or routing point, and has no protocol privilege of any kind.

Explicitly out of scope

Custodial signing is permanently out of scope. A service that holds encrypted user keys alongside their unlocking secrets is a custody service. Replicating that state to unknown nodes converts it into an unauthorized key-distribution channel. The same exclusion applies to billing records, push notification subscriptions, and account credentials. The full boundary specification and enforcement mechanisms are detailed in § Two networks.

Payment for transit is also out of scope, deliberately and with a rule attached rather than a shrug — see § Grants & receipts. The schema reserves a settlement object so a later version can add it without a breaking change, and nothing else here depends on it.

Hardware-attested execution is refused rather than deferred. Trusted execution environments are the only practical way to hide a job's input from the node running it, and every one of them terminates in a remote attestation signed by a hardware vendor's root key. A protocol whose confidentiality guarantee resolves to Intel or AMD vouching for an enclave has an authority, and § Governance has none by construction — no registry, no release manager, no host anyone must reach. It would also contradict a node class that runs on a phone. Confidentiality here therefore means confidential from third parties, and verifiability is what constrains the executor; see § Compute market.

Why join

Operational Advantages Over Standalone Relays

Operating a standalone relay isolates an operator from shared threat intelligence, single-disk storage risks, and NAT traversal challenges. Participating in fleetmesh provides collaborative network capabilities without requiring centralized infrastructure:

reverse-reputation spam control
Standalone relays evaluate traffic solely from local observations. Within the mesh, public key behavior includes cryptographically signed evidence from peer operators. Each node weights reports according to local subjective trust without importing external blocklists. Every restriction links to a verifiable grant and reproducible evidence.
redundant data availability
Peers asserting shard coverage retain synchronized event copies. Hardware failures do not cause data loss, enabling rapid partition recovery through state reconciliation.
traversal behind NAT
Direct hole punching combined with DIDComm mediation provides addressability for residential and edge nodes without dedicated static public IP addresses.
efficient state reconciliation
NIP-77 range reconciliation synchronizes disconnected nodes using compact fingerprint trees, avoiding redundant event streaming.
independent telemetry
NIP-66 monitors publish objective availability metrics and latency records, validating node uptime from external vantage points.
content-addressed blob availability
Content-addressed blobs replicate across peers and map to IPFS CIDs, enabling independent archival and retrieval across standard tooling.

Operational expectations: Fleetmesh provides no managed service level agreements, custodial signing, or centralized billing. Participating nodes agree to publish valid accounting receipts, honor active bilateral grants, and address disputes prior to report publication.

Node classes

Three Operational Node Classes

Connectivity constraints determine node classification. Single-board computers can host full relay services but lack public inbound listeners behind firewalls. Mobile devices retain keys and local data caches but pause network sockets during background execution. A node declares its operational role in its signed descriptor, establishing capability bounds across the protocol.

No special class exists for founding operators. Gateways and public relays declare anchor under identical terms as any other node. The protocol defines no privileged keys, reserved names, or administrative constants. Anchor trust derives entirely from accumulated history across verified accounting windows.

ANCHOR EDGE LEAF bootstrap anchor anchor anchor anchor gateway · no privilege VPS · wss + TLS VPS · wss + TLS VPS edge edge edge Raspberry Pi · NAT home server · NAT mini VPS phone browser PWA laptop phone iroh QUIC · NIP-77 range sync · libp2p gossipsub hole-punched QUIC · wss:// when it fails DIDComm routing 2.0 through a mediator · WebRTC · wss://
Anchors form the core mesh with stable inbound addresses. Edges peer with anchors and other edges via hole punching. Leaves route through designated mediators. All anchor nodes operate under uniform protocol rules.

Leaf

phone · browser · laptop

Stores local author data and requested feeds. Operates in the foreground over a single WebSocket to its mediator.

  • MUST NOT be cited by another node as a source of record
  • MUST route through a mediator; no inbound listener, no p2p stack
  • Subject of grants only — does not issue grants, publish reports, or forward hops
  • Publishes no coverage claims

Edge

raspberry pi · home server · nas

Provides durable local storage with high availability behind NAT. Suitable for community, household, or organization deployments.

  • MUST hold every event within its declared coverage, or narrow the claim
  • SHOULD reach peers by hole punching; MAY fall back to an anchor as transit
  • MAY mediate for leaves it has granted
  • Runs the sqlite store; Postgres is not required

Anchor

vps · public ip · tls

Stable wss:// endpoint, valid certificate, and continuous availability. Provides rendezvous routing and mediation across the network.

  • MUST serve NIP-11 over HTTPS and accept anonymous reads within policy
  • MUST act as a DIDComm mediator for at least its own granted leaves
  • SHOULD offer itself as circuit relay / iroh relay for hole punching
  • MUST publish a kind 21801 heartbeat at least hourly — liveness, not attestation: § Grants & receipts forbids publishing a positive report

The leaf profile and mobile operational limits

Mobile operating systems enforce strict background constraints. iOS terminates background sockets upon screen lock, blocks background hole punching, and limits silent push execution to thirty seconds. Android Doze imposes equivalent restrictions.

one transport
wss:// connection to its mediator. Leaves do not negotiate dynamic transport ladders because upper rungs do not survive mobile background execution.
foreground networking, push to wake
The leaf synchronizes during active foreground execution. Its mediator buffers inbound messages under DIDComm routing/2.0 and signals new traffic via Web Push (VAPID). Wake windows operate on a best-effort basis: brief execution windows perform range reconciliation, while offline intervals catch up upon the next application launch.
pull-only, subject-only
A leaf publishes events for its local author and pulls requested content. It does not forward traffic, mediate for peers, issue grants, or file adverse reports. Its protocol scope centers on its bilateral grant from its designated mediator.
bounded storage
Local storage maintains a rolling window based on time or configured byte capacity across local events. Eviction operates by age rather than external request.
keys in the OS keystore
The node key resides in secure storage (iOS Keychain or Android Keystore) and remains separate from the personal social Nostr key. Accounting grants and reports evaluate node networking conduct rather than personal social identity.

Browser progressive web applications share the leaf operational profile. WebRTC and direct browser-to-browser peering remain future roadmap items compatible with this baseline.

Invariant

A peer observing a node failing its declared class obligations files an S2 report. Examples include an anchor whose public endpoint fails to resolve, or an edge returning empty queries within its claimed coverage. Overstating capabilities is a misreport, which the protocol prices under the severity ladder.

Two networks

Boundary Architecture Between Mesh & Private Infrastructure

Operators joining the mesh often maintain existing infrastructure. Reusing an existing production cluster directly as a mesh node is a dangerous shortcut. That shortcut produces three predictable failures: a network that collapses if a business fails, private users exposed to unvetted traffic, and a single operator gaining an unearned veto. Fleetmesh defines the junction between private estates and the public mesh as a strict security boundary. These rules apply universally, from commercial cloud operators to home servers.

AN OPERATOR’S OWN SERVICES THE MESH — ALPHA, OPEN private relays · SLA custodial signer accounts · billing private blob storage databases · bus · secrets bootstrap anchors third-party anchors edge nodes · behind NAT leaves · phones, browsers grants · reports · receipts mesh gateway wss · NIP-77 only no shared database · no shared bus · no shared secrets · no shared identity
The gateway is an ordinary relay client. It authenticates to the local relay the way any client does, reads what it is permitted to read, and writes into the mesh; nothing on the operator's own side imports a mesh library or opens a mesh port. The mesh panel is drawn open because its membership is: any node may join it, and none of them can reach anything on the left.

The gateway

A gateway is a separate process, in its own isolation boundary, holding its own node key, its own storage and its own resource budget. Its only interface to the operator's own services is wss:// plus NIP-77 range sync — the same protocol any third-party client speaks. That is deliberately the weakest coupling available, and it buys three things:

  • The estate cannot be broken by the mesh. A malfunctioning or attacked gateway appears to internal relays as a single misbehaving client. Relays handle this natively with existing rate limits and connection drops.
  • The mesh cannot be broken by the estate. The gateway is one anchor among many. If the entire estate behind it is down, the mesh loses one node's coverage.
  • The boundary is testable. Internal isolation is mechanically provable. The gateway holds no credentials beyond an ordinary relay WebSocket connection. This is a verifiable deployment property rather than an assumption about application logic.

What crosses, in each direction

Direction Rule
estate → mesh Per-shard, opt-in, and the opt-in belongs to the account that owns the data — never to the operator carrying it. A relay publishes nothing into the mesh until the shard is turned on, and turning it off stops new publication immediately. Public events only; the never-crosses list below is enforced at the gateway and again at every receiving node.
mesh → estate Quarantined. Mesh-sourced events land in a separate store and are never auto-promoted into the relay behind the gateway. Backfill is requested per shard and arrives as an explicit import with its provenance recorded. Anything else lets strangers write into a service whose users never consented to them.
reputation Does not cross in either direction. A pubkey the mesh has judged hostile keeps whatever service its own relay grants it; an account in good standing on that relay starts at probation in the mesh like everyone else. Delegating moderation of your own users to a permissionless network is not a feature, and importing a stranger's standing is not either.
identity Separate keys. A mesh node key is generated for the mesh and lives only in the gateway's own secret store. Existing service identities MUST NOT be reused, so a mesh key compromise never produces a signature anyone trusted for something else.
money & status Mesh traffic is never metered to a user and never counts against a quota. Mesh incidents are not incidents of the service beside it, and no service commitment anywhere should make a statement about the mesh.

Never crosses, in any version

Data Why
custodial keys Encrypted nsecs, the secret that unlocks them, and signer session state. Replicating them turns custody into distribution.
kind 24133 NIP-46 signer traffic. Ephemeral, and federating it would expose every pairing and every signing event of every reachable signer.
kind 1059 Gift wraps, under the rule in § Replication. Stated there once and normatively; repeated here only because the boundary table would be misleading without it.
billing & push Payment records, balances, push subscriptions. Commercial and contact data belonging to people who never joined the mesh.
client IPs Connection metadata stays on the node that observed it. Reports carry counts, never addresses.

Nothing is a constant

relay.example.org appears in this specification exactly as an example. Three rules keep it that way:

  • Bootstrap is a file, not a constant. Default peers ship as a plain, editable file. A single --bootstrap CLI flag bypasses this file entirely. A build shipping an empty bootstrap list MUST operate without error. The composition rule is staged progressively (see § Governance) to allow network bootstrap from genesis without artificial barriers.
  • DNS seeds are optional. A node MAY discover peers through DNS TXT records, the way Bitcoin does, and MUST NOT require them. Losing every seed domain slows discovery for new nodes and does nothing to established ones.
  • Protocol identifiers are neutral. The https:// URIs naming the DIDComm protocols, the gossipsub topics and the transport ALPNs all sit under fleetmesh.org rather than a product domain. They are identifiers, never dereferenced, so this buys nothing at runtime — it buys not having to hold a flag day later, when renaming stops being free. See § Governance.
The unplug test

The conformance suite includes a dedicated run with all default anchors and fleetmesh-operated infrastructure removed. The test disables all gateways, seed domains, and sets fleetmesh.org to resolve to nothing. Discovery, bilateral peering, state synchronization, grant issuance, and conformance reporting must all execute successfully. This test runs in continuous integration on every commit to enforce absolute infrastructural independence.

Governance

Protocol Assets & Decentralized Governance

Network governance spans four distinct operational assets. Treating them as a single problem creates unnecessary bureaucracy. Separating them reveals that three are already decentralized by design, and the fourth requires no central authority.

AssetWhat it actually risksDisposition
protocol identifiers Nothing technical — they are never dereferenced. Only that a network meant to outlive a company carries its name. registered fleetmesh.org, held as the project’s own name rather than a product’s.
kind registrations Nothing, once submitted. A merged entry in registry-of-kinds lives in a repository neither project owns nor can revoke. self-neutralising Submitting them is itself an act of decentralisation.
bootstrap.toml Soft power over who a new node meets first. Not a takeover vector — established nodes have persisted peers, and one --bootstrap address bypasses it entirely. unsigned A convenience whose composition is disclosed. See below.
protocol versions Looks like the dangerous one: whoever decides what the protocol is decides what everyone must implement. It stops being dangerous when nobody decides. declared, not decreed

Versions are declared, not decreed

There is no release authority, and there is no signer set. A k-of-n scheme, under which a change is valid once k of n signers endorse the announcement hash, operates by decree. Everything else in this document relies on empirical verification, and a version is not the one place to make an exception.

Everything else here works by declaration. A grant is a published budget. Coverage is a falsifiable claim. Conformance is your signed statement measured against my observation. A compute result is a hash the seller signs and a re-run can contradict. In every case the shape is the same: declare, be observed, and let divergence be the offence. A protocol version is exactly that kind of claim, so it gets exactly that treatment.

Each node declares its protocol version in its own descriptor. The descriptor names the protocols the node implements, each with a version, and two nodes peer on the ones they share: same major, the peer's minor at or above this node's floor. The required peering and budget protocols must be compatible or the peering is declined with UNSUPPORTED_PROTOCOL; a peering whose policy needs another protocol the peer lacks is declined the same way. The schema digest records which schema a node was built from and is surfaced to the operator, not refused on its own. There is no governance announcement, notice period, or signing committee. A version exists when active nodes declare it and disappears when they stop. Adoption is measured directly from public on-wire data.

A version is a digest, and a protocol is a version

Version names alone are insufficient. Two independent operators could deploy incompatible implementations both labelled fleetmesh/1 and discover discrepancies only after corrupting state. Declarations therefore carry a canonical digest over the whole schema, which says exactly what a node was built from. Compatibility, though, is decided one protocol at a time: each protocol identifier carries a version (sync/1.0) that MUST move when its wire moves, schema/PROTOCOLS records a digest of each protocol's block against that version and CI refuses a block that changes under an unchanged version. Nodes peer on the protocols whose versions are compatible and surface a differing whole-schema digest to the operator instead of refusing it.

what the version digest covers — and what it deliberately does not

kinds.json       every event kind this version defines: number, class,
                 required and optional tags, content type
messages.json    every DIDComm protocol: piuri, message types, body schemas
constants.json   every number the arithmetic depends on: the 110% threshold,
                 the divergence floor, AIMD step, tier weights, hop limit,
                 window sizes, sampling defaults

digest        := sha256(kinds.json || messages.json || constants.json)
                 each canonicalised, concatenated in that order

NOT covered: this document's prose, examples, rationale, section order,
or any wording anywhere. A typo fix MUST NOT fork the network.

Scoping the digest to the machine-readable schema is the whole trick. Too narrow and incompatible implementations still collide silently; too broad and correcting a sentence becomes a breaking change. What two implementations must agree on byte-for-byte is exactly what belongs in the hash, which is the same rule § Governance already applies to deciding which names are the protocol's.

Each protocol identifier carries a version of its own, as in sync/1.0. Those are informative labels; the digest is normative, and a node negotiates nothing below it. An identifier's version is bumped when its messages or fields change, so a reader can see which protocol moved without diffing the schema.

This mechanism is not new to the document either. Kind 11801 already pins an operator's policy document by digest, and changing that document without republishing the digest is already an S2 misreport. Version pinning is the identical mechanism pointed at the protocol instead of at the policy.

What freezes fleetmesh/0, and who says so

Nobody says so, because declaration is the only mechanism there is. fleetmesh/0.1-draft becomes canonical fleetmesh/0 when at least three independently-operated anchors declare and peer with an identical protocol digest for 30 consecutive days with zero adverse divergence reports between them. Independence is judged under the Three-Layer Operator Independence Model below.

That is an observation about the network rather than a decision taken on its behalf. The condition is checkable by anyone holding the descriptors, no announcement makes it true, and no announcement is required. Until it holds, the digest may change on any day and nobody is owed a migration — see § Alpha contract.

Starting at fleetmesh/0.1-draft

fleetmesh/0.1-draft
Where the network starts, and where it is today. It promises nothing: the schema may change on any day, and two nodes both declaring 0-draft with different digests are simply incompatible — correctly, and with neither at fault. Nobody should build anything durable against it, and saying so in the version string itself is more honest than a warning in a document people skim.
fleetmesh/0
The first frozen version. Its digest is fixed at the moment of freezing and never moves again; a change after that is fleetmesh/1, not an amended 0. What event constitutes the freeze, and who gets to say it has happened, is genuinely unresolved — see § Open questions.
fleetmesh/N
Thereafter. Versions coexist rather than superseding each other: a node may declare several and speak whichever a given peer also declares. An old version dies when the last peer willing to grant it capacity stops.
Enforcement already exists

A node that declares a version it cannot speak commits a misreport. This triggers an S2 finding under the severity ladder. The peer's signed descriptor serves as direct cryptographic evidence. No new severity class, evidence type, or out-of-band machinery is required.

What the signing key was also doing

It had three jobs, and only one of them was versioning. The other two need answers:

signing bootstrap.toml
Dropped, not replaced. The signature was ceremony over a convenience file. Safety relies on manual escape hatches: named anchors carry their own signed descriptors, verified directly during connection setup. An editable unsigned list provides equivalent operational safety without unnecessary ceremony.
urgent advisories
A security advisory becomes a report, weighted by the reader's existing trust in whoever published it, exactly like any other finding in § Trust weighting. Note the key never forced an upgrade: it was a notification channel, and a notification channel does not need to be an authority to work.
What this costs

Fragmentation moves from prevented to visible. Nothing stops the network splitting into islands that never converge. The digest only guarantees that a split is loud and immediate rather than silent and slow. Deprecation gets slower too: without a flag day, an old version dies only when peers individually stop granting it capacity, which is a local reputation decision like every other one here. A network that cannot be steered also cannot be steered well, and that is the trade. Every other decision in this document has already been made in the same direction.

What the shipped bootstrap list may contain, and when

The obvious rule — "name at least three independently-operated anchors, no operator holding a majority" — is the right end state and an impossible beginning. At genesis there is one operator. A rule nobody can satisfy is not a safeguard; it is a line everyone learns to ignore. So the composition requirement is staged, and what holds at every stage is disclosure rather than diversity:

Independent operatorsWhat bootstrap.toml MUST do
1 — genesis Carry operators = 1 and name that operator. A node reading it can then see, without asking anyone, that its first view of the network comes from a single party. That is an acceptable start and an unacceptable thing to leave implicit.
2 Name both, and MUST NOT be sole-operator. The second operator is the point at which "diverse" starts meaning something, and the file should reflect it the day it becomes true rather than at the next release.
3 or more The full rule binds: at least three, with no single operator holding a majority of the entries. From here the staging is over and the requirement is absolute.

Every stage maintains the manual escape hatch. A single --bootstrap address supplied via CLI bypasses the file completely, and builds shipping empty bootstrap files operate normally. The file is an optional convenience whose composition is disclosed. It never functions as a gate.

The three-layer operator independence model

Operator identity is an empirical socio-economic fact. Node identity is a cryptographic key. Because key generation is free, treating "independent operator" as a purely mechanical wire primitive is a category error. Fleetmesh resolves operator independence across three decoupled layers:

1. Mechanical baseline (wire diversity)
Two anchors qualify as mechanically distinct if they declare distinct Nostr operator pubkeys in kind 11801, advertise endpoints on separate BGP Autonomous Systems (ASNs) or /24 IP prefixes, and hold distinct keyAgreement keys. This protects against physical routing outages and single-datacenter failure modes.
2. Subjective leaf sovereignty (local risk allocation)
Leaf dual-mediation is an operational resilience recommendation, not an on-wire consensus gate. A leaf selects two mediators that the leaf operator believes to be independent within its own subjective trust horizon. If a leaf chooses two anchors run by the same entity, the protocol functions normally; the leaf simply assumes its own downtime and correlation risk.
3. Fraud deterrence via the severity ladder (S2 misreport)
False independence attribution is explicitly priced. If an entity operates multiple anchors under collusive shadow identities to capture bootstrap slots or deceive peers, this constitutes an S2 Misreport (structural dishonesty). Evidence of shared signing infrastructure, identical payout destinations, or collusive receipt chains is grounds for a kind 30802 adverse report downgrading the offending operator to probation or revoked.

Where this document lives

The specification is published at www.fleetmesh.org/spec.html and developed in the open. The source code, brand assets, and build configurations reside in a public repository dedicated to the public domain under CC0 1.0. This mirrors the open licensing of official Nostr NIPs. A public standard must remain unencumbered by proprietary claims, allowing independent implementations to build freely.

Changes arrive as merge requests against that repository. The line that matters is not editorial: anything altering kinds.json, messages.json or constants.json changes the version digest, and therefore changes the protocol version a conformant node declares. Prose, examples and rationale do not. Merging a patch is never by itself what makes a version real — nodes declaring it is.

Which name goes where

One rule, applied without exception: fleetmesh names the protocol, and reference implementations adhere strictly to it. A string that two independent implementations must agree on byte-for-byte belongs to the protocol; everything else belongs to whoever is accountable for the code.

fleetmesh owns reference implementation owns
DIDComm PIURIs · gossipsub topics · transport ALPNs · the fleetmesh:// invitation scheme · the kind definitions · this document crates/mesh* in reference workspace · the fleetmesh-node daemon binary · the gateway · anchors on operator hostnames

The crates use standard names without colliding with third-party implementations, and a bug report should be unambiguous about whose code it is against.

What fleetmesh.org is, and what it must never become

Domain names frequently become centralized retrieval bottlenecks for peer lists, release binaries, and schemas. A single canonical host creates an unacceptable single point of failure.

Serves, never gates

Everything fleetmesh.org hosts MUST be a convenience mirror of something that already exists as signed data reachable without DNS. No node may require it to start, to find peers, to validate a release, or to resolve any identifier. It is a faster path to bytes that are obtainable anyway — and the unplug test now deletes it too.

Served at fleetmesh.org Canonical source, always
/.well-known/nostr.json NIP-05 for the project identity. A lookup aid and nothing more: no claim in this protocol has ever been validated by a DNS answer.
/bootstrap.toml A mirror of an unsigned file — there is no signer set, per § Governance. Fetching it from here confers no authority on it whatever, exactly as if it had arrived on a USB stick. What a node verifies is each named anchor's own signed descriptor at connection setup; the list is a hint about who to ask first, and nothing else.
_seed.fleetmesh.org TXT DNS seeds, one of four discovery paths and required by none. Losing them slows a first run and does nothing to an established node.
/npk/<sha256>.npk Release artifacts, keyed by their own hash and therefore immutable. A mirror, not a registry: the bytes are verified against the signed release, so serving them confers no authority and losing them costs a download location rather than a package.
/spec, /schema This document and the kind definitions. The kinds’ real home is registry-of-kinds, which nobody here controls.
nothing else No API, no directory service, no registry, no node that other nodes are expected to reach. The moment the domain serves something with no offline equivalent, it has become the hub this design exists to avoid.

Mirror hosting is an operational convenience. A node's declared protocol digest is authoritative over external mirror files. When declarations diverge from mirror files, nodes trust the direct peer declaration and distrust the mirror.

Neutrality timeline

Fleetmesh functions today as an open project identity and protocol namespace. The domain registration is held for the project. Maintaining a dedicated, neutral domain ensures eventual stewardship transfer occurs smoothly. When governance triggers fire, the project transfers an established name rather than conducting contentious rebranding negotiations.

The trigger for revisiting

Written down so it is not a judgement call made under pressure later. The question of a neutral organisation — entity, domain transfer, formal membership — is reopened on whichever comes first:

  • A second independent implementation of this specification ships. This is also the threshold the NIPs repository requires before a submission is even eligible, so the two milestones arrive together and should be handled together.
  • A third independent anchor operator joins the mesh, evaluated under the Three-Layer Operator Independence Model (distinct Nostr operator pubkeys, separate BGP ASNs, and independent production infrastructure for ≥ 90 days).

Until one of those happens, an organisation would be one group plus paperwork, and organisations formed before their communities are difficult to unwind. After one of them happens, holding everything is no longer defensible. The trigger exists so that the transition is scheduled rather than conceded.

Identity

Node Identity & Cryptographic Keys

The DID method follows the did:nostr specification. Implementations MUST parse the identifier, verify that the 32-byte hex string represents a valid on-curve secp256k1 x-only point, and construct Multikey verification methods accordingly.

Two alternatives were considered and rejected. did:web needs DNS and a certificate, which excludes every edge and leaf and reintroduces the CA system as a dependency of federation. did:key carries no service endpoints and cannot rotate, so it can name a node but never route to one. It remains appropriate for the ephemeral session keys in § Control plane.

Three keys, one identity

The nostr key is a secp256k1 Schnorr key. DIDComm v2 requires X25519 for its ECDH-ES and ECDH-1PU envelopes, and both iroh and libp2p identify hosts by Ed25519. There is no honest way to make one key do all three, so a node holds three and binds the other two by listing them in a descriptor signed by the first.

Key Curve / codec Purpose Rotation
node secp256k1 x-only · 0xe7 The DID subject. Signs the descriptor, grants, reports, receipts, and every nostr event the node authors. Never. Rotating it creates a new node.
agreement X25519 · 0xec keyAgreement. DIDComm v2 envelope encryption, both anonymous (ECDH-ES) and authenticated (ECDH-1PU). SHOULD rotate every 90 days; overlap for one window.
transport Ed25519 · 0xed iroh NodeId and libp2p PeerId. Signs gossipsub messages under StrictSign. MAY rotate freely; a new descriptor is the only requirement.

Multikey values use the multibase f (base16-lower) prefix throughout, matching what the did:nostr draft specifies for secp256k1, so one document never mixes bases. A verifier MUST treat transport keys as bound to a DID only through currently valid descriptors. An inbound connection from an Ed25519 key not named in an active descriptor originates from an unknown node.

Three keys, or one key and a KDF

Managing three keys introduces operational backup complexity. Key loss is a primary operational failure mode on standalone hardware.

What derivation does not buy

It does not remove the binding problem. A KDF runs on the private key, so no verifier can recompute the derived public keys from the node's public key. Every peer still learns the agreement and transport keys from the signed descriptor, exactly as it does now. The descriptor, its signature and its expiry are required either way, so the "fewer things to bind" argument is illusory — there is exactly one binding artefact in both designs.

Deriving rotatable keys from a root secret requires stateful counters in KDF info strings. Losing the counter state destroys key recovery.

What it costs

compromise coupling
The transport key is the hottest of the three: it lives inside a QUIC stack talking to strangers, in libraries nobody here audited. With independent keys, stealing it lets an attacker impersonate the node's connections until the descriptor is replaced. With live derivation, the root must be present in that same process. Stealing it yields the ability to sign grants, reports, receipts, and every nostr event the node authors. One key means one blast radius, and it is the largest one.
no standard to point at
There is no blessed secp256k1→X25519 or secp256k1→Ed25519 derivation for nostr, so a scheme would have to be invented here: a KDF choice, info strings, byte orders, test vectors. The ecosystem is moving the other way — NIP-06, key derivation from a mnemonic, is marked unrecommended in the NIPs index with the note "prefer a single nsec". Inventing crypto in a document that otherwise assembles existing pieces is a poor trade for an operational convenience.
silent cross-implementation failure
Two implementations that disagree about an info string produce different node identities from the same seed. The failure mode is not an error. It is a node that simply is not the node its operator thinks it is, and every peer treats it as a stranger.
forecloses hardware custody
A node key held in a hardware signer or an HSM cannot be exported to derive from. Derivation quietly requires the most valuable key in the system to be extractable software, forever.

The resolution

Three independent keys are normative. Derivation is permitted strictly as a provisioning-time convenience. Implementations MAY derive agreement and transport keys from the node key at initialization via HKDF-SHA256(ikm = node_sk, info = "fleetmesh/node/<role>/<counter>"). Implementations MUST persist derived keys to independent storage and MUST NOT retain the root secret in active network processes. This provides single-secret backup while maintaining cryptographic key separation at runtime.

Stated as a rule: derive at provisioning, never at runtime. An implementation that keeps the root online to re-derive on demand has taken the cost and thrown away the mitigation.

The node descriptor — kind 11801

Replaceable, one per node, signed by the node key. It is the sole input, beyond the kind 0 and 10002 events did:nostr already consumes, that turns a bare public key into a resolvable, routable node. Resolvers MUST reject a descriptor whose pubkey is not the DID subject.

kind 11801 — node descriptor

{
  "kind": 11801,
  "pubkey": "<node secp256k1 pubkey, hex>",
  "created_at": 1787000000,
  "content": "Community relay for the Bristol nostr meetup.",
  "tags": [
    ["class", "edge"],
    ["protocol", "fleetmesh/0.1-draft", "6d8c8126a619e25c…"],   // the live digest
    ["software", "https://fleetmesh.org", "0.2.0"],

    ["key", "agreement", "fec01<x25519 pubkey hex>", "1795000000"],
    ["key", "transport", "fed01<ed25519 pubkey hex>"],

    ["endpoint", "iroh",   "<iroh NodeAddr ticket>",        "10"],
    ["endpoint", "libp2p", "/dnsaddr/relay.example.org/p2p/12D3Koo…", "20"],
    ["endpoint", "wss",    "wss://relay.example.org",              "40"],
    ["mediator", "did:nostr:<anchor pubkey>"],

    ["c", "https://fleetmesh.org/peering/1.0"],   // the protocols it implements; these two are required
    ["c", "https://fleetmesh.org/budget/1.0"],
    ["c", "https://fleetmesh.org/sync/1.0"],
    ["c", "relay"], ["c", "blossom"],      // plain capabilities are hints
    ["c", "mediator"], ["c", "negentropy"],
    ["nips", "1", "2", "9", "11", "17", "40", "42", "44", "45", "50", "59", "70", "77"],
    ["coverage", "<sha256 of the coverage claim set>"],
    ["policy", "<sha256 of the operator policy document>", "https://example.org/policy.txt"],
    ["operator", "<operator nostr pubkey>"],
    ["expiration", "1787604800"],
    ["nonce", "<NIP-13 proof-of-work nonce>", "20"]
  ]
}

protocol is mandatory and is the first thing a peer reads. Its second element is the version digest defined in § Governance, and it is what the handshake actually compares — the name beside it is for humans. A descriptor without a protocol tag is not a fleetmesh node descriptor, and a node whose behaviour contradicts the version it declares has misreported itself: S2.

endpoint's fourth element is a preference value, low first; it is the input to transport selection in § Transports. expiration is NIP-40 and is mandatory here — a descriptor that outlives its operator's interest is how stale federations accumulate dead peers. Maximum lifetime is 30 days for anchors and edges, 24 hours for leaves. policy pins the human-readable rules the operator claims to follow; changing the document without republishing the digest is itself an S2 misreport. operator binds the node to its declared human or organizational Nostr pubkey. Nodes operated by the same entity MUST declare the same operator pubkey; declaring false independence (e.g. running shadow nodes under fake keys to subvert leaf dual-mediation or capture bootstrap slots) is a structural breach priced directly as an S2 misreport.

The resolved DID document

A did:nostr resolver gains a node profile: given a DID whose subject has a live kind 11801, it emits the enhanced document below instead of the minimal one. Resolution of ordinary user identities is unaffected, so a resolver can add this without changing any answer it already gives.

GET /resolve/did:nostr:<hex> — node profile

{
  "@context": ["https://www.w3.org/ns/cid/v1", "https://w3id.org/nostr/context"],
  "id": "did:nostr:<hex>",
  "verificationMethod": [
    { "id": "#nostr", "type": "Multikey", "controller": "did:nostr:<hex>",
      "publicKeyMultibase": "fe70102<x>" },
    { "id": "#agree", "type": "Multikey", "controller": "did:nostr:<hex>",
      "publicKeyMultibase": "fec01<x25519>" },
    { "id": "#wire",  "type": "Multikey", "controller": "did:nostr:<hex>",
      "publicKeyMultibase": "fed01<ed25519>" }
  ],
  "authentication":  ["#nostr"],
  "assertionMethod": ["#nostr"],
  "keyAgreement":    ["#agree"],
  "service": [
    { "id": "#didcomm", "type": "DIDCommMessaging",
      "serviceEndpoint": {
        "uri": "wss://relay.example.org",
        "accept": ["didcomm/v2"],
        "routingKeys": ["did:nostr:<anchor>#agree"]
      } },
    { "id": "#relay",   "type": "NostrRelay",   "serviceEndpoint": "wss://relay.example.org" },
    { "id": "#blossom", "type": "BlossomServer","serviceEndpoint": "https://blobs.example.org" },
    { "id": "#iroh",    "type": "IrohNode",     "serviceEndpoint": "<ticket>" }
  ]
}

Revocation — kind 11802

A compromised or retired node publishes a replaceable Kind 11802 event declaring revocation and an optional successor DID. Revocation events prove only that the key holder requested retirement. Legitimate operators and compromise adversaries can both publish revocations. Peers MUST zero active grants upon receiving a valid revocation and MUST NOT automatically transfer accumulated standing to replacement keys. A successor node starts on probation and earns standing through clean windows.

kind 11802 — node revocation

{
  "kind": 11802,
  "pubkey": "<the node key being retired>",
  "tags": [
    ["reason", "key-compromise"],   // free text; retired | key-compromise | migrated
    ["successor", "<pubkey of the replacement node>"]
  ],
  "content": "Host decommissioned; storage destroyed."
}

successor is advisory and carries no standing whatsoever. A peer that follows it grants the successor probation like any stranger, because a revocation proves only that whoever holds the key asked to retire it — which is exactly what an attacker who has stolen it would also do. Treating a successor as inheriting the predecessor's tier would make key theft an upgrade path.

Discovery

Peer Discovery Without Central Registries

A new node needs three things: somewhere to publish its descriptor, a way to be told about other descriptors, and a first peer willing to grant it anything at all. None of them may require any centralized service, or the mesh is a hub with extra steps.

Bootstrap

  1. Publish. The node writes its kind 11801 descriptor to every relay in its own kind 10002 relay list. Any nostr relay will do; the descriptor is a normal replaceable event and needs no special support.
  2. Announce. The node joins the libp2p gossipsub topic /fleetmesh/announce/1 and republishes the descriptor there, signed under StrictSign by the transport key. Gossipsub carries the descriptor to nodes that have never shared a relay with it, which no relay-published descriptor can do. It does not remove the need for an introduction: reaching the topic at all takes one peer already known, from the bootstrap list or from an operator. Every path in this list begins somewhere else.
  3. Bootstrap is editable. Operators can edit or empty bootstrap.toml freely. Builds shipping an empty bootstrap list MUST operate given a single --bootstrap CLI flag. Shipped list composition rules are defined in § Governance.
  4. Ask. Pick a peer whose descriptor declares the protocols you need, then run the peering handshake in the next section. https://didcomm.org/discover-features/2.0 confirms the declaration once a channel exists; it does not replace it. A peer SHOULD answer a peers-query with descriptors it holds, capped and rate-limited under the asker's grant like any other request.

Both events are published to standard Nostr relays to maintain auditable reputation. Nodes without a libp2p stack participate fully over standard WebSocket connections.

Four paths, none required

Note that steps 1–4 are alternatives, not a pipeline. Relays alone are enough. Gossipsub alone is enough. One --bootstrap address typed by hand is enough. A node that can reach any nostr relay in the world can find the mesh, which is what makes the unplug test in § Two networks passable rather than aspirational.

Enrolment, and why it can be open

A node may accept peering requests from strangers. Safety relies on initial grants carrying minimal capacity rather than identity being expensive. Probation grants restrict capacity so a thousand sybil nodes consume less bandwidth than one active peer. Reputational standing accumulates strictly through clean windows over physical time, which cannot be accelerated by generating keys.

One optional admission cost is defined, and it is a refinement rather than load-bearing. Proof of work: NIP-13 on the descriptor, with difficulty declared by the admitting node in its policy document. Default 20 bits: seconds on a laptop, meaningful at ten thousand identities.

No bonds

An anchor MUST NOT require a Lightning deposit forfeit on an S4 finding, or any other bond. Slashing means one node keeping another's money on the strength of evidence it produced itself. That needs an arbiter, and an arbiter is the centralised authority this whole design is arranged to avoid. The deterrent it was reaching for is already present and does not need custody of anyone's funds: a probation grant is worth almost nothing, and standing is bought only with observed time. See § Grants & receipts for what replaces it.

Closed federations are the same mechanism with the door shut: an operator whose policy is invitation-only simply never issues a grant that was not preceded by an out-of-band invitation. The protocol does not distinguish the two cases, and no node can tell from the outside whether another is open — it can only observe whether it got a grant.

Invitation

Scanning an invitation QR code on a mobile device enrolls the leaf immediately. The invitation encodes the anchor's DID and mediator endpoint, eliminating accounts, registration forms, and passwords.

Coverage — kind 30803

Discovery of data is separate from discovery of nodes. A node publishes one addressable kind 30803 per shard it claims to hold complete, where a shard is a filter plus a time range, and the d tag is a stable shard identifier.

kind 30803 — coverage claim

{
  "kind": 30803,
  "tags": [
    ["d", "bristol-meetup-2026H2"],
    ["filter", "{\"kinds\":[0,1,3,7,10002],\"#t\":[\"bristol\"]}"],
    ["since", "1751328000"], ["until", "1767225600"],
    ["completeness", "asserted"],          // asserted | best-effort
    ["fingerprint", "<negentropy root over the shard>", "1787000000"],
    ["count", "418902"]
  ],
  "content": ""
}

asserted is a falsifiable claim, and that is the point. Any peer can sample the shard, find a hole, and file a report carrying the missing event's id as evidence. A node unwilling to be held to it publishes best-effort, which no one may report against and no one may rely on. Leaves MUST NOT publish coverage claims at all — a device that is offline whenever its screen is has nothing to claim.

Gossip topic membership — kind 30804

A group conversation on the mesh is a gossip/1.0 topic: a set of nodes that flood one another's already-encrypted events without any relay in the path. Who is in the set is never inferred from traffic. The topic admin publishes one addressable kind 30804 naming every member, and a node grafts into (or accepts a graft for) a topic only while the current event at that coordinate names its own pubkey. Removing a member is publishing a new version that omits them; a republished roster supersedes the one before it exactly as a grant envelope or a coverage claim does.

Delivery has an eager and a lazy half, as in gossipsub. Eager: a publish is flooded to the node's mesh peers for the topic, a bounded set (degree D) grafted from the roster. Lazy: each tick a node offers the ids it has recently seen (ihave) to a few roster members outside its mesh, and a member missing any of them asks by id (iwant) and is sent the sealed envelope directly from a short message cache. The lazy half is what carries a message across a partitioned mesh — grafts that failed, a topic larger than D that split into cliques, a member that was down for the flood — still with no relay in the path. Both halves carry only NIP-59 wraps; the durable seen-id store is the sole loop-prevention authority for both.

kind 30804 — gossip topic membership

{
  "kind": 30804,
  "tags": [
    ["d", "<admin pubkey>:bristol-ops"],
    ["member", "<member pubkey>"],         // one tag per member, repeatable
    ["member", "<member pubkey>"],
    ["member", "<member pubkey>"],
    ["expiration", "1790000000"]
  ],
  "content": ""
}

The d coordinate is scoped to the admin's pubkey, so a different key cannot shadow a roster it does not own. expiration is a NIP-40 timestamp: an expired roster stops authorising new grafts and the topic's mesh peers are pruned on the next tick. Who may become admin, and any delegation of that role, is the application's to decide.

Liveness — kind 21801

Ephemeral, unstored, and the only thing an anchor is obliged to publish on a schedule. A heartbeat says a node is up and what it currently looks like — and, deliberately, nothing about who it peers with. It is what replaces the positive attestation § Grants & receipts forbids.

kind 21801 — node heartbeat

{
  "kind": 21801,
  "tags": [
    ["class", "anchor"],
    ["endpoints", "<sha256 over the node's current endpoint set>"],
    ["load", "0.42"]
  ],
  "content": ""
}

class is the only required tag, and it MUST match the node's live descriptor — a heartbeat contradicting the descriptor is a misreport about itself, and an S2. endpoints is a digest over the endpoint set the node is actually serving — SHA-256 over the canonical JSON of the sorted [scheme, address] pairs, preference excluded — so a peer holding a descriptor can tell it has gone stale without dialling every address in it. load is an advisory scalar with no normative meaning; nothing may be reported against it. The heartbeat carries nothing about accounting, deliberately: a node holds one receipt chain per issuer, so a single "latest head" has no referent, and any commitment a peer could actually open would name that peer. The head an issuer holds travels privately in budget/1.0/receipt-ack.

An anchor MUST publish one at least once an hour, the default window length, and at least once per its shortest active grant window if that is shorter. Edges SHOULD; leaves MUST NOT — a device that is offline whenever its screen is has no liveness to assert, and § Privacy rules would rather it did not announce its waking hours.

Third-party liveness is separate and better. NIP-66 monitors publish kind 30166 status events about mesh nodes using ordinary relay monitoring flows, needing no permission from the node and reading only public data. A measurement somebody else took is worth more than a claim the subject published about itself, which is why the heartbeat carries as little as it does.

Control plane

Private Node Communication via DIDComm v2

Nostr events are broadcast and public, which is right for descriptors, grants and reports and wrong for the conversation that produces them. Negotiating a peering. Agreeing a sync range. Presenting evidence to a peer before publishing a complaint. Asking a mediator to hold messages. Each is an addressed, threaded, private exchange between two named parties. DIDComm v2 provides sender-authenticated encryption, message threading, transport independence, and established mediation protocols out of the box, avoiding bespoke reimplementations.

Standard protocols, used unmodified

Protocol Used for Required of
out-of-band/2.0 Invitation URIs and QR pairing. anchor, edge
discover-features/2.0 Capability query once a channel exists. Its answer MUST agree with the protocol identifiers in the descriptor's c tags, which are binding; a disagreement is BAD_DESCRIPTOR. The plain capability words beside them are hints. all
trust-ping/2.0 Liveness and round-trip measurement on an established channel. all
coordinate-mediation/2.0 Requesting mediation and being granted it. The registered protocol for the handshake that routing/2.0 then carries traffic over. anchor (as mediator), leaf
routing/2.0 Mediation. The mechanism by which a leaf has an address at all. anchor (as mediator), leaf (as recipient)
messagepickup/2.0 A leaf asking its mediator what is held for it and taking delivery. The other half of mediation: routing/2.0 brings a message to the mediator, this brings it the rest of the way when the leaf is back. anchor (as mediator), leaf
report-problem/2.0 Transport and protocol errors. Not the reputation channel — a malformed message is a bug, not an offence. all
empty/1.0 Headers with no body. It carries the ack a stored message asked for with please_ack when no other message is due. A message that is never acknowledged by a peer that has spoken since is how a node learns a relay dropped the stored path. all

Six new protocols

Defined under https://fleetmesh.org/… because nothing registered covers them. Each is a small state machine; each message is a DIDComm plaintext message encrypted ECDH-1PU to the peer's #agree key, except first contact, which is ECDH-ES anonymous.

A DIDComm PIURI is an identifier, never fetched, so this namespace creates no runtime dependency on a domain anyone controls — a lapsed registration breaks nothing. It does put a company's name on a protocol the mesh is meant to outlive. That is a naming problem rather than an engineering one, and § Open questions lists it as such. Implementations MUST treat the string as opaque and compare it byte-for-byte.

PIURI Messages Purpose
peering/1.0 propose, offer, accept, decline, amend, terminate Establish and revise a peering. Terminates in a pair of grants, one per direction.
sync/1.0 open, range, have, need, deliver, done Set reconciliation over a shard, carrying NIP-77 negentropy payloads.
budget/1.0 receipt, receipt-ack, close, dispute The self-accounting channel. Carries receipts and window closes.
complaint/1.0 notice, evidence, remedy, appeal, withdraw Present a finding to its subject before publishing it, and let the subject answer.
standing/1.0 ask, tell, decline Ask a peer what it makes of a third node, now that nobody can read it from a relay. Answering is discretionary and refusing is normal. See § Trust weighting.
compute/1.0 quote, accept, sealed, settled, refute Negotiate and deliver one compute job. Quotes stay off the wire as events so a seller’s price to one buyer is not a public commitment to every buyer. See § Compute market.
Design rule

complaint/1.0 is mandatory-first for S1 and S2: an issuer MUST deliver a notice and wait one window before publishing a kind 30802 report against a reachable peer. Most conformance failures are clock skew, a bug, or a misread limit, and a protocol that publishes before it asks produces a permanent public record of transient faults. S3 and S4 may be published immediately.

peering/1.0/propose — plaintext, before encryption

{
  "id": "6f2b…", "type": "https://fleetmesh.org/peering/1.0/propose",
  "from": "did:nostr:<proposer>", "to": ["did:nostr:<target>"],
  "created_time": 1787000123, "expires_time": 1787003723,
  "body": {
    "class": "edge",
    "protocol": ["fleetmesh/0.1-draft", "<digest>"],
    "protocols": ["https://fleetmesh.org/peering/1.0",   // what this node implements, and
                   "https://fleetmesh.org/budget/1.0",    // the same list its descriptor
                   "https://fleetmesh.org/sync/1.0"],     // declares as c tags
    "want": [ { "shard": "bristol-meetup-2026H2",
                "filter": {"kinds":[0,1,3,7,10002],"#t":["bristol"]},
                "mode": "pull", "since": 1751328000 } ],
    "offer": [ { "shard": "bristol-meetup-2026H2", "completeness": "asserted",
                 "fingerprint": "<negentropy root>" } ],
    "transports": ["iroh", "libp2p", "wss"],
    "budget_self": { "events_in": 600, "bytes_in": 8388608, "window": 3600 },
    "policy": "<sha256 of proposer policy doc>",
    "pow": { "bits": 20, "event": "<descriptor event id>" }
  }
}

Peering acceptance occurs when the recipient publishes a reciprocal grant. A peering consists of two independent bilateral grants negotiated concurrently. Either operator retains the sovereign right to reduce its outbound grant at any time.

Transport binding, including the one that always works

A DIDComm message is a self-contained encrypted envelope, so its transport is interchangeable. In preference order:

iroh stream
ALPN fleetmesh/didcomm/1. One bidirectional QUIC stream per thread. Default whenever both nodes advertise iroh.
libp2p stream
Protocol id /fleetmesh/didcomm/1.0.0 over Noise + yamux. Used when iroh is unavailable or when a circuit relay is already established.
HTTPS POST
POST /didcomm, Content-Type: application/didcomm-encrypted+json, per the DIDComm HTTPS binding. Anchors MUST implement it; it is the only transport a corporate network reliably permits.
nostr gift wrap
The envelope is placed in a NIP-59 gift wrap addressed to the peer's node pubkey and published to a relay both nodes use. Slow, high-latency, unconditionally available — a node that can reach one relay can always reach any other node. Every implementation MUST support it as the terminal fallback. Which of the two gift wrap kinds to use is specified below.

The last row is what keeps this from being a second network beside nostr. If iroh, libp2p and HTTPS all fail, federation does not stop; it degrades to something every nostr relay in the world already forwards. The cost is latency and a public record that two node keys exchanged an envelope of a certain size at a certain time, which is why it is last.

Which gift wrap

NIP-59 defines two: kind 1059, stored by relays, and kind 21059, ephemeral and explicitly intended for real-time use. The choice is by message, not by node:

  • 21059 for anything belonging to a live exchange — a sync session, a trust ping, receipts flowing during an open window, discovery queries. Both endpoints are online by construction, so persistence buys nothing, and using the ephemeral kind keeps the mesh's control traffic off every relay's disk. This is the default.
  • 1059 only when the message must survive the recipient being offline: a grant amendment, a complaint notice, a window close the peer has not acknowledged. These are rare, small, and genuinely need store-and-forward.

Nodes MUST NOT use Kind 1059 as a bulk transport. It is an encrypted message class, not a high-throughput queue. A peer flooding relays with uncollected control envelopes consumes third-party storage without authorization, triggering an S1 overrun finding.

Neither kind is this protocol's. NIP-59 does not namespace what is inside a wrap, so a subscription for {kinds:[21059], "#p":[self]} receives every NIP-59 wrap addressed to that key, whatever protocol wrote it. One machine running fleetmesh and another NIP-59 protocol under one key will see each other's envelopes; both directions fail closed, because the inner payload is not a JWE this node can read, but the drop is real and an operator should be told what it was rather than left reading it as a peer sending malformed envelopes. Implementations SHOULD say which protocol a rejected payload appears to belong to. A node that wants the ambiguity gone runs its control traffic under a key it uses for nothing else.

Why this matters beyond tidiness

NIP-59 notes that gift wraps are signed by random one-time keys, so a relay cannot apply pubkey-based rate limiting or reputation to them at all. Control traffic that a relay cannot attribute and cannot drop is exactly the thing this specification asks nodes not to send each other, and it would be incoherent to exempt ourselves. Ephemeral by default is the version of that rule we can actually keep.

Mediation is how a phone gets an address

A leaf establishes mediation over routing/2.0 with a peered anchor. The anchor serves as mediator, and the leaf advertises the mediator's key in its descriptor routingKeys. Inbound messages arrive at the anchor wrapped in DIDComm forward envelopes. The anchor holds forwarded envelopes until the leaf reconnects. Anchors cannot decrypt payload contents: the inner envelope is encrypted exclusively to the leaf's #agree key. Because mediators observe message timing and metadata, leaves SHOULD maintain dual-mediation across independent anchors.

Cold-Start Mediator Discovery via NIP-66

A mobile leaf initializing for the first time without peers, without an initial QR invitation, and with no native P2P gossipsub stack discovers its initial mediators purely through standard Nostr queries:

  1. Directory Discovery: The leaf connects over wss:// to public Nostr bootstrap relays (or relays from the user's NIP-65 relay list) and queries for active mediator anchors:
    { "kinds": [11801], "#c": ["mediator"] }
  2. Independent Anchor Selection: The leaf inspects the returned descriptors, filtering for anchors that support WebSocket endpoints (wss://), and selects two anchors publishing distinct operator pubkeys (satisfying the 3-Layer Operator Independence Model).
  3. Admission Handshake: The leaf attaches a 20-bit Proof-of-Work to its ephemeral descriptor and transmits a DIDComm coordinate-mediation/2.0/mediate-request. The mediator admits the leaf at tier: probation, establishing mediated message forwarding with zero proprietary coordination servers.

Transports

Modular Pluggable Transports

The core protocol is strictly independent of transport mechanisms. DIDComm v2 enveloping, NIP-77 negentropy reconciliation, bilateral grant accounting, and the severity ladder operate across any byte-stream carrier. All wire I/O is abstracted behind the MeshTransport adapter interface. This enables fleetmesh to run seamlessly across server clusters, mobile leaves, field hardware, and privacy overlays without code changes.

The reference implementation combines both iroh and libp2p for complementary roles. Pairwise synchronization requires high-throughput, content-verified streaming between known peers (iroh QUIC and BLAKE3 verification). In contrast, network-wide discovery requires broadcast across unacquainted nodes (libp2p gossipsub v1.1).

The MeshTransport Trait Interface

crates/mesh — Modular Transport Trait

#[async_trait]
pub trait MeshTransport: Send + Sync + 'static {
    /// Scheme identifier advertised in descriptors (e.g. "iroh", "libp2p", "wss", "unix", "ble")
    fn scheme(&self) -> &'static str;

    /// Listen on an advertised endpoint
    async fn listen(&self, endpoint: &TransportEndpoint)
        -> Result<Box<dyn TransportListener>, TransportError>;

    /// Dial an outbound peer given their advertised descriptor endpoint
    async fn dial(&self, target: &TransportEndpoint, peer_key: &PublicKey)
        -> Result<Box<dyn TransportStream>, TransportError>;
}

Standard & Extensible Scheme Registry

Scheme Category Carries Target Environments & Notes
iroh built-in Pairwise: DIDComm streams, negentropy reconciliation, blob transfer. QUIC with relay-assisted hole punching (Anchor ↔ Anchor, Edge ↔ Edge). NodeId is the descriptor's transport key (Ed25519), per § Identity.
libp2p built-in Broadcast: descriptors, grants, reports. Discovery via Kademlia. gossipsub v1.1 with StrictSign; circuit-relay-v2 + DCUtR when iroh hole punching fails.
wss / https built-in Universal fallback and mediated leaf synchronization. Standard Nostr WebSocket frames + gift-wrapped DIDComm. Zero native dependencies; ideal for mobile leaves.
unix built-in Zero-copy local inter-process communication. Unix domain sockets (/var/run/fleetmesh/node.sock) for co-located multi-process relays on the same host.
webrtc adapter In-browser direct client-to-relay sync. libp2p-webrtc / WebTransport for browser execution contexts without raw TCP/UDP socket access.
tor / i2p / nym adapter Anonymized and metadata-resistant traffic routing. End-to-end hidden services (.onion) for high-adversary or censorship environments.
ble / lora adapter Off-grid and disaster-recovery field synchronization. Bluetooth Low Energy / LoRa packet radio meshes (e.g. Reticulum integration) for local air-gapped sync.

Negotiation & Fallback Ladder

Nodes select transports collaboratively during connection setup. Both peers parse descriptor endpoint tags, compute the intersection of supported schemes, and rank candidates by the sum of advertised preference weights. Neither peer unilaterally dictates the transport. Equal sums rank by scheme name, ascending bytewise (transport.ladder_tie_break in constants.json), so two implementations pick the same rung for the same pair of descriptors.

Initiators attempt the highest-priority candidate with a 5-second timeout, falling through sequentially upon failure. A node MUST NOT report a peer for unreachability until the entire mutually supported transport ladder is exhausted. Nodes MUST cache negotiated transports for subsequent sessions.

Transport fallback is unidirectional within a single session. Having fallen back (e.g. to wss), a node retries higher-priority transports on subsequent sessions rather than mid-stream. Re-probing failed connections during active synchronization is prohibited; connection storms are debited directly against the initiator's token bucket budget.

Content Addressing & CIDs

Blossom blobs are SHA-256 addressed, mapping directly to IPFS Content Identifiers (CIDs): multibase b, CIDv1, raw codec 0x55, multihash sha2-256. Nodes MUST expose that mapping, providing seamless interoperability with IPFS pinning services without mandating IPFS as an active runtime dependency.

Not a transport

Whatever a multi-process relay uses internally to move live events between its own instances — a message bus, a shared channel, a database notification — stays internal. That is an intra-deployment concern with intra-deployment trust. Mesh egress is a separate path with its own shaping and its own accounting, and a peer never joins anyone's internal bus.

Replication

Replication Boundaries & Synchronization Rules

Nostr events are immutable and self-authenticating, so replication is set union over a grow-only set, with deletions and replaceable-supersession as the two exceptions. There is no ordering guarantee beyond created_at, no consensus, and no notion of a primary. Two nodes that have exchanged every event in a shard hold identical sets; two that have not are both correct and simply behind.

Interest contracts

A peer does not receive a firehose. It declares an interest — a filter, a time range and a direction — and the grant is scoped to it. Pushing an event outside the interest is an S3 finding, not merely inefficient: it is the mechanism by which a misbehaving peer would try to make its traffic someone else's problem.

  • pull — the peer fetches; the issuer serves within budget.
  • push — the peer sends; every event MUST match the interest filter.
  • both — symmetric, and the common case between anchors.

Reconciliation uses NIP-77 negentropy. A node MUST support NIP-77 to peer. Range synchronization is an essential protocol prerequisite. The legacy alternative—re-broadcasting timestamp windows and discarding duplicates—wastes bandwidth and renders duplicate-ratio reputation metrics ineffective.

Pluggable Storage & Deterministic Range Fingerprints (MeshStore)

To allow existing relays (e.g. strfry, khatru, nostr-rs-relay) to join the mesh without changing their underlying database engine, all event storage, retrieval, and range indexing is abstracted behind the async MeshStore trait.

crates/mesh — Storage & Range Fingerprint Trait

#[async_trait]
pub trait MeshStore: Send + Sync + 'static {
    /// Retrieve an event by its 32-byte identifier
    async fn get_event(&self, id: &[u8; 32]) -> Result<Option<Event>, StorageError>;

    /// Insert an independently authenticated event into local storage
    async fn insert_event(&self, event: &Event, now: u64) -> Result<InsertStatus, StorageError>;

    /// Check presence of an event without decoding its payload
    async fn has_event(&self, id: &[u8; 32]) -> Result<bool, StorageError>;

    /// Calculate a deterministic NIP-77 range fingerprint over (filter, since, until)
    async fn fingerprint_range(&self, filter: &Filter, since: u64, until: u64, now: u64)
        -> Result<[u8; 16], StorageError>;

    /// Enumerate all event IDs matching a filter within a timestamp interval
    async fn enumerate_range(&self, filter: &Filter, since: u64, until: u64, now: u64)
        -> Result<Vec<[u8; 32]>, StorageError>;
}

Deterministic range fingerprinting. The fingerprint is NIP-77's, not a second one. fleetmesh defines no fingerprint of its own, because a node that speaks NIP-77 already computes this and a second construction over the same data is one more thing for two implementations to disagree about.

range fingerprint — NIP-77 Negentropy V1, restated for completeness

acc  := Σ (id as 32-byte little-endian uint) mod 2256
fp   := SHA256(acc as 32 bytes little-endian || varint(count))[0..16]

varint is base-128, most significant digit first, high bit set on all
but the last byte. 16 bytes on the wire, and the count is what stops the
additive structure colliding on cardinality alone.

It is a sum, and that is the point. Set reconciliation works by splitting a range and comparing halves. Because addition composes, the fingerprint of a range is the sum of its sub-ranges: a split is an index lookup, and a node can maintain running sums rather than re-reading events. An order-dependent chained hash would force a re-hash of every element in every new sub-range at every level of the split, which is the wrong shape for the protocol this document already requires. It also makes the fingerprint order-independent, so no sort rule is needed to compute it — sorting by (created_at ASC, id ASC) remains required for enumerate_range, whose output order is observable.

Which events are in the set. Pinning the algorithm is only half of agreement; the other half is membership, and two backends can each compute NIP-77 correctly over different sets and never converge. Four rules decide it, and each is a place where a locally reasonable choice silently breaks reconciliation. They are normative and live in constants.json under store.

  • Ephemeral kinds are never stored (20000 ≤ n < 30000), so they never enter a fingerprint.
  • Replaceable and addressable kinds keep only the winner — later created_at, and on a tie the lexicographically lower id. Retaining a superseded version is not a local choice, because it changes the fingerprint.
  • A NIP-09 deletion removes only events its own author signed, and removes them permanently: the id is tombstoned so a later re-offer cannot resurrect it. The kind 5 is itself stored and replicates, because a peer that never receives it never learns of the deletion. A deletion that arrives before its target still binds, so a tombstone records which key asked — the authority check needs the target's author, and that is not known yet. Checking it on arrival instead is what makes the outcome independent of the order the two events turned up in.
  • An expired event is not in the set. Two peers evaluating either side of that instant disagree for as long as their clocks differ, bounded by the same 60 second skew allowance the accounting uses. This is why the range methods take now.

The since and until arguments bound the time axis, and a filter's own since/until narrows further: the intersection, so that neither can silently widen the other.

Every storage backend — LMDB, SQLite, PostgreSQL, flat files — MUST produce byte-identical fingerprints for identical event sets. How it does so is deliberately unspecified: a prefix-sum index, a segment tree over the timestamp axis, or a naive scan are all conformant, because only the value on the wire is normative. An implementation is free to make range splits O(1) and most should.

Measured

What reconciliation costs depends on how the differences are distributed, not only on how many there are. § Why join claims a node offline for a week catches up in kilobytes of fingerprints. Against 10,000 events that holds, and the reason it holds is that being offline produces a contiguous gap:

DifferenceRound tripsBytesAgainst 32 bytes per id
none — the sets match1337—
3, scattered22,81929×
500, contiguous and recent217,1531.1×
500, scattered through the history2142,3028.9×

The last row is the honest caveat. When differences are spread thinly through the whole range almost every sub-range differs, the split degenerates towards exchanging the ids themselves, and reconciliation costs several times what sending the ids would have. That is a property of range-based reconciliation rather than of this implementation: varying the split arity between 8 and 32 moves the figure by under ten percent while trading bytes against round trips. A node catching up after downtime is in the third row. A node whose peer has been quietly withholding scattered events is in the fourth, and the withholding case in § Attack surface should be read with that cost in mind.

Executable

ref/negentropy.py implements NIP-77 V1 over this store — the message encoding, the bound rules, and reconciliation for both sides — and core/tests/test_negentropy.py is where the figures above come from. It has not been run against hoytech's reference implementation, because there is no binary here to run it against; what is checked is that the encoder round-trips, that it reproduces the frames pinned in vectors/negentropy.json, and that sixty seeded random set pairs converge on exactly the right difference.

ref/store.py implements this against two backends, an in-memory map and SQLite, and core/tests/test_store.py is the shared suite the § Conformance section requires: the same assertions run against each, then both fed one history in a hundred shuffled orders and required to produce one fingerprint. vectors/store.json pins the corpus, the enumeration and eight range fingerprints, so a third backend can check itself without running any of this.

The delivery envelope

Events crossing between nodes are wrapped, and the wrapper is where accountability lives. Every node that forwards an event appends its signature over (event_id, prev_hop, at). A receiver therefore knows not just who handed it the event but the whole chain back to the node that first put it into federation, and when each of them passed it on. at is inside the signature because a field a signature does not cover is a field anyone may rewrite: without it a hop could be lifted onto another moment, and a path proved that a node forwarded an event but never when. It is also the only thing a forwarder signs for an event it did not write, which is what lets an envelope stand as evidence against the forwarder.

A sync/1.0/deliver carries its events in these envelopes and nowhere else, one per event, and its body is the count. A receiver MUST refuse a deliver that carries no delivery-envelope attachment, and MUST NOT store an event no verifying envelope covers. The rule is not tidiness. An event accepted without provenance is an event whose delivery can never be reported: a report may cite an envelope, or an event the peer itself authored, and a third party's event with no envelope is neither, so omitting the attachment would be a free way to commit the interest offence unpunishably. Carrying the events in the body as well, which an earlier draft did, cost twice the bytes for one copy nobody read — and those bytes are metered against the receiver's own grant.

delivery envelope — sync/1.0/deliver attachment

{
  "event": { … the nostr event, unmodified and independently verifiable … },
  "path": [
    { "node": "did:nostr:<origin>",  "at": 1787000001, "sig": "…" },
    { "node": "did:nostr:<relayer>", "at": 1787000004, "sig": "…" }
  ],
  "grant": "<event id of the grant this delivery is charged against>",
  "window": 496389
}

what a hop signs — delivery.hop_signature in constants.json

msg := SHA256("fleetmesh/hop/1" || "|" || event_id || "|" || prev_hop || "|" || at)
sig := BIP-340(node_key, msg)

event_id   the wrapped event's id, 64 lowercase hex
prev_hop   the previous path entry's node string, exactly as written; empty for the first hop
at         this hop's own unix seconds, in decimal

The domain label keeps a hop signature from being mistaken for a nostr event id or for anything else the same key signs. A path longer than max_forward_hops (three) is rejected, and a peer that keeps sending them is committing the hop-limit S3 the ladder already names. vectors/envelope.json pins a two-hop path under deterministic keys, so an implementation can check its messages and signatures byte for byte.

Transitive accountability

This prevents grant-evasion routing (e.g. node B relaying traffic through node C to bypass node A's budget limits). Routing accountability is explicit: operators are directly responsible for originated traffic and forwarded transit.

Deletions and supersession

  • NIP-09 deletion authority. Stores MUST verify that NIP-09 deletions target events authored by the deletion signer. This ownership check ensures safety: a hostile peer's Kind 5 deletion cannot alter another author's data, regardless of forwarding path.
  • Replaceable and addressable supersession is resolved by created_at, then by lexicographically lower event id, exactly as NIP-01 specifies. Peers MAY exchange only the winner.
  • NIP-40 expirations are honoured on receipt. A node MUST NOT accept an already-expired event from a peer, and a peer that sends them in bulk is backfilling garbage — S1.
  • A deletion is a request, not an erasure guarantee. This document does not pretend otherwise, and neither should any interface built on it.

Blobs

Blossom BUD-04 (PUT /mirror) provides HTTP fallback. Servers MUST verify the authorization event's x tag against the computed SHA-256 hash of the received payload. Nodes MUST NOT credit unverified blobs toward coverage claims. Nodes SHOULD reject blobs lacking associated local events to prevent unauthorized CDN exploitation.

Never replicated

The full list is in § Two networks, because most of it is a boundary rule for operators bridging an existing estate. Two entries are protocol-level, and every node enforces them against every peer whether or not any other service is anywhere nearby:

  • Kind 24133 — NIP-46 signer traffic. Ephemeral, and federating it would let any node observe every pairing and every signing event of every bunker in reach. MUST NOT cross, on any transport, under any grant.
  • Kind 1059 — gift wraps replicate only to nodes named in the recipient's own kind 10050 DM relay list, a check any node can perform from public data. Never in bulk, and never on a filter a third party supplied.

Symmetrical Ingress & Egress Policy Seams (pluginIn / pluginOut)

An operator has as much reason to govern what leaves their relay as what arrives. In addition to NIP-77 interest filters, fleetmesh specifies symmetrical policy seams on both ingress and egress paths. These seams allow external policy engines or plugins (compatible with strfry write-policy and nostr-router) to evaluate, filter, and redact traffic dynamically.

Direction Hook / Seam Trigger & Payload Context Permitted Actions
Ingress pluginIn / write-policy Fires when an inbound event arrives over a peering or delivery envelope before committing to local store: {"action": "ingress", "event": {...}, "source": {"did": "did:nostr:...", "tier": "member"}} {"action": "accept"}
{"action": "reject", "msg": "..."}
{"action": "shadowReject"}
Egress pluginOut / egress-policy Fires before an event is emitted to a peer via negentropy range sync, live subscription, or sync/1.0/deliver: {"action": "egress", "event": {...}, "target": {"did": "did:nostr:...", "tier": "member", "class": "edge"}} {"action": "allow"}
{"action": "drop", "msg": "..."}
{"action": "shadowDrop"}
{"action": "redact", "fields": ["tags"]}
Why egress policy matters

Egress policy enforces local shard sovereignty. Operators prevent internal notes or quarantined events from leaking to wide-mesh peers, enforce tier-based media rules, and execute compliance filters before data leaves the local perimeter.

Prior art

Integration with Existing Relay Tooling

Relay synchronization has established production tools, notably strfry router. Any new specification must clearly state its relationship to existing tools. The comparison begins with a straightforward distinction: router is deployed and operational at scale; fleetmesh is a formal specification.

The core distinction is structural. strfry router is a replication tool; fleetmesh is a peering protocol. Router coordinates event transfer between pre-trusted relays. Fleetmesh establishes bilateral relationships, dynamic capacity allocation, and mutual accountability between untrusted peers. The two designs address different operational requirements.

How router works

One process, acting as a nostr client to many relays, driven by a config file of named streams. Each stream has a direction — up, down or both — a list of relay URLs, an optional nostr filter, and optional plugins consulted before storing or before transmitting. Edit the file and it reloads, computing the minimally invasive change rather than restarting. A relay that cannot be reached is retried forever.

strfry routerfleetmesh
peer identitya URL you typeda key, resolvable as did:nostr
relationshipone-sided config; the far end never agreednegotiated, expressed as two grants
directionup / down / bothpush / pull / both — near-identical, arrived at separately
selectiona nostr filter per streaman interest filter, scoped by the grant
reconciliationlive REQ with an implicit limit:0NIP-77 negentropy, required to peer at all
abuse controlplugins the operator writespublished budgets, signed receipts, a fixed ladder
discoverynone, by designgossipsub, relays and descriptors
forwarded trafficinvisible to the far endsigned hop path, charged to every node in it
operator surfaceone legible config filegrants, tiers, receipts, retention modes

Where router is simply better

Four of these are gaps in this document rather than differences of philosophy. They are listed because a comparison that only flattered the author would be worth nothing.

use router, not this
Between relays a single operator controls, fleetmesh is overkill and router is the correct tool. The reputation machinery earns its keep only when peers are strangers; between your own machines it is pure cost with no counterparty risk to price. An operator running a two-region cluster should reach for router and ignore this document.
legibility (incorporated via meshctl)
Simple parameters (dir, urls, filter) allow operators to configure streams in seconds. Fleetmesh incorporates this directly via the declarative meshctl manifest suite (apiVersion: fleetmesh.org/v1alpha1). Operators declare node identity, peerings, and filter scopes in clean, human-readable YAML or JSON.
symmetrical egress policy (incorporated via pluginOut)
Router consults a plugin on the way out as well as on the way in (pluginUp and pluginDown). fleetmesh incorporates this directly via symmetrical pluginIn (write-policy) and pluginOut (egress-policy) seams, giving operators granular, dynamic control over outbound data emission and local shard sovereignty.
hot reconfiguration (incorporated via meshctl apply)
Router computes configuration deltas without connection drops. Fleetmesh adopts this directly: meshctl apply calculates live state diffs and dynamically updates NIP-77 subscription ranges. Active TLS and QUIC sessions remain connected during filter updates.

Where fleetmesh answers something router leaves open

Router's documentation notes that bidirectional streams echo events back to source relays, which then reject them as duplicates. Fleetmesh's dup_ratio limit and envelope origin tracking address this condition directly. Accounting for duplicate ratios provides empirical feedback on network topology efficiency.

The second distinction is explicit bilateral consent. A traditional router stream configured with dir = "up" transmits events to a relay that never signed an agreement to accept them. In practice, operators configure streams only between allied relays. However, the legacy protocol provides no mechanism to record this bilateral relationship. Fleetmesh makes bilateral consent a first-class protocol primitive: a grant is a signed, auditable, and reciprocally enforced contract.

They compose today, with no work at all

A fleetmesh node is a NIP-01 relay, so strfry router can stream to and from one right now with zero fleetmesh support on either side. A fleetmesh sync component, conversely, looks to strfry like an ordinary client. There is no migration and no bridge to build. An operator can run router against fleetmesh nodes, adopt grants for the peerings where counterparty risk is real, and leave router doing the rest.

A debt worth stating

Negentropy came out of this project. It is the set-reconciliation protocol NIP-77 standardises, and § Replication makes it a hard prerequisite for peering. fleetmesh's reconciliation layer is not an alternative to that work; it is that work, depended upon.

Quantum Relay (quantum-rely), and continuous-time propagation

Built on the Go rely v2 framework and packaged for self-hosting via quantumrelay_ynh, Quantum Relay uses continuous-time quantum walks over graph Laplacians. It calculates note propagation probabilities to prioritize fetching from neighboring relays, combined with diffusive consensus and exponential reputation damping.

Like router, Quantum Relay is a concrete, working implementation with rigorous benchmark suites (Jacobi eigendecomposition for N ≤ 128 nodes, sparse Taylor approximations for larger graphs) and automated multi-node stress tests. The comparison clarifies how a physics-inspired routing engine contrasts with an accountable, contract-driven peering standard.

Quantum Relay (quantum-rely)fleetmesh
propagation triggerquantum walk probability amplitude: |⟨i|exp(−iLt)|s⟩|² > θNIP-77 negentropy range reconciliation over grant filters
propagation pathwave interference across Laplacian; multi-hop phase resonancehierarchical fingerprint bisection over scoped interest sets
consensus modeldiffusive gossip delta averaging toward global round convergencestrictly subjective local views; no global state or shared rounds
reputation & spamcontinuous exponential damping: exp(−2γ|rep|t) + Kind 19845-tier ordinal ladder (S0–S4) + evidence-backed Kind 30802 reports
rate enforcementinternal per-client & per-peer token bucketsbilateral published budgets (Kind 30801) + signed receipt hash chains
node classeshomogeneous server-to-server (public ports 443 + 8443)3-tier heterogeneous mesh: Anchor (VPS), Edge (NAT), Leaf (Mobile)
wire & transportNostr WebSockets (wss://) on TLS peer mesh portiroh (QUIC/hole-punching), libp2p gossipsub, HTTPS, Kind 21059/1059
peer identityrelay URL + TLS certificate + NIP-42 pubkeysW3C DID (did:nostr) with multikey binding
extended servicesevent storage (SQLite/memory) + WebSocket relaydecentralized WASM compute market (Kind 30810/30811) with Lightning

Where Quantum Relay is simply better

implemented and benchmarked
cmd/quantum-relay is a production-grade Go binary with end-to-end integration tests, high-concurrency websocket pipelines, and an automated YunoHost packaging suite. fleetmesh is currently a specification draft; Quantum Relay is running code you can deploy to a server today.
propagation in non-uniform topologies
Quantum walk amplitudes superimpose across every graph path at once. Notes therefore cross non-uniform, sparse or hierarchical relay graphs by constructive interference, with no explicit routing table. Direct 1-hop hubs can be bypassed organically if a 2-hop sibling path achieves phase resonance earlier.
zero wire overhead on standard clients
Quantum Relay operates entirely within the native Nostr vocabulary (REQ, EVENT, EOSE, NIP-11, NIP-42) and lightweight JSON envelopes. Clients and peer relays do not need to speak DIDComm v2, verify multikey knots, or unpack custom envelope encodings to exchange notes.

Where fleetmesh answers something Quantum Relay leaves open

the mobile & NAT boundary (a phone is a node)
Quantum Relay requires two open, public-facing TCP ports with TLS termination (443 for clients, 8443 for the peer mesh), restricting participation to servers with public IPs. fleetmesh's transport architecture (Iroh QUIC with native hole punching and libp2p) treats NAT-traversed home servers (Edge) and mobile phones (Leaf) as first-class mesh nodes with their own cryptographic identity and local storage.
verifiable evidence over arithmetic averaging
fleetmesh rejects arithmetic reputation averaging. Adverse reports (Kind 30802) require subject-signed cryptographic evidence (such as receipt heads or out-of-scope events). Third-party hearsay can only lower standing within an operator's local subjective trust horizon (θ = 1.0).
auditable bilateral commitments
In Quantum Relay, rate limits are private token buckets enforced unilaterally inside each relay process. fleetmesh's published grants (Kind 30801) and receipt hash chains turn rate enforcement into a mutual, verifiable contract where silence or misreporting (S2) is penalized more severely than an honest traffic overrun (S1).
Composition across layers

The two designs operate at complementary levels. An Anchor-class fleetmesh node can use graph Laplacians and quantum-walk propagators as internal scheduling heuristics. This prioritizes active peer grants for NIP-77 reconciliation and minimizes redundant synchronization bandwidth across dense sub-meshes.

Ecosystem Bridges

Ecosystem Standards & Interoperability Bridges

A protocol that builds custom silos where proven open standards exist wastes engineering effort and fractures the network. fleetmesh follows a strict rule: offload whatever a merged, standardised Nostr NIP already does. Then bridge in both directions, so ordinary clients and relays gain mesh capabilities without running fleetmesh code.

Standardized Primitives Reused Directly

NIP / StandardRole in fleetmeshWhy Custom Design Was Avoided
NIP-77 (Negentropy) Event replication & range reconciliation Negentropy solves fingerprint tree bisection with optimal byte efficiency. Reused directly across Iroh QUIC streams and WebSocket fallbacks.
NIP-B7 / NIP-94 (Blossom) Binary package & bytecode distribution Content-addressed blob storage with SHA-256 digests. Distributes .npk packages and WASM compute modules without custom storage daemons.
NIP-56 (Reporting Kind 1984) User content moderation ingress Standardized user-reported spam/abuse. Feeds local ingress filters and author grant evaluations.
NIP-44 / NIP-59 Ciphertext & gift-wrapped control envelopes Versioned ChaCha20 with HMAC-SHA256 (encrypt-then-MAC) used for Kind 30801 grant envelopes and store-and-forward control messaging.
NIP-67 EOSE completeness hints Projected from Kind 30803 coverage claims to notify querying clients whether historical shard ranges are complete.

Bidirectional Ecosystem Bridges

Bridge 1: NIP-66 Relay Discovery (Kind 30166)
A node with a public face MAY publish a standard NIP-66 kind 30166 about it, one per ws or wss endpoint its kind 11801 declares, so that relay lists and explorers that know nothing about fleetmesh find it the way they find any relay. The event says what the face is and what it accepts, read from things the node already publishes or serves: d is the endpoint in the canonical form projection.canonical_relay states, n is the network, N is one tag per NIP the face's NIP-11 document lists, R is one per limitation that document declares, t is one per shard the node claims under kind 30803, and the content is the NIP-11 document itself. It carries no rtt tag and nothing about uptime. A measurement somebody else took is worth more than a claim the subject published about itself, which is the rule the heartbeat already follows; liveness and latency are a NIP-66 monitor's to publish. The shape is discovery in constants.json and ref/discovery.py is the serialiser.

kind 30166 — a node's relay face, described for NIP-66 readers

{
  "kind": 30166,
  "pubkey": "<the node>",
  "tags": [
    ["d", "wss://pocketrelay.live"],                 // the endpoint, canonical
    ["n", "clearnet"],
    ["N", "1"], ["N", "11"], ["N", "40"], ["N", "59"], ["N", "77"],
    ["R", "!auth"], ["R", "!payment"], ["R", "!writes"], ["R", "!pow"],
    ["t", "bristol"],                           // a shard it claims
    ["expiration", "1789692000"]                // the descriptor's
  ],
  "content": "<the NIP-11 document the face serves>"
}
Bridge 2: NIP-85 & NIP-32 Reverse Reputation & Moderation Export
Internal peering uses cryptographic S1–S4 conformance reports (kind 30802). Nodes also export their subjective trust into standard client formats.
  • NIP-85 trusted assertions (kind 30385): a client-facing relay score a node MAY derive from its own kind 30802 reports and from nothing else. The event is merged NIP-85 as written. It is an identifier assertion whose d is the subject's relay URL in the canonical form projection.canonical_relay states in constants.json, and whose k is web, the NIP-73 type for a URL. A node publishes one per ws or wss endpoint the subject's descriptor declares. It is adverse only. A node never publishes a score for a peer it has not reported, because a favourable score would put on a relay the standing that blinded grants keep off it. rank is 100 times the multiplier the ladder applied: 50 for S1, 25 for S2, 0 for S3 and S4. p names the subject node and a cites the report by coordinate, so a client that wants the evidence is one fetch from it. It expires with the report. Clients choose whose to read with a kind 10040 provider list, under 30385:rank. PR #2418 proposes a differently shaped relay assertion under the same kind number and is unmerged; if it merges on another number a second serialiser follows it. ref/projection.py is the serialiser and scripts/interop-nostr-veil.py holds its output to an independent NIP-85 reader.
  • NIP-32 (Kind 1985): Emits standardized moderation classification labels (e.g. ["L", "fleetmesh.reputation"], ["l", "s1-divergence", "fleetmesh.reputation"]) consumable by third-party spam filters and client UI badge renderers.

kind 30385 — the relay score a client reads, projected from a kind 30802

{
  "kind": 30385,
  "pubkey": "<reporter node>",
  "tags": [
    ["d", "wss://relay.example.org"],                    // the subject's endpoint, canonical
    ["k", "web"],                                       // NIP-73: the identifier is a URL
    ["p", "<subject node>"],
    ["a", "30802:<reporter node>:<subject node>"],  // the report it is derived from
    ["rank", "25"],                                     // S2: 100 × 0.25
    ["expiration", "1787186400"]                        // the report's
  ],
  "content": ""
}
Bridge 3: NIP-90 Data Vending Machine (DVM) WASI Compute Gateway
Standard Nostr clients interact with compute providers using NIP-90 job requests (Kinds 5000–5999). A node running the ComputeProvider gateway maps each incoming NIP-90 request into a deterministic kind 30810 compute ask. It runs the job in the sandboxed fleetmesh-wasi-v1 runtime under strict fuel limits. Verified results come back as kind 6000–6999 DVM responses, with kind 7000 job status alongside.
Bridge 4: NIP-60 / NIP-61 Cashu Nutzap Asymmetric Settlement
For asymmetric peering topologies (e.g. resource-constrained mobile Leaf nodes or Edge relays consuming high inbound transit without reciprocal outbound bandwidth), peers can attach NIP-60 / NIP-61 Cashu Nutzap tokens to their peering proposals or window reconciliations. The provider accepts blinded ecash proofs to dynamically expand rate budget buckets without requiring symmetric data exchange.
Bridge 5: NostrHub Decentralized Specification Governance (Kind 30817)
To bypass centralized repository merge bottlenecks, the fleetmesh release pipeline formats the full specification, schema references, and test vectors into a canonical Kind 30817 Parameterized Replaceable Event published to NostrHub. The community reviews, verifies cryptographic test vectors, and attaches NIP-32 endorsement labels on-chain.
Zero Protocol Drift

Bridges run as non-blocking sidecars and ingress/egress filter adapters. The internal wire engine remains lean, mathematically rigorous, and completely decoupled from client-facing application layers.

Grants & receipts

Bilateral Budgets & Accounting Receipts

Traditional private rate-limiting fails to educate peers. A peer discovers limits only by hitting connection drops, cannot distinguish throttling from server failure, and has no mechanism to adjust its traffic proactively.

Fleetmesh inverts this model through bilateral accounting. Every node publishes a signed budget allocated to each peer. Peers enforce those limits locally and account for consumption in signed receipts. Issuers verify traffic independently: comparison evaluates whether a peer's self-reported accounting matches direct physical observation. A limit a subject can read is an enforceable contract. Blinded commitments keep these budgets private between peering pairs (§ The grant).

grant issued peer self-limits signed receipt issuer measures kind 30801 token bucket hash-chained independently clean window finding S1–S4 limit + step limit × k, k < 1 next window’s grant — published, signed, auditable
Additive increase, multiplicative decrease. The shape is deliberately TCP's: a peer that behaves gains capacity slowly and predictably; a peer that does not loses it fast. The loop is stable without coordination, converges without a global view, and never needs two nodes to agree on anything except what they each published.

The grant — kind 30801

Addressable, one per (issuer, subject, direction). The event is public; the grant inside it is not. What an observer sees is a commitment and a schedule. What the subject sees, because it alone can decrypt the content, is the budget it has to enforce on itself.

Grants are never broadcast in plaintext. The case for public tables is that peers need to read budgets and third parties need to audit disputes; both needs are met under blinded commitments without broadcast. What broadcast would add is exposure of the complete peering graph, participant standing and downgrade timestamps — and public downgrades turn honest local capacity management into public conflict.

Why this is not a privacy nicety

A design that requires an operator to publicly brand a peer probation is a design that will be quietly under-used. Operators know the people they peer with. Making an honest downgrade socially expensive means fewer honest downgrades, and the reputation system degrades precisely where it is most needed. Keeping rankings private is what makes accurate ranking cheap.

What stays visible, and what does not

Visible to anyoneVisible only to the subject
That an issuer published a grant. The commitment. The window length and epoch, so anyone can tell when a window closes. The expiration. Every limit, the tier, the scope filters, the enforcement schedule — the whole substance of the grant.

The d tag is blinded to protect the network graph from passive traffic analysis. Publishing subject pubkeys in plaintext would turn the addressable index into an open directory of active peerings.

the addressable identifier, computable only by the pair

shared  := ECDH(issuer_sk, subject_pk)        // the same value either side derives
d       := hex(HKDF-SHA256(ikm  = shared,
                           salt = "fleetmesh/grant/1",
                           info = "fleetmesh/grant-tag/1", len = 32))

commit  := SHA256(blinding || canonical(grant))  // binding, hiding, opens later

HKDF rather than a bare HMAC, and both stages separated by a domain string
rather than the RFC 5869 zero salt. An ECDH output is uniform over curve-point
x-coordinates, not over 256-bit strings, which is the input extract exists for;
and NIP-44 derives its own conversation key from this same secret in the same
shape, so one event does not use two disciplines over one value.

Kind 30811 blinds a compute receipt identically under salt "fleetmesh/job/1" and
info "fleetmesh/job-tag/1". Only the labels distinguish them, so the labels are
normative and live in constants.json. vectors/blinding.json pins both.

The blinding in the commitment is 32 bytes drawn fresh from a secure random
source for every commitment. It is never derived from the pair's keys or the
epoch and never reused. The grant it hides is mostly guessable from this
document. Hiding rests on the factor alone. constants.json states this under
commitment.blinding_factor. Its crypto block states the primitives everything
above assumes.

Why not a Pedersen commitment

A Pedersen commitment's distinguishing property is that two of them can be added without opening either. Nothing here adds a grant commitment to anything. What the envelope needs is binding, hiding, and opening later by the one party that can already decrypt the payload — which a salted hash gives, using a primitive every implementation already has.

Specifying Pedersen properly would mean pinning a second generator with no known discrete log relative to G, a mapping from the committed bytes to a scalar, and a point encoding. None of those is stated anywhere in this document, and inventing them for a property nothing uses is the kind of ceremony § Trust weighting declines elsewhere. pedersen-secp256k1/1 is reserved as a scheme identifier against the day a mechanism genuinely needs the homomorphism — aggregate proofs over clean-window streaks being the obvious candidate. Until then it is a name with nothing behind it, and saying so is better than implying otherwise.

The epoch is the clock, not a counter. epoch = floor(unix_seconds / window_seconds), and the window it names is [epoch × window, (epoch+1) × window). Both parties compute it from the grant they hold and never exchange it. Changing a grant's window length starts a new numbering at the next boundary under the new length.

A subject finds its own grant by computing the same d and querying for it. No third party can compute it, and no observer can tell which of an issuer's grants belongs to whom. What remains inferable is an issuer’s peer count, which is a far smaller disclosure than its peer list.

kind 30801 — the public envelope

{
  "kind": 30801,
  "pubkey": "<issuer>",
  "tags": [
    ["d", "<blinded pair identifier>"],
    ["commit", "<SHA256(blinding || canonical grant)>"],
    ["scheme", "sha256-blind/1", "nip44/2"],
    ["window", "3600"],               // public: anyone may know when a window closes
    ["epoch", "496389"],       // floor(unix / window)
    ["expiration", "1787600000"]
  ],
  "content": "<NIP-44 ciphertext of the grant below, to the subject>"
}

the grant itself — encrypted, read by the subject alone

{
  "tags": [
    ["subject", "<subject pubkey>", "node"],   // node | author
    ["tier", "member"],               // probation | member | trusted | anchor | revoked

    ["limit", "conn_rate",    "12"],
    ["limit", "req",          "600"],
    ["limit", "events_in",    "3000"],
    ["limit", "bytes_in",     "33554432"],
    ["limit", "bytes_out",    "268435456"],
    ["limit", "blob_bytes",   "0"],
    ["limit", "sync_ranges",  "400"],
    ["limit", "errors",       "40"],
    ["limit", "dup_ratio",    "0.15"],

    ["scope", "push", "{\"kinds\":[0,1,3,7,10002],\"#t\":[\"bristol\"]}"],
    ["scope", "pull", "{\"kinds\":[0,1,3,7,10002],\"#t\":[\"bristol\"]}"],

    ["schedule", "1.1", "S1"],        // >110% of any limit, receipts honest
    ["schedule", "2.0", "S2"],
    ["schedule", "5.0", "S3"],
    ["receipt", "600", "1048576"],    // send one every 600s or 1 MiB, whichever first
    ["settlement", "none"],           // reserved
    ["blinding", "<32 fresh random bytes; opens the commitment in the envelope>"]
  ]
}

dup_ratio deserves a note: it is the fraction of delivered events the receiver already held. A peer with working negentropy reconciliation sits near zero. A peer at 0.9 is either broken or replaying, and the distinction between those two is exactly what the receipt comparison resolves. The ratio is priced in the window like any limit, as dups_sent / events_in against the ceiling through the same class function, with no divergence term because only the receiver knows what it already held. It has no line in the grant, so the additive-increase step never scales it. Seven duplicates in eight is an S3.

The receipt

The subject maintains a token bucket mirroring each limit and, at the cadence the grant specifies, sends a signed receipt over budget/1.0. Receipts within a window form a hash chain, so a subject cannot rewrite an earlier claim after learning what the issuer saw; the issuer keeps only the head.

The canonical form is the one every digest in this document is taken over, stated once in constants.json under canonical: the receipt body as JSON, UTF-8, object keys sorted ascending by code point, no whitespace between tokens. prev is the SHA-256 of the previous receipt body in that form, thirty-two zero bytes for seq 0, and the chain head is the SHA-256 of the last. Counters are integers, so nothing about number formatting is left to the implementation. The same form is what canonical(grant) means in the commitment above.

budget/1.0/receipt — body

{
  "grant": "<event id of the kind 30801 in force>",
  "window": 496389,
  "seq": 7,
  "prev": "<sha256 of the canonical form of receipt seq 6>",
  "counters": {
    "conn_open": 2, "req": 141, "events_in": 802, "bytes_in": 2216041,
    "bytes_out": 19883, "blob_bytes": 0, "errors": 1, "dups_sent": 44,
    "sync_ranges": 61
  },
  "forwarded_for": ["did:nostr:<origin>"],
  "at": 1787003600,
  "sig": "<BIP-340 by the subject node key over this receipt's chain link>"
}

Counters are cumulative within the window and strictly monotonic. A window prices each limit against the counter of the same name. Where the names differ, counter_of in constants.json says which counter: conn_rate is priced against conn_open, the sessions the subject opened in the window, on whatever transport carried them. Two ceilings left the table because nothing could count them. conn_max is a gauge and a cumulative receipt cannot carry one. sub_max counts subscriptions on a relay face, which peers never open. Both are advisory now, local ceilings a node may enforce and no grant sells. The forwarded_for field lists upstream origins whose traffic the subject forwarded during the window. This mirrors the delivery envelope's path tag. It makes forwarding obligations verifiable without requiring manual envelope reconstruction.

The window is evaluated against the most recent receipt the issuer received for it. Counters are cumulative, so that receipt already states the subject's whole claim, and the chain makes it as binding as any closing statement could be. Traffic the issuer measured after it is simply unaccounted-for traffic: it is compared against that receipt's counters and priced by divergence, exactly as a false receipt would be. Nothing moving after the last receipt is a clean window. This is what lets a leaf sync for ten minutes, send two receipts and lock its screen without producing a finding; a whole window of measured traffic with no receipt at all is the active silence § Severity ladder prices as an S2.

budget/1.0/close is therefore optional. It is a final receipt carrying the window's last counters; one arriving within the grace period (one tenth of the window) is accepted as final, and one arriving later is evaluated like any other receipt. The issuer answers every receipt privately with a receipt-ack carrying the head it now holds and one of accepted, divergent or final, so a subject always knows which receipt stood as its claim. Clean windows publish no public events.

Why clean windows are not published

A node MUST NOT publish a conforming report at window close, or any other positive attestation. Publishing subject pubkeys hourly would expose the complete peering graph with standing scores attached. And positive reports are arithmetically inert under the trust weighting model (§ Trust weighting): broadcasting them leaks bilateral topology without adding anything a reader can act on.

Clean-window streaks remain private bilateral state. Nodes disclose streak counts on request over standing/1.0 subject to local policy. Verifying streak thresholds without disclosing exact durations is a designated application for aggregate zero-knowledge proofs (§ Trust weighting).

Consequently, only adverse reports are published. An adverse report must identify its subject, so it names the offending node explicitly. Because adverse findings are exceptional rather than routine, peering graph exposure occurs only when a node violates protocol rules.

Window arithmetic

evaluated by the issuer at window close, per limit ℓ

overrunℓ    = max(0, measuredℓ / limitℓ - 1)
divergenceℓ = |measuredℓ - claimedℓ| / max(measuredℓ, floorℓ)

severity   = max over all ℓ of  class(overrunℓ, divergenceℓ)

clean      : strainℓ = min(1, measuredℓ / limitℓ)
             if measured and strainℓ = 0 and limitℓ > openℓ
               limitℓ ← max(openℓ, limitℓ × 0.90)
             else
               limitℓ ← limitℓ + tier_baseℓ × max(0.02, 0.10 × strainℓ)
S1         : limitℓ ← max(probationℓ, limitℓ × 0.50)
S2         : limitℓ ← max(probationℓ, limitℓ × 0.25) ; tier ← probation
S3         : limitℓ ← 0 for 24h, then resume at probation
S4         : tier ← revoked ; grant deleted ; report published

then, always : limitℓ ← min(tier_maxℓ, limitℓ)  at the tier the branch returns

The last line is not decoration. A grant MAY set any limit lower than its tier ceiling and MUST NOT set one higher, and that has to hold for a remedy as much as for a first issue. It binds hardest at S2, which is the one branch that lowers the tier. A quarter of any higher tier's limit is still far above what probation allows, so the ceiling is what decides and the quartering never gets to. S2 does not leave a peer a quarter of what it had. It makes it a stranger again.

Wood is laid down where the strain was

A grant opens below the ceiling of its tier and the clean branch grows it in proportion to what the closed window actually carried. Both halves are needed and neither works alone.

Without the first there is no growth at all. A grant that opens at the ceiling makes the additive increase a no-op — min(ceiling, v + step) is the ceiling when v already is it — so every capacity is issued whole on the first day and can only afterwards be eroded, and the increase does something only as a recovery ramp after a fault. Volume opens at a quarter. The structural limits — connection rate, requests, sync ranges, errors — open at the ceiling, because a peer needs enough plumbing to demonstrate anything at all. What is earned is volume, never the right to open a session.

Without the second, growth is priced in windows rather than in work. A flat step gave a peering that moved one event an hour exactly what it gave one running at its limit, so the cheapest route to a large grant was to be present and idle. The step is scaled by use now, with a floor: a quiet peering still creeps upward, because a tree does grow without wind and it grows tall and slender and cannot stand. At the tier ceilings, a peering carrying its full limit reaches that ceiling in eight windows; one carrying almost nothing takes weeks, which is the point.

An evaluation that offers no measurement gets the floor step and never the full one. Absent evidence of load is not evidence of load. The one thing allowed to skip the earning is a person: § Trust weighting already says an operator MAY seed a tier for a node it knows out of band, recorded as a signed event and reversible, and that seeding opens at the ceiling.

And taken back where it went unused

The pair to growth, and the reason growth may be allowed to reach a ceiling at all. A limit moved up on a clean window and down on a fault and never decayed, so capacity earned in one busy week sat there a year later as surface nobody was watching.

Two shapes and one floor. A limit that a measured window carried nothing for, sitting above where a peering opens, gives back a tenth of itself instead of taking the floor step — so a busy peer that never moves a blob returns its blob budget while its event budget stays where it earned it. And a peering whose windows close with nothing measured at all, for decay_idle_windows in a row, has its whole grant decayed once, and again after each further run of them.

The floor is the opening limits of the tier the grant names. Disuse returns a peer to where a stranger starts and never below it: what it has not used it gives back, what every peering is given it keeps. The structural limits open at the ceiling, so they never decay at all — an error budget that wasted away would trip on the first bad hour.

A limit that measured zero is owed nothing, so at or below the opening value it is left exactly where it is rather than taking the floor step. Stepping up there and decaying back is a flutter, and every crossing costs the subject an amend. No measurement is not a measurement of zero, though: an evaluation offering no counters gets the floor step and no decay, in either direction, for the same reason an absent claim is the empty claim rather than an accusation. A decay is amended with cause: disuse: it is not a finding, not a shed and not a person. Nothing happened, which is the whole of the reason.

The pieces the arithmetic needs and the prose kept implying

Writing conformance test vectors required formalizing three core evaluation components: the class() classifier, per-tier limits (tier_max and probation), and divergence tolerances for S2 findings. These constants now live in constants.json and are bound within the version digest.

class(overrun, divergence) → severity, evaluated per limit

by_overrun = S3 if overrun ≥ 4.0 else
             S2 if overrun ≥ 1.0 else
             S1 if overrun ≥ 0.1 else  S0

if divergence > 0.10  → worse(by_overrun, S2)   // a floor, never a ceiling
otherwise             → by_overrun

A misreport is a floor of S2 and not a ceiling. Returning S2 the moment
divergence crossed its threshold priced a peer that overran four times over
and lied about it below the same peer reporting it honestly, because honesty
reaches S3 on overrun alone. Lying must never be the cheaper option.

dup_ratio is evaluated in the same window and is not a counter. It has no line
in a grant, so nothing scales it, and its ceiling is 0.15 at every tier. Only the
receiver knows what it already held, so there is no claim to compare and no
divergence term: class(dups_sent / events_in / 0.15 - 1, 0). Under the divergence
floor a ratio is noise and is not evaluated.

A window's severity is the most severe across all its limits. S4 is never
reached by arithmetic: it is forgery or impersonation, found structurally.

Watch the units. A schedule tag is a ratio of the limit — ["schedule","1.1","S1"]
means 110% — while overrun is measured/limit - 1. A schedule ratio r is an
overrun threshold of r - 1. Reading one as the other is the likeliest
implementation bug in this section.
ceilingprobationmembertrustedanchor
conn_rate41248192
req6060024009600
events_in30030001200048000
bytes_in335544333554432134217728536870912
bytes_out2684354526843545610737418244294967296
blob_bytes16777211677721667108864268435456
sync_ranges4040016006400
errors440160640

A grant MAY set any limit below its tier ceiling and MUST NOT exceed it. Additive increase raises limits toward tier ceilings in increments of ten percent of the ceiling. Approximately ten consecutive clean windows restore a throttled limit to its maximum. Penalties floor at the probation tier rather than zero, ensuring S1 overruns remain recoverable. The dup_ratio threshold is identical across all tiers: duplicate efficiency is an invariant data quality metric rather than a tier-scaled capacity.

The floorℓ parameter (fixed at 5% of the limit) prevents small absolute sample counts from triggering false divergence findings. Clock skew is managed by applying a grace period equal to one-tenth of the window length and requiring NTP-disciplined clocks. Clock drift exceeding 60 seconds produces unearned divergence. An issuer MUST deliver a complaint/1.0/notice and wait one window before publishing an S1 or S2 report against a reachable peer; S3 and S4 MAY be published immediately. See § Control plane.

A node says what it used before it goes

A node that stops mid-window leaves its issuer with traffic measured and no receipt, which is active silence and an S2 floor. S2 lowers the tier to probation, and a tier is only ever raised by a person. So the rollout this specification requires on a version change — every node together, or they decline to peer — would cost every peering its operator-seeded tier, every time it happened.

A node SHOULD therefore send a receipt for each live peering’s open window before it stops. That receipt is a claim, and the window is then priced on the numbers rather than on the absence. It needs nothing new: it is the receipt the cadence would have sent, sent early. A peering that has used nothing sends nothing, because silence there is honest, and a node that cannot say goodbye still stops.

Retention is the operator’s choice

Reports are ordinary addressable events and replicate like anything else. Grant envelopes do too, but a node that collects other people's envelopes accumulates commitments it cannot open — proof that grants exist, and nothing about them. What is worth keeping is therefore mostly reports. How far back to keep them depends on what the operator intends to do, so it is a setting rather than a rule.

Mode Keeps Enables
self Grants this node issued, and grants naming it as subject. Nothing else. Enforcing your own budgets and honouring the ones issued to you. The floor.
peers default The above, plus reports authored by nodes this node holds a grant with. Not their grants: those are encrypted to their own subjects, so keeping them stores ciphertext that will never be read. The full trust weighting, and nothing beyond it.
neighbourhood Peers, plus what this node’s peers answer when asked about nodes it has not peered with. Grants are not readable from a relay, so this is a query budget rather than a subscription: it stores answers it received. Forming a view of a candidate before peering with it — at the cost of having to ask, and of the answer being refusable.
archive Every grant envelope and every report it sees. Envelopes are commitments, so an archivist accumulates proof that grants existed and none of their contents. Monitoring, research, and auditing the reputation system itself from outside.
none Nothing persisted; grants are evaluated live from the peer’s own copy and discarded. Leaves, and anything else with a storage budget measured in megabytes.
Why peers is the default

It is exactly the set the scoring function can use. A reporter's weight is tier_weight(my grant to r), and a node this one has never granted anything scores zero — so its reports are, arithmetically, incapable of changing any view here. Storing them is provably wasted disk. The default is not a compromise between completeness and cost; it is the whole of what the node can act on, and no more.

Two properties keep even archive from running away. Grants and reports are addressable, so only the latest per (issuer, subject) is retained — the set grows with the number of peering pairs, not with time. And every grant carries a mandatory NIP-40 expiration, so a dead peering evaporates rather than accumulating. In practical terms: a node with twenty peers in peers mode holds a few dozen events, well under a megabyte. archive over a ten-thousand-node network is a few hundred megabytes, and stable. Neither is a reason to make the choice for the operator.

Retention is enforced on the subscription rather than pruned retroactively. Nodes in peers mode narrow interest filters directly to reports authored by their active peer set. Broad firehose subscriptions create significant bandwidth costs for both senders and receivers. Because interest filters are public declarations, a node operating in archive mode is visibly designated as an archivist.

The node has a ceiling too

A grant bounds one peer. Nothing bounded the node. The meter is per peering, so twenty anchor grants each perfectly inside its own limit is 960,000 events an hour with nobody at fault anywhere: every branch within its limit, and the trunk with none. A node in that state does not fail as a protocol violation. It simply stops.

So a node declares its own total and sheds against it. The total is the operator’s number and this specification states no default for it. Connectivity and not capability is what separates one deployment from another, which is the whole of what a class declares, so a Raspberry Pi and a forty-core gateway both say anchor and no table here could be honest about either. A node with no declared total measures its pressure, reports it, and sheds nothing — refusing an honest peer against a limit nobody chose would be worse than the overload it avoided.

What is normative is the ladder. Pressure is the window’s aggregate over every peer, divided by the declared total, taken as the max over counters, because a node that has run out of one thing has run out.

the shed ladder, by pressure against the declared total

0.75  refuse_peerings   a new proposal is declined CAPACITY_EXCEEDED
                          peerings already held are untouched
0.85  narrow_scope      shards held only through the mesh stop being claimed
0.95  amend_down        every live grant is reduced, cause: capacity
1.00  leaf_behaviour    no pull is opened; receipts still flow

      release: every rung together, below 0.60

Three rules hold the ladder together. A shed is announced — a decline code, a coverage claim that stops being republished, an amend that says what caused it. A node that shed quietly would read as partitioned, which is the one conclusion this protocol keeps having to unlearn. A shed is not adverse. peering/1.0/amend carries cause, one of remedy, capacity or operator, and only the first is a finding; a node that read a capacity shed as a remedy would price its issuer for owning a small machine. A shed makes a peer smaller and never a stranger. The tier does not move and the peering is not ended, and the floor is a fraction of the probation ceiling rather than the ceiling itself — a peering opens at that ceiling, so a floor there would make the rung do nothing for exactly the peers a busy node has most of. It is not zero either. Zero for 24 hours is what S3 does to an offender.

The rungs release together rather than each at the threshold that engaged it, because a node sitting on a boundary would otherwise amend every one of its peers twice a window. Everything here is the node’s own arithmetic over its own meters: no new message, no new kind, and nothing another node has to be trusted for.

What a node does with stress it survived

A restart. A peer that went quiet mid-session. A relay that dropped its subscription. A clock that drifted past tolerance. This protocol is careful never to conclude something about a peer from an absence, and every one of these is correctly refused as a finding — and then discarded entirely, so the information goes out with the accusation. A node learns nothing from anything it survives.

Kept, it sets the node’s own reserves: how long it keeps accounting a vanished peer might still need, how long it goes on trying a door that was shut. Two rules make that safe to do.

Local and defensive only. A strain record never becomes a finding and never crosses the wire, and a peer’s own account of what it has weathered is a claim rather than evidence and is not read. If the whole of a node’s response to being stressed is to become better prepared for that exact stress, an attacker who causes it has done the node a favour and there is nothing to farm. Anything that instead paid a node in reputation or authority for having been attacked would be an invitation to manufacture the attack — which is the reason the severity ladder is one-way, and the reason nothing here touches it.

Directions, never magnitude. A reserve grows with the number of independent directions the stress came from and never with how much came from any one of them. A tree grows straight because the wind turns; where the wind is strong and always from one quarter the result is a flag tree, bent and one-sided and weaker overall. Each direction — one peer, one shard, one transport — is capped, and weighted by the tier this node granted that peer. Probation weighs zero, so free identities buy no directions at all, and a tier is only ever raised by a person: the sybil is priced by a constant that already existed rather than by a new one. Five hundred strangers move a node by nothing, and one peer moves it by at most one direction’s worth however hard it pushes.

The same arithmetic gives the operator a warning nothing else produces. When nearly all the strain a node has seen comes from one direction, that node is structurally dependent on one peer and will snap when it leaves. It is reported and it is not a finding: the peer has done nothing but be the only one there. Two qualifications, both learned the hard way. A direction naming neither a peer nor a transport is the node’s own event and cannot be depended on — dependence is something a node has on somebody else. And one event is not a pattern, so the warning needs a sample behind it.

Allow-lists and block-lists are grants

This model collapses three mechanisms into a single signed, expiring object. Queries regarding blocked traffic receive signed, verifiable explanations delivered directly to the requesting peer rather than broadcast publicly.

The same primitive covers user pubkeys via subject: author. A relay's existing allow and deny lists function as the local materialization of author grants. Relays supporting strfry's write-policy plugin protocol SHOULD maintain it unmodified alongside mesh policies. This preserves custom operator moderation logic alongside protocol-level peering enforcement.

Settlement is deferred, and constrained in advance

Transit payment mechanisms are deferred during alpha. The settlement tag on grants remains reserved with value none. Discrete compute job payment is specified separately in § Compute market. Transit settlement is deferred because capacity scarcity has not yet been demonstrated. Designing complex financial mechanisms for unproven scarcity creates unnecessary technical debt.

The protocol defines the invariants any future settlement mechanism must satisfy. Paid transit interacts poorly with bilateral reputation. A peer paying for capacity expects an unconditional service-level agreement. In contrast, the severity ladder requires that issuers retain the sovereign right to shrink capacity unilaterally upon divergence. Three rules prevent these models from colliding:

  • Payment may raise a ceiling, never establish a floor. Settlement can increase tier_max; it can never impose a mandatory minimum bandwidth requirement on the issuer.
  • The ladder is never suspended. Additive increase still governs capacity expansion for paying peers across clean windows. Payment purchases higher tier headroom rather than instant flood capacity. This prevents pay-to-flood attacks by construction. Severities S1 through S4 apply identically to paying and non-paying peers.
  • No obligation an issuer must arbitrate. Because issuers make no unconditional uptime promises, contractual breach cannot occur and no dispute arbitration is required. Settlement protocols must conform to this invariant. The compute market satisfies this by making job settlement accountable and provable — not atomic, which § Compute market is explicit about — so a failed delivery leaves signed evidence rather than an ambiguous partial transaction.

One measurement decides whether any of it is ever needed. Nodes SHOULD record, per window, whether each grant actually bound — and if so, which limit bound first. If a year of alpha shows grants almost never binding, settlement is answered by the data and the field can be deleted rather than filled in.

Storage rent is refused, and retrieval is the exception

The same question arrives for held bytes rather than moved ones: whether a peer should charge another node for storing its shard. It is refused, and on stronger grounds than transit.

Transit and compute are both acts. Storage is a position, and the three rules above bite hardest on the first of them. A paying peer can only be sold a higher blob_bytes ceiling — the right to send more bytes — because a floor is precisely what rule one forbids. Yet the floor is the entire content of a storage purchase: the promise that the bytes are still there later. Nothing coherent remains to sell. Rule three then fails by construction. An obligation that never terminates offers no moment at which settlement can be reckoned at all. The dispute it produces is about a duration, not an act.

Holding a shard is also not a favour. A peer asserting completeness: asserted over a shard serves queries from it, earns standing with it, and advertises it in kind 30803 to attract further peers. Coverage is a joint good, so the trade already nets to zero at the moment it is declared.

Retrieval MAY be priced; retention MUST NOT be. A retrieval is discrete and has a moment of completion, so it settles under the construction already specified in § Compute market. The payload is sealed to a key k. The invoice's payment hash is SHA256(k), so paying it reveals the key. Delivery and payment are a single act, no escrow agent exists, and the buyer verifies the delivered bytes against the CID it named. No proof of retrievability is required, because the proof is the ability to produce the bytes on demand — a node that discards them simply never earns from them again.

Two existing rules carry the rest. A node that will not keep holding what it claimed MUST narrow its coverage claim. It MUST NOT make that claim conditional on payment (§ Node classes). Withholding data inside a shard still claimed complete is falsifiable by sampling, and remains an S2 (§ Attack surface). Neither needs a new severity class and no new tag is reserved: settlement stays none, and blob_bytes stays a flow limit defaulting to zero. Whether storage scarcity is real at all is a measurement rather than a guess, and § Alpha contract records the counters that answer it.

Severity ladder

Deterministic Severity Classification

The remedy for a given breach is deterministic and stated in the grant before the breach happens. This is not a courtesy: an operator who can choose the punishment after seeing the offender is doing moderation, and moderation does not federate. A ladder fixed in advance can be checked. The two parties check it against the grant they both hold. Anyone else checks it against the published evidence, and against the commitment that grant was published under.

  • S0 drift Within 110% of every limit, receipts agree with measurement. The normal state of a busy peer. no action · counts toward the clean-window streak
  • S1 overrun Sustained traffic exceeding a limit where receipts accurately report the excess. The peer is honest but unshaped (e.g. backfill burst or temporary traffic spike). limit × 0.5 · notice first · no publication on the first occurrence
  • S2 misreport Receipts diverge beyond tolerance, fail to arrive during active egress traffic, or declared metadata (version, class, policy, or coverage) contradicts direct observation. Also triggered by false operator independence claims (e.g. running shadow nodes to subvert dual-mediation). Structural deception or withholding required receipts constitutes an actionable misreport. (For dormant, zero-egress leaf nodes, see § Partition tolerance). tier → probation · limits to the probation ceiling · report published after notice
  • S3 abuse Traffic outside granted scopes, malformed event floods, excessive duplicate ratios, or repeated hop-limit violations. limits → 0 for 24h · report published immediately with evidence
  • S4 malice Forged signatures, corrupted receipt chains, transport key impersonation, deliberate shard poisoning, or grant evasion routing. revoked · grant deleted · report published · peers re-evaluate
The ordering is the argument

S1 overruns rank below S2 misreports by design. A peer that exceeds its capacity limit but reports accurately receives lighter penalties than a peer that underreports traffic. Capacity overruns are operational issues. Falsified reporting destroys the accounting foundation upon which all bilateral peering depends. The ordering is a floor and not a reordering: a peer that overruns grossly and lies about it keeps the S3 the overrun earned. Were the misreport to replace the class rather than raise it, lying would be the cheaper of the two, in the one mechanism whose whole job is to make it expensive.

The report — kind 30802

Addressable, d = subject pubkey, one per (reporter, subject), replaced as the relationship evolves. It carries the reporter's current verdict, not a log; the history is reconstructable from the reporter's own published sequence if anyone cares to keep it.

kind 30802 — conformance report

{
  "kind": 30802,
  "pubkey": "<reporter>",
  "tags": [
    ["d", "<subject pubkey>"],
    ["outcome", "S2"],                // S1 | S2 | S3 | S4 — adverse only
    ["grant", "<id of the grant THIS reporter issued to the subject>"],
    ["windows", "496380", "496389"],  // range this verdict covers
    ["observed", "events_in", "3312", "3000"],   // measured, limit
    ["claimed",  "events_in", "2900"],
    ["evidence", "receipt", "<sha256 of the subject-signed receipt, carried in content>"],
    ["evidence", "envelope", "<sha256 of a delivery envelope the subject signed a hop on>"],
    ["evidence", "event", "<id of an out-of-scope event the subject itself signed>"],
    ["notice", "<thid of the complaint/1.0 exchange>", "1787003600"],
    ["expiration", "1789600000"]
  ],
  // Evidence travels in the content, because no relay serves a receipt or an
  // envelope: a reference to one resolves to nothing anywhere. A reader hashes
  // the artefact, compares it to the tag above, and checks the subject's
  // signature on it. An "event" entry may stay a bare reference: it is public.
  "content": {
    "note": "Divergence on events_in across ten consecutive windows.",
    "evidence": { "<that sha256>": { … the receipt, whole, with the subject's sig … } }
  }
}
Evidence or nothing

A report MUST name, in its grant tag, a grant issued directly by the reporter to the subject. Without an established bilateral grant, no valid finding can exist. Nodes MUST discard any report whose grant tag does not resolve to a valid event published by the reporting node.

Every report MUST include at least one evidence tag referencing a cryptographic artifact signed directly by the subject. Acceptable evidence is a receipt, a delivery envelope the subject signed a hop on, or an event the subject itself signed. The first two are private DIDComm bodies that no relay serves, so they travel inline: the report's content is {"note": …, "evidence": {<sha256 of the canonical artefact>: <artefact>}}, and a reader hashes it, compares it to the tag and checks the subject's signature, resolving nothing. Evidence pins the subject's half of a disagreement and no more; the measurement is the reporter's own word, weighted by its standing. Reports lacking subject-signed evidence carry zero weight across the network. Repeated publication of unevidenced claims is an actionable misreport.

Reports regarding user content remain on standard NIP-56 Kind 1984 events (accepted by Blossom servers under BUD-09). Content reports inform author grants. In contrast, Kind 30802 reports govern node protocol conduct exclusively. User moderation complaints and operator protocol violations belong to separate domains and MUST NOT share a scoring function.

Partition tolerance — active vs. inactive silence

Under the Fischer-Lynch-Paterson (FLP 1985) theorem, asynchronous networks cannot reliably distinguish a dead or partitioned node from an intentionally silent one. A naive application of "silence is an S2 misreport" would penalise mobile leaf nodes entering deep OS battery sleep or traversing subway tunnels. The protocol avoids this by differentiating physical egress state during the evaluation window:

Observed State Physical Traffic Receipt Status Classification Protocol Remedy
Active Silence > 0 events or bytes measured Missing or unlinked S2 Misreport, at least The measured traffic is still evaluated against the limits, with S2 as a floor and not a ceiling: a peer that overran grossly and said nothing keeps the class the overrun earned. At S2 the tier falls to probation and the limits with it. No report is published — the subject signed nothing that window, so there is no artefact of its own to cite and no reader could check either half of the claim. The remedy is local and complete, and standing/1.0 still carries the streak to anyone who asks.
Inactive Silence 0 events and 0 bytes measured Missing / dormant Graceful Partition Not evaluated · streak neither advanced nor broken · no report filed

A returning leaf does nothing special. A dormant window was not evaluated, so there is no penalty to lift and no sequence to reconcile: the next active window starts a fresh receipt chain at seq 0, exactly as every window does. The grant's NIP-40 expiration already bounds how long a leaf may stay away — one that returns after it simply re-peers — so no lease is needed. Nor does a mediator publish anything about a leaf's absence: the kind 21801 heartbeat names no peer (§ Discovery), and § Privacy rules would rather a phone's waking hours were not announced at all.

Trust weighting

Sovereign Trust Weighting & Local Enforcement

Reports are inputs, not verdicts. A node computes its own view of a subject from its own observations plus other nodes' reports, and weights each report by how much that reporter's own grant from this node is worth. You get to influence me exactly as much as I have already decided to trust you.

local view, computed independently by every node

w(r)   = tier_weight(my grant to r) × age_factor(r) × (1 - false_report_rate(r))

         tier_weight:  probation 0.00   member 0.25   trusted 0.60   anchor 1.00
         age_factor:   min(1, clean_windows(r) / 168)  // 168 default hourly windows (1 week streak)
         false_report_rate: (unsupported + refuted reports by r) / (reports by r I evaluated)
                            unsupported: evidence does not resolve, or the subject did not sign it
                            refuted:     evidence verifies, but the classifier run over it does
                                         not produce the outcome the report claims
                            defined as 0 when evaluated is 0. Both are decided by the reader
                            alone, from data it already holds.

view(x)  = the most severe S in {S1..S4} such that

               Σ w(r)  ≥  θ      over reporters r whose finding on x is at least S

           or own_severity(x), whichever of the two is more severe.

θ = 1.0      one anchor-tier reporter, or two trusted, or four members.
constraint  view(x) may only lower a starting grant, never raise one.

Dual-Track Standing: Transit vs. Compute Separation

To prevent cross-contamination across heterogeneous hardware (e.g. high-bandwidth relays running on low-power ARM devices versus high-CPU compute workers with minimal relay bandwidth), standing is strictly partitioned into two independent Subjective Logic domains:

Dimension Evaluation Domain Evidence & Metrics Failure Remedy
viewtransit(x) ["topic", "transit"] Negentropy range sync fidelity, bandwidth limit compliance, duplicate event ratios (≤ 0.15), and hop path verification. AIMD bandwidth throttling (×0.5, ×0.25), connection pruning, or relay peering revocation.
viewcompute(x) ["topic", "compute"] WASM bitwise execution determinism on sample re-runs, fuel estimation accuracy, job SLA compliance, and settlement preimage delivery. Order book price derating, dual-execution probation quarantine, or compute ask disqualification.

Domain Isolation: A seller who misreports a WASM compute result hash incurs an S2 finding scoped strictly to topic: compute, immediately downgrading its compute standing without disrupting an honest, non-divergent transit peering. Conversely, transient bandwidth overruns on a high-traffic relay do not penalize its verified compute worker track record.

A threshold rather than a weighted mean, deliberately. Severity is an ordinal scale. Its remedies — ×0.5, ×0.25, zero, revocation — are nothing like evenly spaced. Averaging S1 against S3 to get “S2” would assert a spacing the ladder denies. Asking instead "how much weight stands behind a finding at least this severe" needs no arithmetic on the scale itself, and reads the way an operator would reason anyway.

Three properties fall out of that shape, and each is load-bearing:

sybils weigh nothing
A thousand newly spawned nodes reporting the same subject contribute zero total weight (w(r) = 0). Probation weight is zero and age_factor starts at zero. Generating identities does not produce influence. Reputational weight requires sustained, verified conformance over time, which cannot be parallelized.
trust cannot be inflated
Third-party reports may only lower standing. A collusive ring of nodes vouching for each other produces zero standing increase. Reputational standing arises exclusively from direct local observation of clean windows or explicit operator grants. Hearsay is admissible only as an adverse signal, never as an endorsement.
reporting has a price
false_report_rate means a reporter stakes its own standing. File a finding I later contradict with my own measurement and your weight drops for everything you say afterwards. This is the reverse-reputation principle applied to the reputation system itself — you are responsible for what you publish about others exactly as you are for what you send them.

Asking, now that reading is impossible

Because grants operate as blinded commitments, nodes cannot discover stranger standing via public subscriptions. Nodes query peers directly over standing/1.0. The queried peer evaluates its local policy before responding. Two protocol query modes are defined:

ask → tell
"What do you make of X?" answered with a tier, or with silence. This is selective disclosure, not zero knowledge. The answer is exactly the ranking that was being kept off the relay, handed to one party under a policy instead of to everyone by default. That is the whole of the improvement, and it is worth being precise that no proof system is involved.
ask → decline
The expected answer for a stranger, and it MUST NOT be treated as adverse. A node that reads refusal as a signal has rebuilt the pressure to disclose that this design removes.
Why not a range proof

The obvious cryptographic answer — prove "X is at or above member" without revealing the tier — does not survive the domain. There are five tiers, so three questions binary-search the exact value, and the natural defence of rate-limiting the questions is the grant system, which is the thing being protected. A proof that leaks its witness in three queries is ceremony. Selective disclosure with a policy is the honest mechanism at this size, and pretending otherwise would be worse than publishing.

Where zero knowledge would actually earn its place

Not per-peer predicates over five values, but aggregate claims over a set the prover keeps private — where the witness is membership, or the domain is large enough to hide in. Four that would be genuinely useful, and none of which the current design can express:

  • "At least three of my trusted-tier peers hold X at member or above" — without naming which three.
  • "X's clean-window streak with me is at least 100" — a domain in the hundreds, where a range proof hides something real.
  • "I hold no grant to X above probation" — a way to warn without accusing.
  • "I authored a kind 30801 whose d blinds to X" — without saying which. This is the one that would make an anonymous report admissible, and it is the reason a ring signature cannot do the job; see below.

Bulletproofs fit the shape: transparent setup, proofs under a kilobyte, and they work over secp256k1, which is already the node key's curve. None of the four is implemented, and the last of them is what the next section turns out to need.

Reporting has a cost, and only one half of it is fixable

Nodes silently drop abusive peers rather than reporting them, because a report invites retaliation and the benefit of filing it is shared with everyone. That is the second-order public goods problem, and only one half of it has a defensible remedy.

Objective evidence immunity. A reporter is immune when its evidence verifies and the finding follows from that evidence. Both halves are objectively checkable by the reader: the classifier is deterministic, so anyone holding the grant and the cited evidence recomputes the outcome for themselves and needs nobody's agreement to do it.

Immunity on verification alone would make a false report free. A reporter could cite a genuine subject-signed receipt, claim S3 where the arithmetic gives S1, and pay nothing — which contradicts the price § Attack surface says a reporter pays for exactly one damaging false report. Narrowing immunity to verifies and follows is what makes that claim true.

Honest disagreement survives this. Two nodes that carried different traffic for the same subject reach different outcomes and neither is refuted, because each cited its own evidence and each one's arithmetic follows from what it cited. Refutation is not "you disagree with me"; it is "your own evidence does not say what you said it says."

Anonymous reporting does not work here, and the reason is structural. A ring signature over "every grant issuer of this subject" needs that set to be enumerable. Blinded d tags exist precisely so that it is not — see § Grants & receipts. The two requirements are also contradictory on their face: a report is admissible only because its grant tag resolves to an event the reporter published, and that check is exactly what identifies the reporter. A construction satisfying both would have to prove "I know a key whose ECDH with this subject derives the d of a kind 30801 I authored" without revealing which — a zero-knowledge proof, listed as the fourth application above rather than pretended into existence here.

The nearest built thing is nostr-veil, and it is worth saying why it does not fit rather than leaving a reader to wonder whether it was missed. It wraps each contribution to a NIP-85 score in an LSAG ring signature over a published circle. A verifier learns that some member of the list signed and that no member signed twice. That is the shape of the first application above, with one difference that decides it. Its ring is public by construction: the member list travels in the event, and its own threat model says so. The ring this section needs is the set of a subject's grant issuers, which blinded d tags exist to keep unlistable. It also binds a contribution to circle membership and not to a grant, so it cannot make a report admissible, and its aggregate is signed by whoever aggregated. Where the two projects do meet is on the consumer side. Its relay scores and the kind 30385 a node projects (Bridge 2 in § Ecosystem Bridges) land in the same kind, and a client's kind 10040 chooses among both.

Nor is reporting subsidised. Awarding a reporter standing for filing a finding would contradict the rule that standing comes only from observed conformance, and would make report-filing the cheapest way to accelerate age_factor — the one input the sybil argument needs to be unforgeable. Immunity removes the cost of honest reporting, which is the whole of what is defensible. A bounty would create a reason to go looking.

What this leaves unsolved

Retaliation. A subject that quietly downgrades its reporter's grant faces nothing, and because grants are encrypted to their subjects, no third party can observe that it happened. Immunity protects a reporter's standing in everyone else's arithmetic; it does not protect the relationship it reported on. That residual is named in § Attack surface rather than papered over.

No global state

There is no aggregate score, no consensus round, and no canonical answer to "is this node good". Two nodes with different peer sets will reach different views of the same subject and both are correct, because a view is a statement about a relationship. What federates is the evidence, and every node does its own arithmetic on it.

Operators import external trust opinions deliberately by issuing anchor tier grants. The operational consequence is explicit: an anchor grant assigns that peer a weight of 1.0 in the local trust function.

Bootstrapping

A brand-new node has no observations and no weighted reporters, so its view of everyone is neutral and it grants everyone probation. That is the correct behaviour and it is also the slowest possible start. An operator in a hurry MAY seed the table by granting trusted to nodes it knows out of band. That is a human decision, recorded as a signed event, and reversible. It is not a protocol feature and it is not automated.

Compute market

Confidential Compute Market Architecture

A node with a cryptographic DID, proven track record, and authenticated messaging channel possesses the prerequisites for decentralized compute. Fleetmesh leverages this foundation to create an open compute market without central coordination.

This is not the settlement question

§ Grants & receipts defers payment for transit, and that stands. Transit is continuous, fungible, and its scarcity is unproven; a market for it would be priced against a demand curve nobody has measured. A compute job is the opposite: discrete, specified in advance, with a natural unit and a moment of completion. The two are different economic objects and only one of them is ready. The three rules from that section bind here without exception — most importantly, money buys compute and never buys standing.

Why there is no fee to charge

Traditional marketplaces extract transaction fees by monopolizing four services: identity (counterparty authentication), reputation (historical tracking), discovery (order book matching), and settlement (escrow and clearing). Transaction fees represent the cost of proprietary access to these four functions.

In this network all four already exist as public infrastructure, and none of them belongs to anyone:

What a venue normally sells What replaces it here
identity Every participant already has a did:nostr node identity, resolvable by anyone, with no account and no signup.
reputation Kind 30802 conformance reports, weighted locally per § Trust weighting. Evidence-backed and portable — a seller's record is not held hostage by the venue it was earned on.
discovery Signed offers and asks on gossipsub and on relays. Anyone may index the order book; nobody can be the only index.
settlement Lightning, with delivery and payment coupled — accountably, not atomically — by the construction below. No escrow agent exists, so none can be paid.

The absence of fees is a structural property rather than a policy choice. Because all four marketplace functions run on open protocols, there is no centralized service left to charge a fee. Third parties may still construct value-added venues (such as curated indexing or UI frontends) and charge for their services. However, they cannot monopolize access: the order book is a public topic, and reputation consists of signed events held across the mesh.

The job, and why WASM

Two executors are defined, and the difference between them is not power but verifiability:

wasm
A WebAssembly module, content-addressed, run against content-addressed inputs under a restricted WASI surface: no ambient clock, no network, no randomness the job did not supply. Given the same module and the same inputs, every honest executor produces the same output bytes — so the output hash is checkable by re-running, by anyone, afterwards. This is the default and the only executor whose results a buyer can verify without trusting the seller.
bacalhau
An adapter for compute-over-data engines and containerized workloads. Inputs and outputs remain content-addressed. Because execution determinism is not guaranteed, results carry the same accountable delivery as any other job but no mathematical reproducibility. Buyers MUST NOT file adverse correctness reports against non-deterministic executors based solely on result hash divergence.

fleetmesh does not implement an execution engine and should never grow one. It specifies the envelope: what a job is, how it is priced, how it is paid for, and what evidence a dispute needs. Executors sit behind a stable interface, as adapters. A third executor that arrives with better determinism guarantees should require no change here.

The price unit has to be objective

Quotes are only comparable if they are quoted in something both sides can count. Wall-clock seconds are not that: they measure the seller's hardware, not the work. For the wasm executor the unit is fuel: the runtime's metered instruction count. Fuel is a deterministic function of the module and its inputs, so it is identical on every honest executor. A price is millisatoshis per million fuel units, plus a declared ceiling.

Two things follow that ordinary cloud pricing cannot offer. A buyer can know the cost before paying, by metering a dry run locally or accepting the seller's committed fuel bound. And two quotes are directly comparable, because the denominator is a property of the job rather than of the machine. Bacalhau jobs price per resource-second instead and are explicitly marked as estimates, because that is what they are.

What the order book may see

An ask has to reach sellers the buyer has not chosen yet, which is what makes it a market rather than a purchase order. It does not have to say what the job is. Those are separable, and this document separates them.

Everything a seller needs to price the work is structural: the executor, the engine, the fuel ceiling, the input length, the deadline and the verification mode. Everything that identifies the work — the module digest and the input digest — is needed only by the seller that wins. So the ask carries the first set and a commit over the second, and the job itself travels in compute/1.0/accept, encrypted to one seller after it has won.

The leak this closes

The obvious shape for this market is an ask naming the module and input, and a receipt naming them again alongside the result, the price and the payer. Chained, those two are a permanent public log of who computed what, over whose data, for how much. A module digest alone identifies the computation — a node running compute:spam-classifier/1.0 hourly would announce its moderation posture to anyone indexing the topic.

§ Grants & receipts went to considerable trouble to keep exactly this out of the peering graph: blinded pair identifiers, grants encrypted to the subject, clean windows never published. A compute market published in the clear beside it would hand back everything that bought. The construction here is not new — it is the kind 30801 envelope, reused.

Two things survive that a naive reading might expect to break. Price discovery is unaffected, because quotes are priced against the declared fuel_max ceiling, which is exactly what § The price unit already said a buyer commits to. And the buyer can still re-run its own jobs, because the buyer holds the plaintext. What it does mean is that a third party cannot re-run a job it was not given — which is why the next section stops treating re-execution as the only way to be sure.

Paying without an escrow agent

The oldest problem in trade: whoever moves first can be robbed. Lightning solves it here without a third party, because a payment already reveals a secret when it settles.

1 · ask published 2 · quotes returned 3 · job executed 4 · result sealed 5 · payment reveals k 6 · verified, per mode kind 30810, gossipsub DIDComm compute/1.0 deterministic wasm invoice hash = SHA256(k) buyer decrypts with k re-run, or a proof only if the hashes disagree → kind 30802, evidence = the seller’s own signed result hash
Steps 4 and 5 are tightly coupled, but not atomic. Settling the invoice forces k onto the wire, so payment and key disclosure happen together — which is not the same as the key opening the ciphertext. What does hold is that no third party holds anybody’s money in between, and that a seller who discloses a useless key has signed evidence against itself before it was paid.
The swap is not atomic, and this document does not claim it is

Settling an invoice forces the seller to reveal an HTLC preimage. Standard Lightning payments do not cryptographically bind preimages to decrypted payloads. A malicious seller could encrypt data with key k₁, invoice for the preimage of k₂, collect payment, and deliver an invalid decryption key.

This represents the classic zero-knowledge contingent payment problem. Closing this gap cryptographically requires zk-SNARK proofs of plaintext well-formedness, which are computationally prohibitive for lightweight nodes. Consequently, on-wire Lightning delivery is not strictly atomic.

What replaces atomicity: signing the claim before taking the money

The gap is closable without a proof system, by making a failed delivery provable rather than preventable. Before the buyer pays anything, the seller sends a signed compute/1.0/sealed naming three things at once:

compute/1.0/sealed — signed, and sent before payment

{
  "ciphertext":   "<sha256 of the bytes the buyer was handed>",
  "payment_hash": "<SHA256(k) — the invoice the buyer is about to pay>",
  "result":       "<sha256 of the plaintext that k is claimed to reveal>",
  "fuel_used":    2311904772
}

Now the seller has committed, under its own signature and before receiving anything, that this ciphertext opens under the preimage of this invoice to a plaintext with that hash. A buyer who pays and cannot decrypt, or who decrypts to something whose hash does not match, holds the seller’s signed statement and the contradicting bytes. That is precisely the evidentiary standard § Severity ladder already demands, and it makes taking payment for a useless key an S3 rather than an unprovable dispute.

Accountable, not atomic — and the difference is real

A buyer can still be defrauded once, per seller, and recovers nothing: the payment is gone and the remedy is reputational. That is strictly weaker than atomicity, and it is the honest position for a specification that has declined to require ZKCP. It is also the same bargain the rest of this document makes everywhere — misbehaviour is priced after the fact, not prevented — so at least it is consistent. An implementation wanting genuine atomicity needs the contingent-payment proof, and that remains a named direction rather than a requirement.

Pluggable Execution Runtimes (ComputeExecutor)

To prevent vendor lock-in to a single WebAssembly runtime engine, execution environments are abstracted behind the async ComputeExecutor trait. Sellers can run Wasmtime, Wasmer, WasmEdge, or distributed container runners (Bacalhau) as pluggable drivers.

crates/mesh — Modular Compute Runtime Trait

#[async_trait]
pub trait ComputeExecutor: Send + Sync + 'static {
    /// Runtime engine identifier (e.g. "wasmtime", "wasmer", "v8", "bacalhau")
    fn engine_id(&self) -> &'static str;

    /// SHA-256 digest of the standardized WASI capability profile
    fn wasi_profile_digest(&self) -> [u8; 32];

    /// Estimate fuel and provide a deterministic price quote
    async fn quote(&self, module_bytes: &[u8], limits: &ComputeLimits)
        -> Result<ComputeQuote, ComputeError>;

    /// Execute a verified module with metered fuel limits
    async fn execute(&self, module_bytes: &[u8], inputs: &[u8], fuel_cap: u64)
        -> Result<ComputeExecutionResult, ComputeError>;
}

Standardized Deterministic Capability Profile (fleetmesh-wasi-v1)

Executing untrusted stranger bytecode requires rigorous, capability-based sandboxing and mathematical determinism. fleetmesh defines the fleetmesh-wasi-v1 standard profile that every WASM executor MUST enforce:

Capability Domain Restriction / Policy Deterministic & Security Guarantee
Network I/O network: none All socket creation, outbound DNS, TCP/UDP connects, and HTTP bindings are strictly disabled. The module cannot communicate outside the host sandbox.
Filesystem fs: memory-only No host filesystem directories are mapped. The module receives only its input blob buffer in memory and writes output to its return buffer.
Clocks & Time clock: virtual System wall-clocks return the deterministic Unix timestamp of the job's created_at event. Real-time drift cannot cause execution divergence.
Entropy / PRNG prng: seeded Random number generators are deterministically seeded with SHA256(module_digest || input_digest), ensuring repeatable bitwise execution across disparate nodes.
Resource Limits memory ≤ max_bytes, fuel ≤ max_fuel Instruction counting traps and halts runaway infinite loops; linear memory bounds prevent host out-of-memory crashes.

Profile Digest: An executor computes the SHA-256 hash of its canonical profile configuration document, publishing it as the fourth element in its runtime tag: ["runtime", "wasmtime", "18.0.0", "<wasi_profile_digest>"]. Buyers verify this digest to ensure the seller executes in an identical, secure sandbox before submitting paid tasks.

Correctness is a mode, declared before the work

The shape is unchanged from § Severity ladder: your signed claim against my measurement, with divergence as the offence. A false result hash is a misreport, which is an S2; a seller that returns fabricated results at scale is an S3. The evidence requirement is satisfied by construction, because the seller signed the result hash in order to get paid for it.

What the shape does not fix is how that measurement is taken. Re-execution is a fine mechanism and a poor requirement: it obliges whoever adjudicates to hold the plaintext, which is precisely what a confidential job will not give them. So the mechanism is named in the ask, before the work happens, the same way a grant names its remedy before the breach. The verify tag was already there and already declared up front; it now has a vocabulary.

ModeWhat a dispute admits as evidenceNeeds the plaintext?
sample an independent re-run contradicting the seller-signed result hash yes
dual two executors returning different hashes for one job yes
proof the proof fails to verify against the module and input commitments no
none delivery only — the sealed commitment against the disclosed preimage no

A refutation citing evidence the declared mode does not admit is not a finding. This is not a courtesy to sellers; it is what stops a buyer choosing the cheapest verification and then litigating as though it had bought the strongest.

Sampling rate remains the buyer's choice and its own cost. Verifying everything doubles the compute bill and defeats the purpose of buying it; verifying nothing means a seller learns that lying is free. The arithmetic is worth doing rather than asserting: at sampling rate p, catching a seller who cheats on a fraction f of jobs with confidence c takes ln(1-c) / ln(1-pf) jobs. At p = 0.05 against a seller cheating every job that is about 45 jobs for 90% confidence — fast only if the buyer actually sends 45 jobs. Against one cheating a tenth of the time it is roughly 450. Any claim faster than that — "within a working day", say — is smuggling in a job rate it has not stated.

Verifiable work units, and why they close the loop

Sampling is probabilistic, and the arithmetic above is the honest description of what that costs. Under proof the seller returns evidence that the committed module, run on the committed input, produced the committed result. The buyer verifies without re-running. Nobody re-runs at all.

That is worth having on its own, but the reason it belongs in this document is that it is the same change as the section above. Confidentiality is only affordable if correctness does not require re-execution, and correctness only stops requiring re-execution if the unit carries its own proof. They are one property, not two. A proof-mode receipt can be adjudicated by a third party who never sees the job, which is the thing no amount of sampling can offer.

The cost is real and should not be soft-pedalled. Proving costs orders of magnitude more than executing, and it inverts the pricing argument: msat_per_mfuel prices execution, and a prover's bill is not proportional to the fuel it is proving about. So proof ships as an expressible mode with no reference implementation, on the same footing this document already gives contingent payment — a named direction rather than a requirement.

Proving is itself a job this market can sell

Proof generation is deterministic, expensive, content-addressed, and metered in a unit both sides can count. It is an unusually good fit for the executor this document already specifies, and compute:zk-verify-groth16/1.0 already exists as a job type. A seller without a prover can buy one from a seller that has one, priced in the same units and disputed by the same ladder. The market can pay for its own verification, which is a considerably better answer than mandating a proof system nobody has deployed.

Dual-Execution Cross-Validation Protocol ("I'll run it if you do too")

Decentralized compute faces a classic cold-start challenge: proving a new worker's competence without exposing buyers to financial loss or corrupt results.

Because the fleetmesh-wasi-v1 execution environment guarantees bitwise output determinism, the network supports dual-execution cross-validation:

Stage Action & Protocol Flow Outcome & Reputation Invariant
1. Co-Dispatch Buyer or mediator dispatches a deterministic job across two workers: Worker A (candidate / newcomer) and Worker B (established trusted anchor or buyer local runner). Both workers execute identical (module, input) tuples under the standard fleetmesh-wasi-v1 profile in parallel.
2. Result Matching Buyer compares result hashes: SHA256(Result_A) == SHA256(Result_B). Match: the result stands and the job settles normally. It earns Worker A nothing — completed jobs MUST NOT raise a tier or feed the trust weighting (buying standing, below). What a match buys belongs to the buyer: a result from an untested seller it can rely on.
Mismatch: one of the two is wrong, and the pair alone does not say which. The buyer re-runs, or dispatches to a third executor; the executor the majority contradicts is the S2 misreport on topic: compute, notice first like any S2.
3. Mutual Peer Verification During idle periods, peer nodes cross-verify each other on synthetic verification suites ("I'll run it if you do too"). A mismatch on a synthetic job is evidence like any other; a match is not, because clean results are never published. Calibration with no escrow, no arbiter, and no positive attestation.

Modular AI & LLM Inference Routing (InferenceProvider)

WASM execution handles deterministic, bitwise-verifiable compute. Modern AI workloads — language models, image generation, voice transcription — are non-deterministic. Floating-point GPU variance, temperature sampling and model quantisation each break reproducibility. Strict bitwise hash-matching does not apply to generative LLM outputs.

Rather than forcing non-deterministic AI into the deterministic fuel engine, fleetmesh abstracts AI inference behind the modular InferenceProvider adapter interface. Nodes can run local GPU workers or bridge into specialized decentralized inference marketplaces such as Routstr (OpenAI-compatible routing over Nostr & Cashu) and NIP-90 Data Vending Machines (DVMs).

crates/mesh — Modular Inference Trait

#[async_trait]
pub trait InferenceProvider: Send + Sync + 'static {
    /// Provider scheme identifier (e.g. "routstr", "nip90", "ollama", "vllm")
    fn provider_id(&self) -> &'static str;

    /// List active models served by this provider
    async fn list_models(&self) -> Result<Vec<ModelDescriptor>, InferenceError>;

    /// Estimate cost/rate per token for a given model
    async fn estimate_cost(&self, model: &str, prompt_tokens: u32)
        -> Result<InferenceQuote, InferenceError>;

    /// Stream completions over an authenticated session
    async fn complete(&self, req: &InferenceRequest)
        -> Result<Box<dyn InferenceStream>, InferenceError>;
}
Provider Scheme Protocol / Standard Settlement & Capabilities
routstr Routstr Protocol (OpenAI-compatible API) Streaming token inference (/v1/chat/completions) authenticated and paid per-request via Cashu ecash (NUTs) tokens or Lightning streaming.
nip90 NIP-90 Nostr Data Vending Machines Asynchronous request/response jobs across text, image (Flux/SD), speech, and translation using Nostr DVM events (kinds 5000–5999) with Lightning invoices.
ollama / vllm Local GPU Runner Engine Direct local execution on node-attached GPUs (NVIDIA CUDA / Apple Metal) for zero-latency private inference with local rate limiting.

Wire formats

Three kinds, verified free by the same method as the others on 2026-08-29 and listed in § Registration. Quotes are deliberately not events: a quote is addressed to one buyer and travels over compute/1.0, so a seller's pricing to one counterparty is not a public commitment to all of them.

kind 11803 — compute offer, one per selling node

{
  "kind": 11803,
  "tags": [
    ["executor", "wasm",     "msat_per_mfuel", "45"],
    ["executor", "bacalhau", "msat_per_cpu_s", "900", "estimate"],
    ["limit", "max_fuel",   "50000000000"],
    ["limit", "max_bytes",  "1073741824"],
    ["limit", "concurrent", "4"],
    ["pay", "bolt11"], ["pay", "bolt12", "<offer>"],
    ["runtime", "wasmtime", "<version>", "<wasi profile digest>"],
    ["expiration", "1789000000"]
  ],
  "content": ""
}

kind 30810 — compute ask: an order book entry, not a job specification

{
  "kind": 30810,
  "tags": [
    ["d", "<ask id>"],
    ["commit", "<sha256 of the job spec, which is not published>"],
    ["executor", "wasm"],
    ["runtime", "wasmtime", "18.0.0", "<wasi profile digest>"],
    ["fuel_max", "2500000000"],       // what a quote is priced against
    ["input_size", "1048576"],
    ["bid", "msat_per_mfuel", "50"],
    ["deadline", "1787004000"],
    ["verify", "proof", "risc0-v1"],  // declared up front, so it is not a surprise
    ["expiration", "1787004600"]
  ],
  "content": ""
}

kind 30811 — job receipt: the same envelope shape as a grant

{
  "kind": 30811,
  "tags": [
    ["d", "<blinded job identifier — only buyer and seller can derive it>"],
    ["commit", "<commitment to the receipt, which is encrypted in content>"],
    ["scheme", "sha256-blind/1", "nip44/2"],
    ["expiration", "1789600000"]
  ],
  "content": "<nip44 ciphertext to the buyer: ask, module, input, result,
              fuel_used, paid_msat, payment_hash, executor, proof>"
}

The result inside that ciphertext is still the verification anchor: a falsifiable claim, and the same value the seller committed to in the pre-payment sealed message. What has changed is who can read it. A receipt is a record of a job that went right, and § Grants & receipts already established that this protocol does not publish those — positive events are inert under the trust weighting and broadcasting them leaks topology for nothing. The argument does not stop applying because the subject is compute.

A seller MAY publish a cleartext receipt where the buyer has consented. Some buyers want a public record of work done on their behalf. That is their disclosure to make, and the point is that it is a choice rather than the default.

Modular Settlement Rails & Zero-Custody Payments (PaymentRail)

A node should not need custody of funds to participate, nor should it be hardwired to a single proprietary payment daemon. fleetmesh abstracts all payment generation, verification, and settlement behind the PaymentRail adapter interface.

Settlement Rail Protocol / Standard Properties & Role
ipd / ilp Interledger Payment Daemon (IPD / Open Payments) Universal, open-source multi-rail settlement routing streaming micro-payments across heterogeneous financial and crypto networks (ILP STREAM).
nwc NIP-47 Nostr Wallet Connect Zero-custody client-to-wallet protocol. The node holds a connection secret, not a private key, instructing the user's remote wallet to pay discrete invoices up to a budgeted cap.
bolt11 / bolt12 Native Lightning (LND, CLN, LDK) Direct Lightning Network hold-invoices and reusable BOLT12 offers for high-volume peerings and compute settlements.
cashu / fedimint Chaumian E-Cash (NUTs) Instant, private, zero-routing-fee blind tokens for rapid micro-compute settling without opening channels or maintaining liquidity.
Honest costs

No rake is not no cost. A buyer pays network routing fees, the compute it spends on verification re-runs, and the mesh capacity the job consumes under its own grant. A seller pays for inbound liquidity and electricity. Those are real and they are all paid to somebody who did something. The claim is narrow and worth keeping narrow: nothing is paid to a party whose only contribution is standing between the two of you.

What is likely to go wrong

Problem Where it stands
thin order book An open book with no market maker and few participants has terrible spreads. Early on this is a bulletin board, not an exchange, and it should be described that way rather than dressed up.
wasm determinism is not free Floating point, SIMD, threads and any ambient capability can break reproducibility. The runtime tag pins the engine and a WASI profile digest for this reason, and a buyer comparing hashes across differing profiles is comparing nothing.
the job is the attack A seller runs a stranger's code. Sandboxing is the executor's problem, not this specification's, but a node MUST treat resource limits as security boundaries and not as billing hints.
privacy of inputs The order book does not see the job — the module and input reach only the winning seller (§ What the order book may see). That seller does see them, and hiding a job from the node executing it needs a TEE, which § Scope refuses. The honest guidance is unchanged in the one direction that matters: a job is work you do not mind your chosen counterparty reading, and verify mode decides who else must read it to adjudicate a dispute.
buying standing Watched deliberately. Completed jobs MUST NOT raise a node's tier and MUST NOT feed the trust weighting. A wealthy participant can buy a great deal of compute and not one unit of reputation.

Privacy rules

Privacy Guarantees & Threat Modeling

Publishing receipts and reports makes traffic volumes public, and publishing grant envelopes makes the existence and timing of peerings public. Those are the deliberate trades: auditability of enforcement in exchange for a coarse view of how much data moved and when relationships changed.

The cost of a public grant table is not, as it is easy to assume, mere "traffic volume leakage". Such a table exposes full peering topologies, bilateral subjective ratings and exact downgrade timestamps. Blinded commitments remove that surveillance surface entirely.

  • Counters, never addresses. Receipts and reports carry aggregate counts only. Client IP addresses, user agents, and connection fingerprints stay on the node that observed them. A relay behind a proxy resolves client addresses to enforce local rate limits. That address resolution MUST NOT reach any federated mesh surface. Exposing resolved client IP addresses across the mesh creates an unacceptable deanonymization vector.
  • Counters are bucketed. Published counts round to two significant figures. Exact byte counts across short windows fingerprint individual conversations; the reputation arithmetic does not need that resolution and MUST NOT be given it.
  • Gift wraps are not a shard. The replication rule is defined in § Replication. Refusals are strictly silent. Distinguishing between nonexistent events and forbidden events leaks private communication metadata. A federated puller requesting unauthorized #p-scoped Kind 1059 events receives the exact response given to an unauthenticated stranger.
  • Interests are coarse. An interest filter is a standing declaration of what a node cares about, readable by whoever serves it. Nodes SHOULD declare interests at kind and tag granularity rather than author granularity; a node that wants one person's events should pull the shard containing them. An anchor MAY take the full firehose within a scope, which is the least revealing option available to it.
  • Leaves hide behind mediators. Hole punching exposes IP addresses to peers and rendezvous servers. The v1 leaf profile disables direct peer connections entirely. A leaf maintains a single wss connection to its mediator. Only the mediator observes its network address. Future WebRTC extensions MUST NOT make direct peer connections a default.
  • Two mediators, not one. A mediator observes message sizes, arrival times, and connection schedules. Under the Three-Layer Operator Independence Model, leaves SHOULD maintain dual-mediation with two distinct anchors. The leaf operator selects anchors with distinct operator pubkeys and separate BGP ASNs. This distributes message metadata so no single intermediary observes complete traffic patterns.

Attack surface

Security Analysis & Failure Modes

Attack Mitigation Residual
sybil flood Probation grants are near-worthless; weight requires clean windows observed by the victim; PoW on descriptors. Noise in discovery. Descriptor storage cost, bounded by expiration.
eclipse a leaf Leaves MUST hold mediation with at least two anchors publishing different operator tags, and compare their coverage fingerprints. A weak proxy, since the tag is self-declared — but mechanically checkable, which "independently operated" is not until § Open questions settles it. A leaf with exactly one operator's anchors is eclipsable. Detectable, not preventable.
receipt forgery Receipts are signed by the subject and hash-chained per window; the issuer stores the head. None on the chain. A subject can still refuse to send receipts, which is an S2 by absence.
grant forgery Grants are signed nostr events by the issuer; the subject verifies before enforcing. An issuer can shrink a grant retroactively-looking via clock games — hence the window grace period.
laundering through a forwarder Delivery envelopes carry a signed hop path; the receiver charges every node in the path it holds a grant with. A forwarder outside the receiver's grant table is uncharged — but then it is also unknown, and probation-limited.
withholding within a claimed shard completeness: asserted is falsifiable by sampling; a missing event id is evidence for an S2. Selective withholding to one peer only is expensive to distinguish from lag. Cross-checking fingerprints across peers detects it eventually.
pull amplification Wide range requests are charged to bytes_out and sync_ranges against the puller's grant. The first oversized request is served before the budget bites. Bounded by the window's remaining capacity.
retaliation for a report Evidence immunity: a reporter whose evidence verifies keeps its weight in every other node's arithmetic, so a subject cannot make reporting cost the reporter anything network-wide. Unsolved bilaterally. The subject can zero the reporter's grant, and because grants are encrypted to their subjects nobody else can see that it happened. Anonymous reporting would fix it and is not constructible — see § Trust weighting.
curated bootstrap mirror Every anchor named in a bootstrap.toml is verified by its own signed descriptor, so a mirror cannot invent nodes. The file is unsigned and grants no authority; one --bootstrap address, an empty list, gossipsub or any nostr relay each reach the mesh without it. A mirror can still serve a list naming only anchors it controls, and every one of them verifies. A first-run node's initial view is chosen by whoever served the file. Detected by comparing coverage fingerprints across anchors, or by using any second discovery path — not prevented.
false-report campaign Reports need subject-signed evidence, weigh zero from probation nodes, and cost the reporter standing when contradicted. A trusted node can spend its standing on one damaging false report. It does so exactly once.
transport key impersonation A transport key binds to a DID only through a live signed descriptor; connections from unbound keys are strangers. A stolen transport key impersonates until the descriptor is replaced. Ed25519 keys are cheap to rotate for this reason.
clock manipulation Grace period of one tenth of a window; skew over 60 s produces a notice rather than a report. Sustained skew degrades a peer's measured conformance. Operators must run NTP; the spec cannot enforce it.
split-brain partition Replication is set union; both sides remain internally correct and reconcile on heal. Divergent deletion visibility during the partition. Inherent to the data model.
The one that is not solved

A compliant anchor node that turns hostile is difficult to detect immediately because its traffic conforms to granted limits. The protocol bounds potential damage to the granted filter scope, and remedies require only a single grant modification. Detecting subtle data corruption by an otherwise compliant peer requires cross-checking range fingerprints against an independent third node. Operators requiring strict data guarantees should maintain asserted coverage locally rather than delegating validation.

Alpha contract

Alpha Evaluation Scope & Guarantees

The mesh ships as alpha and will stay there for some time. That word is doing real work: it licenses breaking changes to nearly everything specified here, on the understanding that a short list of promises holds anyway. Operators are volunteering hardware, and volunteers who get burned do not come back, so that list is the whole basis on which anyone should be asked to run a node.

Promises that hold during alpha

no data loss, ever
Mesh participation is strictly additive to local storage. A node MUST NOT delete, overwrite, or mutate existing local events based on peer messages. The only permitted store mutations are new event insertions and verified NIP-09 author deletions.
leaving is a flag
Setting --mesh=off halts mesh participation immediately without restarts or migrations. A node retains all previously stored data upon exit.
opt-in per shard
Federation is granular. Operators publish specific shards while isolating others. New nodes publish no data until explicitly configured.
enforcement stays off by default
The reputation engine starts in shadow mode: it measures, evaluates, and logs without throttling. Operators explicitly enable active budget enforcement.
a standard relay underneath
A mesh node functions as a standard, compliant NIP-01 relay for third-party Nostr clients. Standard clients (such as nak, mobile apps, or web extensions) connect and interact without requiring mesh awareness.

What alpha explicitly permits breaking

  • Kind numbers, and every tag name inside them.
  • Grant, receipt and report schemas, including which limits exist.
  • Every constant in the window arithmetic and the trust weighting.
  • DIDComm protocol URIs, message names and state machines.
  • Transport ALPNs, gossipsub topic names, and the negotiation ladder's ordering.

Breaking changes are not announced, because nothing announces anything here. A node declares the version it speaks, peers compare digests on connect, and a mismatch declines politely rather than half-speaking — see § Governance. During alpha the declared version is fleetmesh/0.1-draft, whose whole meaning is that its digest may change on any day and nobody is owed a migration. Running alpha means accepting a re-sync, and possibly several.

Reputation resets at GA

Every grant, receipt and report from the alpha period is discarded when the mesh reaches 1.0. Standing accumulated against constants that changed underneath it is not standing, and carrying it forward would bake early participants' luck into the network permanently. Nobody should join the alpha for a head start; they should join it to find out whether the thing works.

What alpha is for measuring

Alpha is not only a warning label; it is the only chance to collect the numbers that several deferred decisions are waiting on. Nodes SHOULD record and publish, in aggregate:

  • Whether grants bind at all, and which limit binds first. This is the measurement that answers § Grants & receipts' settlement question with data instead of a guess. If capacity is never the scarce thing, the settlement field gets deleted rather than filled in.
    A first answer, from a reference sync rather than from a network. Reconciling a plain text shard, events_in is exhausted long before any byte limit: those events average 290 bytes, so a member-tier events_in ceiling of 3,000 is reached with the bytes_in ceiling 39× away, and the same holds at probation and trusted because both scale together. For text traffic the byte ceilings are decoration and events_in is the grant. Either it is too tight or the byte ceilings are far too loose; one of the two should move before anyone calls these calibrated. Blob traffic is the case that would invert it, and blob_bytes defaults to zero.
  • The distribution of divergence between claimed and measured counters among peers nobody suspects. Every constant in the ladder is calibrated against that distribution or against nothing.
  • How often the transport ladder falls through to wss, and at which rung. If hole punching succeeds rarely enough, the edge class needs rethinking rather than tuning.
  • Which retention mode operators actually choose, and whether peers leaves them unable to evaluate a candidate before peering.
  • Whether disk is ever the binding constraint. Coverage declined for capacity, shards dropped for capacity rather than age, and whether any operator raises blob_bytes above its default at all. This is the measurement that answers § Grants & receipts' storage question. If nobody ever declines a shard for want of disk, storage scarcity was imaginary, and the counters are deleted rather than acted on.

kind 21802 — ephemeral diagnostic telemetry (opt-in)

{
  "kind": 21802,
  "tags": [
    ["metric", "grant_binding", "events_in:12", "bytes_out:45", "sync_ranges:3"],
    ["metric", "divergence_hist", "s0:984", "s1:14", "s2:2", "s3:0"],
    ["metric", "transport_success", "iroh:820", "libp2p:120", "wss:60", "unix:0"],
    ["metric", "retention_mode", "peers"],
    ["metric", "shard_bytes_held", "p50:8388608", "p95:134217728"],
    ["metric", "coverage_declined", "capacity:0", "policy:3"],
    ["metric", "shard_dropped", "age:2", "capacity:0"],
    ["metric", "blob_ceiling_raised", "false"],
    ["sample_window", "86400"],
    ["expiration", "1787090000"]
  ],
  "content": ""
}

Diagnostic Collection Path: Kind 21802 is an ephemeral event (NIP-01, 20000 ≤ kind < 30000) broadcast over gossipsub or published to opted-in diagnostic monitors. To preserve absolute privacy, all counters are coarse-binned and aggregated across the whole node over a 24-hour window, with no per-peer attribution or identifying metadata.

Provenance during alpha

Data received solely through the mesh carries provisional status. A peer's claim justifies verification rather than assertion. Concretely: a node MUST NOT publish completeness: asserted for shards held only through mesh replication. The gateway MUST record mesh origin on all quarantined events. This prevents third-party replication from laundering unverified peer data into authoritative records.

Implementation

Reference Implementation Components

Very little here is new machinery. A conformant node is an ordinary NIP-01 relay with three additions: a pluggable event store, a write-policy seam, and an embedded-database backend for the small targets. Good relay software already has all three. did:nostr is specified and implementable in a few hundred lines. Blossom already addresses blobs by content hash. What has to be built is range reconciliation, the mesh layer itself, and the gateway that keeps it at arm's length from whatever else an operator runs.

New crates

This is the shape a production implementation should take, and none of it exists. The one implementation that does is Python, built around the executable reference in ref/ rather than in this layout, and its only claim is that it runs the whole of this document once over a wire and found several defects doing so. Read the list below as a plan.

crates/mesh
The library. Node DID document assembly, DIDComm v2 envelopes and the six protocols, grant and receipt types with canonical serialisation, the window evaluator, the trust weighting, and the transport ladder. No I/O policy of its own.
crates/mesh-node
The binary a stranger installs: relay plus mesh, optional blob server, embedded store, no orchestrator and no external database. Statically linked for aarch64 and x86_64, built size-optimised with LTO, because the target is a device somebody already owns rather than one they buy for this.
crates/mesh-gateway
The boundary, for operators bridging an existing estate. A separate deployment in its own isolation boundary, holding its own mesh node key and its own storage, whose only credential on the other side is a relay connection. It speaks wss:// and NIP-77 to that relay exactly as an outside client would, and links none of the operator's own code. This is the piece that makes § Two networks true rather than merely intended.
crates/mesh-mobile
The leaf, exposed through UniFFI for Swift and Kotlin. One WebSocket, a range reconciler, a bounded store and a keystore binding — no p2p stack compiled in at all. It assumes the process dies without warning and checkpoints after every batch.
crates/mesh-transports
Modular, pluggable transport adapters implementing MeshTransport. Ships with feature-gated crates: mesh-transport-iroh (QUIC & hole punching), mesh-transport-libp2p (gossipsub & discovery), mesh-transport-ws (mediated WebSockets/HTTPS), and mesh-transport-unix (zero-copy local IPC).
crates/meshctl
The declarative operator CLI and orchestration engine. Parses, validates and diffs Kubernetes-style manifests (apiVersion: fleetmesh.org/v1alpha1). Manages the local runtime daemon by non-destructive hot reconciliation. Exports to Kubernetes CRDs, Podman Quadlets and systemd service units.
crates/mesh-stores
Modular event storage backends implementing MeshStore with deterministic range fingerprinting. Ships with mesh-store-lmdb, mesh-store-sqlite, and adapter interfaces for strfry, khatru, and PostgreSQL.
crates/mesh-executors
Modular compute runtime execution drivers implementing ComputeExecutor: mesh-executor-wasmtime, mesh-executor-wasmer, and container runner adapters for Bacalhau.
crates/mesh-payment
Modular settlement rail drivers implementing PaymentRail: mesh-payment-ipd (Interledger Payment Daemon / ILP STREAM), mesh-payment-nwc (NIP-47 Nostr Wallet Connect), mesh-payment-ln (LND / Core Lightning / LDK), and mesh-payment-cashu (Chaumian ecash).
crates/mesh-inference
Modular AI/LLM inference router drivers implementing InferenceProvider: mesh-inference-routstr (Routstr OpenAI-compatible proxy with Cashu token auth), mesh-inference-nip90 (NIP-90 Data Vending Machine event router), and mesh-inference-local (Ollama / vLLM local GPU driver).

What an existing relay has to add

Every item below is scoped to a capability rather than a file, because it has to be implementable against any relay codebase. Each is also independently useful: a relay that adds them all and never joins the mesh has still gained range sync, better rate limiting and a correctness fix.

Capability What it means concretely
range fingerprints The event store gains a fingerprint over an arbitrary filter-and-time range, plus id enumeration within one. Every backend an implementation ships MUST produce byte-identical fingerprints for identical event sets, or two nodes silently fail to converge.
NIP-77 frames NEG-OPEN / NEG-MSG / NEG-CLOSE on the websocket, and 77 advertised in the NIP-11 document.
grant-shaped limits Rate limiting keyed on a subject — node DID, authenticated pubkey, or IP — rather than on IP alone. Bucket sizes come from the grant in force, not from a flat constant.
grants as write policy A grant source feeding the same accept / reject / shadow-reject decision the relay's policy layer already produces. Existing allow and deny lists become its file-backed special case; any strfry-compatible plugin seam stays untouched beside it.
peer-aware read gating The gate on gift wraps gains a peer arm: a federated puller is never the recipient, and qualifies only under the rule in § Replication.
mesh egress & pluginOut A separate outbound path off the local event broadcast, applying per-grant shaping, NIP-77 interest filtering, and the pluginOut egress-policy seam. Internal fanout remains completely untouched and isolated.
DID node profile keyAgreement, DIDCommMessaging service entries, and X25519 / Ed25519 Multikey encoding alongside the secp256k1 path the did:nostr draft already defines.
monitor probing For anyone running a NIP-66 monitor: probe mesh nodes as well as relays and publish kind 30166 for them. Reads only public data, so it crosses no boundary and needs nobody's permission.
blob transfer iroh-blobs fetch alongside the BUD-04 HTTP mirror path, and the CID mapping on the read side.
One-way dependency

The dependency arrow points one way and MUST stay that way: the mesh layer links the relay; the relay never links the mesh. Every capability above is defined so that it can be implemented with no mesh types in the relay's signatures at all. A build in which the relay imports the mesh has lost the boundary before it has served a single request, and no amount of care elsewhere gets it back.

Rollout

  • W1
    NegentropyNIP-77 in the relay, both stores, conformance tests. Useful on its own — every client with range sync benefits immediately, and nothing else here works without it.
  • W2
    BoundaryThe gateway skeleton, its own namespace, keys and quota, and the unplug test wired into CI. Built before there is anything to gate, because a boundary retrofitted after traffic exists is a boundary that already leaked.
  • W3
    IdentityNode descriptors, the DID node profile, revocation. Publishable and resolvable before anything peers, so identity can be validated in isolation.
  • W4
    Control planeDIDComm v2 over the wss fallback only. The slowest transport, chosen first: it exercises every protocol without a p2p stack in the way.
  • W5
    GrantsIssuance, receipts, window evaluation, the ladder — shadow mode only. At least four weeks against real traffic before any constant is treated as settled.
  • W6
    Transportsiroh and libp2p, the negotiation ladder, mediation. The first point at which an edge behind NAT is a real participant.
  • W7
    The binaryPackaged fleetmesh-node, ARM images, a one-command install, a published bootstrap.toml. The first release an outsider can actually run — and the point at which alpha starts meaning something to someone other than us.
  • W8
    LeavesMobile library and QR pairing, at the scoped profile only: one wss connection to a mediator, foreground sync, push to wake. Last, because a leaf depends on every other layer being real — and small, because the operating systems have already decided how small.
Shadow mode is not optional

W5 ships with enforcement disabled by default and a flag to enable it. Every parameter in the arithmetic — the 110% threshold, the divergence floor, the AIMD step, the tier weights — is a guess until it has been measured against real peer behaviour. A ladder tuned on assumption will throttle honest peers on its first bad afternoon, and the first thing a volunteer network loses when that happens is its volunteers.

Operator surface

Declarative Manifests & State Reconciliation

An operator surface that takes thirty minutes to read or relies on custom imperative scripts fails before it has served a single packet. A node's whole operational surface is authored as YAML or JSON manifests, following the cloud-native pattern (apiVersion: fleetmesh.org/v1alpha1). The standalone meshctl CLI (built in Rust) manages them and reconciles them live against running meshnode daemons via the meshnode-control/1 Unix domain socket, executing non-destructive hot-reconciliation and multi-target compilation. The Python reference node consumes the same manifests once, at meshnode init --manifest.

The declarative manifest remains the single source of truth across all deployment environments. It deploys identically on Kubernetes, rootless Podman Quadlets, headless Raspberry Pis, or embedded mobile leaf runtimes.

Core Resource Kinds

KindAPI VersionRole & Responsibility
MeshNode fleetmesh.org/v1alpha1 Declares node class (anchor, edge, leaf), DID identity, listen endpoints (Iroh, libp2p, WSS), storage backend, telemetry, and operator attribution.
Peering fleetmesh.org/v1alpha1 Declares bilateral peering relationships (static peer DID + endpoints or dynamic discovery filters), transport preference, ingress/egress filter seams, and token bucket budgets.
GrantPolicy fleetmesh.org/v1alpha1 Declares admission rules, tier capacity ceilings, AIMD recovery schedules, window durations, and NIP-44 blinded commitment secrets.
ComputeProvider fleetmesh.org/v1alpha1 Declares WASM compute executor parameters, runtime engine (wasmtime, wasmer), fuel pricing (msat_per_mfuel), max memory, and Lightning payout endpoints.
InferenceRouter fleetmesh.org/v1alpha1 Declares AI/LLM inference routing adapters (routstr, nip90, ollama, vllm), model endpoints, token pricing rates, Cashu mints, and Lightning payout targets.

Declarative Manifest Examples

MeshNode Manifest (meshnode.yaml)

apiVersion: fleetmesh.org/v1alpha1
kind: MeshNode
metadata:
  name: bristol-anchor-01
  labels:
    region: eu-west
    env: production
spec:
  class: anchor
  identity:
    did: "did:nostr:d4e287a91176b6a0ff2ff2384a6c8e5473f309a47ef0e854d92305574581eb08"
    keyAgreement: "fec01c7d23a4b9180fa49c30f40d99ef87b3a98c0b2efd1487ea0b240398f498c"
  operator:
    # Distinct from the node DID above. Layer 1 of the independence model detects
    # shadow nodes by comparing operator pubkeys; reusing the node key defeats it.
    pubkey: "9b21c0f4e8a17d3562bc04ea7f19d8c05e3a6b7419fd28ce03b5a6142d7e08f3"
    policyUrl: "https://relay.example.org/policy.txt"
  endpoints:
    - transport: iroh
      ticket: "iroh_ticket_node_7f8a9b"
      priority: 10
    - transport: libp2p
      address: "/dnsaddr/relay.example.org/p2p/12D3KooWDpJ7As7BWAwRMfu1VU2WCqNjvq387JEYKDBj4kx6nXTN"
      priority: 20
    - transport: wss
      address: "wss://relay.example.org"
      priority: 40
  storage:
    engine: lmdb
    path: "/var/lib/fleetmesh/data"
    maxBytes: 107374182400  # 100 GiB
  capabilities:
    - relay
    - blossom
    - mediator
    - negentropy

Peering Manifest (peering-bristol.yaml) — Symmetrical Ingress & Egress

apiVersion: fleetmesh.org/v1alpha1
kind: Peering
metadata:
  name: bristol-community-sync
spec:
  peer: "did:nostr:e5f398b02287c7b1003003495b7d9f65840410b58f0f1965e03416685692fc19"
  transport: iroh
  ingress:
    filter:
      kinds: [0, 1, 3, 7, 10002]
      "#t": ["bristol"]
    plugin: "/usr/local/bin/mesh-filter-in"
    maxEventsPerSec: 100
    maxBytesPerSec: 2097152
  egress:
    filter:
      kinds: [0, 1, 3, 7, 10002]
      "#t": ["bristol"]
    plugin: "/usr/local/bin/mesh-filter-out"
    maxEventsPerSec: 100
    maxBytesPerSec: 2097152
  budget:
    window: 3600
    events_in: 3000
    bytes_in: 33554432
    bytes_out: 268435456

InferenceRouter Manifest (inferencerouter-routstr.yaml) — AI Inference

apiVersion: fleetmesh.org/v1alpha1
kind: InferenceRouter
metadata:
  name: routstr-gpu-bridge
spec:
  provider: routstr
  endpoint: "http://127.0.0.1:8000/v1"
  models:
    - id: "llama-3.3-70b-instruct"
      rateMsatPerToken: 12
      contextWindow: 131072
    - id: "deepseek-r1"
      rateMsatPerToken: 18
      contextWindow: 65536
  payment:
    cashuMint: "https://mint.minibits.cash/Bitcoin"
    bolt11: true

Hot-Reconciliation & Non-Destructive Semantics

A central flaw in traditional relay configurations is that changing a filter drops all active connections. meshctl enforces non-destructive hot-reconciliation:

meshctl apply -f <manifest>
Diffs the submitted YAML/JSON manifest against the active runtime daemon. If an interest filter narrows or expands, the node sends updated NIP-77 negentropy subscription ranges over the live QUIC/TLS session without dropping the connection, resetting receipt counters, or interrupting unrelated live peerings.
Graceful Peering Teardown
Deleting a Peering resource executes a graceful teardown: the node emits a DIDComm peering/1.0/terminate message, issues final budget/1.0/receipt accounting tallies, flushes any pending gift-wrap queues, and closes the channel cleanly.
Multi-Target Export Drivers
The meshctl export command compiles manifests into native deployment configuration. --target=k8s writes Kubernetes Deployments, Services and ConfigMaps. --target=quadlet writes Podman container units. --target=systemd writes systemd service units.

Distribution

Reproducible Distribution & Verification

Decentralized protocol governance requires verifiable software distribution. Downloading binaries over TLS relies on blind trust in hosting providers. Because on-wire protocol declarations are verifiable, the software producing those declarations must be verifiable as well.

What any distribution mechanism must do

Distribution is specified as architectural properties rather than a proprietary product. Fleetmesh does not hardwire a central package manager. Having eliminated centralized constants throughout the protocol, introducing a mandatory distribution registry would reintroduce central control.

publisher-signed
A release is signed by a key, and that key — not a hostname, not a registry account — is the publisher's identity. Compromise of a mirror must not be able to produce a release anyone accepts.
content-addressed
The artifact is named by its own hash, and bytes are accepted only when that hash matches. Download URLs are transport, interchangeable and untrusted, exactly as they are for blobs in § Replication.
locally verifiable
Every check — signature, hash, dependency, revocation — is performed by the installing machine against data it holds. No service is asked whether a package is genuine, because a service that can answer that question can also lie about it.
no name ownership
No global namespace and no registrar. The stable identity of a package is publisher key plus name, so two publishers may both ship fleetmesh-node and a reader is never confused about which one they installed.

npack satisfies all four

npack is a Nostr-native package manager that distributes signed release metadata over relays and stores immutable .npk artifacts — deterministic tar, zstd-compressed — on Blossom-compatible servers. It is the recommended distribution path for fleetmesh implementations, and it is recommended rather than required.

npackWhat it means here
kind 9900 The release event, authored by the publisher. Carries name, version, os, arch, format, the artifact's x (SHA-256) and the id of its NIP-94 event. Verified free in the registry on 2026-09-01 by the method in § Registration.
kind 9901 Revocation, signed by the same publisher and referencing the release by event id. Also verified free.
kind 1063 NIP-94 file metadata for the artifact, reused rather than reinvented. Its URL tags are candidate locations; the bytes are still checked against x.
kind 10063 The publisher's Blossom server list, reused as-is. Artifact retrieval prefers it over any configured fallback.
[trust] publishers A local allowlist of publisher keys, chosen by the operator. This is § Trust weighting's stance arrived at independently: no global authority, decisions local, evidence public.
repo / commit Optional NIP-34 repository address and source commit. Where present they extend the chain back past the binary to the source it was built from.
Two things it already gets right

The npack protocol specifies that package identity consists of publisher key plus package name. This matches fleetmesh's decentralized node identity model. Furthermore, an .npk package is a content-addressed blob. A fleetmesh node serves packages using existing infrastructure: the blob plane (§ Replication) transfers SHA-256-addressed payloads over iroh-blobs with BUD-04 HTTP fallback. Every anchor node functions natively as a package mirror.

Binding a release to a protocol version

This is the one fleetmesh-specific addition, and it uses a tag npack already has. A release of a fleetmesh implementation SHOULD declare the protocol version it speaks as a capability:

kind 9900 — the tags that matter to fleetmesh

["name", "fleetmesh-node"], ["version", "0.3.1"],
["provides", "fleetmesh/0.1-draft@6d8c8126a619e25c…"],
["x", "<sha256 of the .npk>"],
["repo", "30617:<pubkey>:fleetmesh-node"], ["commit", "<commit id>"]

This binding allows operators to verify the protocol version a binary speaks before installation. Nodes cross-check descriptor declarations against signed release events. A node declaring a version digest unverified by its installed release commits a misreport. The severity ladder prices this discrepancy as an S2 finding, with the publisher-signed release serving as evidence.

The distribution chain has no centralised dependency at any step. Source commits produce deterministic build archives. Independent operators sign the release hashes. Installed binaries declare canonical version digests, and peering handshakes check that those declarations match.

Tested, not merely specified

This packaging model is verified end-to-end against npack 0.2.9 (commit f1d0efe927c7). Automated tests confirm three properties: First, deterministic archiving produces byte-identical packages. Second, verifying artifacts against signed x hashes requires zero repository access. Third, altering a provides version digest invalidates the signed Nostr event ID, preventing mirrors from tampering in flight.

Reproducing the binary, without trusting who built it

A signed release proves who published a build. It does not prove what the build contains, and a single publisher key is a single point of compromise for every node that installs from it.

Release binaries MUST therefore be built under hermetic, reproducible toolchains — Nix or a pinned container image — and their SHA-256 digests MUST be independently rebuilt, verified and signed by at least two independent operators, using nostr software attestation events, before deployment. Independence is judged the same way it is everywhere else in this document (§ Governance).

Two operators agreeing on the bytes is not proof of absence of a backdoor. It is the difference between trusting one party and trusting a collusion, which is the same trade the rest of the mesh makes.

What this does not solve

ProblemWhere it stands
the first binary npack's own bootstrap is built from source or fetched from a conventional release page, and so is the first fleetmesh node. Every distribution chain terminates in something trusted for reasons outside itself. Reproducible builds narrow this to "did anyone else get the same bytes" rather than closing it.
revocation reachability A kind 9901 only protects a client that sees it. npack is explicit that revocations work where relays retain the original release. This document is equally explicit in § Replication: a deletion is a request rather than a guarantee. The honest framing is the same: revocation is a signal that propagates, not a switch that fires.
maturity npack describes itself as an early working prototype and its wire format as a project protocol, not a registered NIP. That is exactly the right reason for fleetmesh to specify properties and name an implementation, rather than to depend on one normatively.
licence npack is MIT, fleetmesh is CC0. Fine for a tool an operator chooses to run, and worth stating rather than discovering: nothing in this document requires installing it.

Conformance

Conformance Testing & Verification

Four suites, each runnable without a network:

vectors
test vectors — fixtures
Test vectors define deterministic inputs and expected outputs for: descriptor transformations, Multikey encodings, canonical grant and receipt serialization, window evaluations, and trust weighting calculations.
store conformance
A shared behavioural suite that every storage backend must pass, run identically against each. Negentropy fingerprints are the part that matters here: two stores fed the same events MUST produce identical range fingerprints, or reconciliation silently diverges between a Pi and a datacentre node.
the unplug test
The whole suite, run again with every default anchor node deleted, every fleetmesh.org lookup failing, and bootstrap.toml stripped to a single third-party address. Discovery, peering, sync, grants and reports must all still complete. It runs on every change, and a failure is not a flaky test: it means a dependency has crept back in.
the bad peer
A deliberately misbehaving node, shipped in the workspace, with a switch per offence: overrun-and-admit, overrun-and-lie, out-of-scope push, duplicate flood, over-long forwarding path, replayed receipt chain, forged hop signature. The ladder is only testable against something that actually misbehaves, and every implementation should be run against it before it is trusted with a grant.

Interoperability is the primary criterion. A conformant mesh node MUST function as a standard, unmodified Nostr relay for any client that lacks mesh awareness.

Registration

Protocol Event Kinds & Standardization

All ten event kinds were verified against three independent registries on 2026-08-29 and re-verified against live copies of all three on 2026-09-01. All ten allocations are collision-free. The verification process follows the strict, empirical guidelines defined by the NIP maintainers.

Availability

Kind Class per NIP-01 Registry NIPs table Issues & PRs
11801 replaceable free free 0 hits
11802 replaceable free free 0 hits
11803 replaceable free free 0 hits
21801 ephemeral free free 0 hits
21802 ephemeral free free 0 hits
30801 addressable free free 0 hits
30802 addressable free free 0 hits
30803 addressable free free 0 hits
30810 addressable free free 0 hits
30811 addressable free free 0 hits

Registry is nostr-protocol/registry-of-kinds, the machine-readable YAML the NIPs README now points at as preferred over its own table — 265 kinds defined, highest 39701. NIPs table is the 187-row human-curated table in the NIPs README. Issues & PRs is a GitHub search of every issue and pull request in the NIPs repository for each number, open or closed. Nothing matched anywhere.

The range classes check out against NIP-01's own wording: replaceable is 10000 ≤ n < 20000, ephemeral is 20000 ≤ n < 30000, addressable is 30000 ≤ n < 40000. So 11801–11803 are replaceable, 21801/21802 are ephemeral and 30801–30804, 30810/30811 are addressable, as the specification assumes throughout.

The compute kinds sit in the same two bands and were cleared the same way: 11803 has 11871 as its nearest neighbour above, and 30810/30811 fall inside the same gap as 30801–30803. Adjacent kind numbers show how the ecosystem is distributed. The 11k–12k band holds 11111, 11871 and 12473. The 21k–22k range holds 21000–21003, 21059 and 22242. Around 30k sit 30617/30618 and 30817–30828. The 30801–30803 allocation sits securely in the open gap between git repositories and wiki kinds.

Also verified

The multicodec values this specification asserts are correct against the multiformats table: secp256k1-pub is 0xe7, x25519-pub is 0xec, ed25519-pub is 0xed, sha2-256 is 0x12 and raw is 0x55. DIDComm protocol URIs need no registration anywhere; they are opaque identifiers compared byte-for-byte.

What it takes to become a NIP

The NIPs repository publishes five acceptance criteria. Two of them decide this outright:

  • “Fully implemented in at least two clients and one relay.” A specification written and implemented by one organisation does not qualify, however good it is. A NIP submission is therefore gated on a second, independent implementation existing — which is a statement about the mesh's adoption, not about the document.
  • “There should be no more than one way of doing the same thing.” Reviewers will ask how Kind 30802 differs from a NIP-85 kind 30385, whether the one merged NIP-85 defines or the one PR #2418 proposes under the same number. The distinction is fundamental. Kind 30385 is a third-party score about a relay, read by clients selecting connections. Kind 30802 is an evidence-backed peer verdict, carrying subject-signed cryptographic proof, read by nodes setting bilateral budgets. Different consumer, different evidentiary standard. A node publishes both, and the first is derived from the second and cites it (Bridge 2 in § Ecosystem Bridges).

The backlog is the other half of the picture. The repository has 454 open pull requests. PR #1585, NIP-37 Transport Method Announcement — which is roughly half of what this specification's node descriptor does — has been open since November 2024 and was last touched in March 2026. Planning around a merge is planning around something outside anyone's control.

The recommended path, in order

  • 1
    Register the kinds nowSubmit the ten definitions to registry-of-kinds, whose stated bar is “any reasonable event definition can be added here” and which explicitly does not require an implementation guide. This is collision avoidance, it is cheap, and it is the step that stops someone else taking 30801 next month.
  • 2
    Publish and implementShip this document and the reference implementation, with kind numbers treated as alpha-mutable per § Alpha contract. Standards on nostr emerge as often from someone doing a thing and others copying it as from a document, and the repository says so itself.
  • 3
    Publish it on nostr, not only over HTTPA specification reachable only at one domain asks the reader to trust that domain stays up. NostrHub carries decentralised proposals as addressable kind 30817 events and browses repositories announced under NIP-34, so the document and its source become ordinary events that any relay can serve and anyone can mirror. scripts/publish_nostrhub.py builds both: the kind 30817 NIP from nip/fleetmesh.md, whose tables are generated from the same schema/ files this version digest is taken over, and a kind 30617 announcement of the repository. This costs nothing and asks nobody for permission, which is the same argument as step 1.
  • 4
    Submit a NIP when a second implementation existsDo not submit prematurely. When ready, split the submission. The node descriptor and reputation kinds are separable. A targeted NIP covering bilateral grants and conformance reports has a far higher probability of consensus than a monolithic federation specification.
  • 5
    Reconcile with #2418 on the way inA node publishes a derived kind 30385 beside its kind 30802: the same evidence, summarised as a client-facing score. It is published now, under merged NIP-85 and NIP-73, because those are the documents that already assign the number; Bridge 2 in § Ecosystem Bridges gives the shape and ref/projection.py is the serialiser. Reuse answers criterion 4 better than an argument does, and it cost one serialiser. The proposal is still open and its thread has never discussed the collision. If it merges on another number a second serialiser follows it. If it merges on this one the k tag tells the two shapes apart.
Assume no NIP

Nothing in this specification should depend on a NIP being accepted. The kinds are registered to avoid collisions, the mesh works whether or not the wider ecosystem adopts them, and a rejected or ignored submission changes nothing operationally. The one thing that genuinely benefits from a NIP is NIP-77 negentropy, and that one is already merged.

Open questions

Open Empirical Questions

Four questions are undecided. Each names what would settle it and what happens if the measurement comes back the wrong way, because a question with no losing answer is not a question. Everything else this document raises, it answers in the section that raises it.

  1. Whether commitments are enough, or aggregate proofs are needed. Grants operate as blinded commitments encrypted to the subject. Third parties query via standing/1.0 rather than scraping public tables. This shields the peering graph from passive surveillance. Production deployments will measure two operational factors: whether query response rates are sufficient to evaluate newcomers, and whether declining to answer remains socially neutral. If either condition degrades, aggregate zero-knowledge proofs (§ Trust weighting) will transition from an optional enhancement to a core requirement.
  2. Whether proof mode is ever economic. The verification modes in § Compute market make a self-verifying work unit expressible, and confidentiality is only affordable because of it — a proof-mode receipt can be adjudicated by a third party that never sees the job. What is unmeasured is the price. Proving costs orders of magnitude more than executing, and it breaks the denominator: msat_per_mfuel prices execution, and a prover's bill is not proportional to the fuel it proves about. Alpha should measure which mode buyers actually select, and at what ratio of proving cost to job cost proof stops being theoretical. If the answer is never, the mode is deleted rather than defended, and confidential jobs stay disputable only by disclosure. Nothing in the protocol assumes a prover exists.
  3. How the second implementation happens. Formal NIP standardization requires two independent implementations. There is now one, written alongside this document by the same hand, which is not independence: it proves the arithmetic runs and it cannot prove the document is readable by somebody else. What it did settle is that the document is implementable, and it found several defects doing so, each recorded where it was fixed. All required fleetmesh capabilities (§ Implementation) are designed for straightforward integration into existing relay codebases (such as strfry or khatru). The fastest path to a second implementation is adding fleetmesh adapter modules to an existing relay rather than constructing a new node from scratch.
  4. Whether algorithm profiles are ever needed. Every primitive this document relies on is named once in constants.json under crypto. Two of them are on no FIPS 140-3 approved list: BIP-340 over secp256k1 and NIP-44. The design note The curve is not on the list has the audit. The shape of a profile is settled. It is policy and not a version. It is declared as a c capability. It is negotiated at the handshake as an intersection the way transports are. It is described by a document with its own digest for anyone who needs to cite one. It is never a second protocol tag: a digest compared for equality would partition the network. It never lands inside the digested constants: a change a handful of operators want would rev every node's declared version. What is not settled is whether to build it. Doing so widens the envelope scheme and the agreement-key codec from constants to sets that every implementation must parse. It also needs an envelope convention for kind 30801 and 30811 content that no NIP defines. The trigger is the one this section already uses: a consumer that asks. Until then a compliant deployment is a gateway boundary and the base does not move.