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.orgappears 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.
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.
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:
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.
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.
wss:// connection to its mediator. Leaves do not negotiate dynamic transport ladders because upper rungs do not survive mobile background execution.
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.
Browser progressive web applications share the leaf operational profile. WebRTC and direct browser-to-browser peering remain future roadmap items compatible with this baseline.
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.
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
--bootstrapCLI 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 underfleetmesh.orgrather 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 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.
| Asset | What it actually risks | Disposition |
|---|---|---|
| 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
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/1, not an amended 0.
What event constitutes the freeze, and who gets to say it has happened, is genuinely
unresolved — see § Open questions.
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:
bootstrap.tomlFragmentation 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 operators | What 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:
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.
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.
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.
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
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
- 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.
-
Announce. The node joins the libp2p gossipsub topic
/fleetmesh/announce/1and 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. -
Bootstrap is editable.
Operators can edit or empty
bootstrap.tomlfreely. Builds shipping an empty bootstrap list MUST operate given a single--bootstrapCLI flag. Shipped list composition rules are defined in § Governance. -
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.0confirms the declaration once a channel exists; it does not replace it. A peer SHOULD answer apeers-querywith 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.
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.
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. |
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:
fleetmesh/didcomm/1. One bidirectional QUIC stream per thread. Default whenever both nodes
advertise iroh./fleetmesh/didcomm/1.0.0 over Noise + yamux. Used when iroh is unavailable or when
a circuit relay is already established.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.
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.
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:
-
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"] } -
Independent Anchor Selection: The leaf inspects the returned descriptors, filtering
for anchors that support WebSocket endpoints (
wss://), and selects two anchors publishing distinctoperatorpubkeys (satisfying the 3-Layer Operator Independence Model). -
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 attier: 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.
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.
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:
| Difference | Round trips | Bytes | Against 32 bytes per id |
|---|---|---|---|
| none — the sets match | 1 | 337 | — |
| 3, scattered | 2 | 2,819 | 29× |
| 500, contiguous and recent | 2 | 17,153 | 1.1× |
| 500, scattered through the history | 2 | 142,302 | 8.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.
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.
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"]} |
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 router | fleetmesh | |
|---|---|---|
| peer identity | a URL you typed | a key, resolvable as did:nostr |
| relationship | one-sided config; the far end never agreed | negotiated, expressed as two grants |
| direction | up / down / both | push / pull / both — near-identical, arrived at separately |
| selection | a nostr filter per stream | an interest filter, scoped by the grant |
| reconciliation | live REQ with an implicit limit:0 | NIP-77 negentropy, required to peer at all |
| abuse control | plugins the operator writes | published budgets, signed receipts, a fixed ladder |
| discovery | none, by design | gossipsub, relays and descriptors |
| forwarded traffic | invisible to the far end | signed hop path, charged to every node in it |
| operator surface | one legible config file | grants, 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.
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.
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.
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.
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.
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 trigger | quantum walk probability amplitude: |⟨i|exp(−iLt)|s⟩|² > θ | NIP-77 negentropy range reconciliation over grant filters |
| propagation path | wave interference across Laplacian; multi-hop phase resonance | hierarchical fingerprint bisection over scoped interest sets |
| consensus model | diffusive gossip delta averaging toward global round convergence | strictly subjective local views; no global state or shared rounds |
| reputation & spam | continuous exponential damping: exp(−2γ|rep|t) + Kind 1984 | 5-tier ordinal ladder (S0–S4) + evidence-backed Kind 30802 reports |
| rate enforcement | internal per-client & per-peer token buckets | bilateral published budgets (Kind 30801) + signed receipt hash chains |
| node classes | homogeneous server-to-server (public ports 443 + 8443) | 3-tier heterogeneous mesh: Anchor (VPS), Edge (NAT), Leaf (Mobile) |
| wire & transport | Nostr WebSockets (wss://) on TLS peer mesh port | iroh (QUIC/hole-punching), libp2p gossipsub, HTTPS, Kind 21059/1059 |
| peer identity | relay URL + TLS certificate + NIP-42 pubkeys | W3C DID (did:nostr) with multikey binding |
| extended services | event storage (SQLite/memory) + WebSocket relay | decentralized WASM compute market (Kind 30810/30811) with Lightning |
Where Quantum Relay is simply better
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.
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 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 / Standard | Role in fleetmesh | Why 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
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>"
}
- 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
dis the subject's relay URL in the canonical formprojection.canonical_relaystates inconstants.json, and whosekisweb, 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.rankis 100 times the multiplier the ladder applied: 50 for S1, 25 for S2, 0 for S3 and S4.pnames the subject node andacites 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, under30385: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.pyis the serialiser andscripts/interop-nostr-veil.pyholds 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": ""
}
fleetmesh-wasi-v1 runtime under strict fuel limits. Verified results come back as kind 6000–6999 DVM responses, with kind 7000 job status alongside.
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).
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.
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 anyone | Visible 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.
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.
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.
| ceiling | probation | member | trusted | anchor |
|---|---|---|---|---|
| conn_rate | 4 | 12 | 48 | 192 |
| req | 60 | 600 | 2400 | 9600 |
| events_in | 300 | 3000 | 12000 | 48000 |
| bytes_in | 3355443 | 33554432 | 134217728 | 536870912 |
| bytes_out | 26843545 | 268435456 | 1073741824 | 4294967296 |
| blob_bytes | 1677721 | 16777216 | 67108864 | 268435456 |
| sync_ranges | 40 | 400 | 1600 | 6400 |
| errors | 4 | 40 | 160 | 640 |
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. |
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
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 … } }
}
}
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:
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.
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:
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 atmemberor 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
dblinds 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.
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.
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.
§ 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:
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.
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.
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.
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.
| Mode | What a dispute admits as evidence | Needs 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. |
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
wssconnection 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
operatorpubkeys 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. |
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
--mesh=off halts mesh participation immediately without restarts or migrations.
A node retains all previously stored data upon exit.
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.
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
settlementfield gets deleted rather than filled in.
A first answer, from a reference sync rather than from a network. Reconciling a plain text shard,events_inis exhausted long before any byte limit: those events average 290 bytes, so a member-tierevents_inceiling of 3,000 is reached with thebytes_inceiling 39× away, and the same holds at probation and trusted because both scale together. For text traffic the byte ceilings are decoration andevents_inis 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, andblob_bytesdefaults 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
peersleaves 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_bytesabove 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.
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.
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.
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).
apiVersion: fleetmesh.org/v1alpha1). Manages the local runtime daemon by non-destructive hot reconciliation. Exports to Kubernetes CRDs, Podman Quadlets and systemd service units.
MeshStore with deterministic
range fingerprinting. Ships with mesh-store-lmdb, mesh-store-sqlite,
and adapter interfaces for strfry, khatru, and PostgreSQL.
ComputeExecutor:
mesh-executor-wasmtime, mesh-executor-wasmer, and container runner
adapters for Bacalhau.
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).
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. |
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 publishedbootstrap.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
wssconnection 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.
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
| Kind | API Version | Role & 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:
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.
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.
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.
| npack | What 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. |
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.
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
| Problem | Where 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:
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.
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.
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.pybuilds both: the kind 30817 NIP fromnip/fleetmesh.md, whose tables are generated from the sameschema/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.pyis 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 thektag tells the two shapes apart.
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.
-
Whether commitments are enough, or aggregate proofs are needed.
Grants operate as blinded commitments encrypted to the subject. Third parties query via
standing/1.0rather 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. -
Whether
proofmode 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_mfuelprices 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 costproofstops 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. -
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
strfryorkhatru). The fastest path to a second implementation is adding fleetmesh adapter modules to an existing relay rather than constructing a new node from scratch. -
Whether algorithm profiles are ever needed.
Every primitive this document relies on is named once in
constants.jsonundercrypto. 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 accapability. 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 secondprotocoltag: 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.