Digital Trust Protocol

Status: draft · 2026-09-30

Protocol#

The Global Server implements exactly the same resolver protocol v1 as the Personal Server — messages, operations, defaults, results, errors, auth, ingest, deletions, guardrails — and passes the same session suite (TrustGraph conformance.md § One API). Nothing below adds or changes an operation; it only sets values of the shared settings for a public deployment.

Global deployment configuration#

Shared settingGlobal valueWhy
listenPublic, TLS (wss://<host>/)Serves anyone
allowedOriginsanyAny app may use it
auth.readsopen (option: members)Anonymous reads under quotas
auth.writesmembersOnly whitelisted writers (Sync Servers) may push events; do not open it on a public server
auth.adminsOperator pubkeysNIP-86 management
maxInFlight16Many connections share the machine
maxConnections4 read per IP, 8 write per writer pubkeyFair use
operationDeadline / batchDeadline250 ms / 500 ms (protocol defaults 500 / 1 000)Tighter latency on a shared server
maxDepth6 (protocol default 7)Bounds cold work
rateLimitCost units per client (below)Fair use

Authentication#

Standard Nostr relay authentication (NIP-42), with a standard whitelist (NIP-43), managed with a standard API (NIP-86) — the shared mechanism of resolver-api.md § Authentication and access.

AccessGlobal valueWith auth.reads = members
Resolver and lookup operations, REQOpen, anonymous, rate-limited per IPMembers with role read only
EVENT (ingest)Members with role write onlysame
NIP-86 managementOperator pubkeys and members with role admin, via NIP-98same
  • On connect the server sends ["AUTH", <challenge>]. A client answers with a kind 22242 event (relay and challenge tags); the server checks it per NIP-42 and answers OK. Several pubkeys may authenticate on one connection.
  • Refusals use the NIP-42 prefixes: auth-required: (not authenticated), restricted: (authenticated but not a member with the needed role).
  • Whitelist: a NIP-43 kind 13534 membership list signed by the server's self key (published in NIP-11), with ["member", <pubkey>, <role>…] tags; roles are kind 33534 events with d = read, write, admin. Anyone can read both with REQ. The server re-signs 13534 after each change.
  • Management: NIP-86 allowpubkey, unallowpubkey, listallowedpubkeys, assignrole, unassignrole, banpubkey, over HTTP with NIP-98 auth; or the CLI (dtp-global members …).
  • Later — WoT as the whitelist: membership source wot derives members from the operator's web of trust (pubkeys that qualify within a configured degree and threshold of the operator root, via the same model). It is published as the same 13534 list, recomputed on graph changes. Clients and writers see no protocol change.

Authenticated members also get higher quotas than anonymous clients.

Limits and guardrails#

Resolve guardrails are the shared ones of resolver-api.md § Guardrails, with the Global values above, enforced as in resolver-engine.md § Guardrails. Memory guardrails are in graph-engine.md § Memory budget.

TierQuota (default)
Anonymous4 read connections and 20 cost units/s per IP, burst 100
Member (read)Per member, set by the operator (default 10× anonymous)
Writer (write)8 connections per pubkey; 5 000 pending OKs per connection
  • Rate limiting counts cost units, not messages: a warm batch item costs 1, a cold field build costs according to the work it did, a path query costs more. Expensive requests from anonymous clients queue behind cheap ones.
  • Past maxInFlight (16) the server answers busy; over the rate limit, rate-limited — the same codes every deployment uses.
  • Idle read connections are closed after 5 minutes.

Privacy#

A query reveals the root and the subjects it asks about. For Attention, the subjects are the X accounts and posts on the user's screen, which is browsing history. Commitments, published in the Global Server privacy policy on the Website:

  1. No logging of subjects or message bodies. Access logs keep time, message type, operation, status, latency, and a truncated IP hash, for 7 days.
  2. No (root, subject) pairs are persisted. Field caches are in memory only.
  3. Active roots (needed for admission) are stored as pubkey + last active day, deleted after 30 days of inactivity.
  4. Metrics are aggregates without identifiers.
  5. TLS only. No third-party analytics or trackers.
  6. The server never receives X identity or post data; it can only learn the X ids that appear as query subjects.

Privacy modes for clients#

ModeWhat the server learnsHow
Query modeRoot and queried subjectstrust.batch, rating.batch, …
Field modeRoot onlyClient calls graph.field (who counts per level, and the binds decided in the requested mode) and resolves subjects itself from relay data. For each subject, the hitting degree is min(level(A) + 1) over counted statements by authors in the field. Results match the server's.
Frontier modeRoot onlyA Personal Server calls graph.frontier for sync hints and runs its own exact heap (PersonalServer sync.md)

Attention's use of the Global Server is opt-in and disclosed in Settings. Field mode needs a hit-test function inside @dtp/trust-graph (the one trust walk may not be re-implemented outside it), which needs the same approval as any change there. Until then Attention can only offer query mode, and the Settings disclosure must say what the server learns.

Verifiability#

  • Every result carries sourceEventIds; REQ with ids serves the signed events, which clients can verify.
  • Field and frontier results return evidence ids (via) so a client can check any listed hop.
  • The membership list is itself a signed, readable Nostr event.

Configuration#

dtp-global.toml (resolver):

SectionKeys
[server]listen address, TLS, instance_name, server key reference (NIP-11 self)
[model]scope_profile = "x.com", default thresholds, default bindingMode
[auth]reads = "open" | "members", operator pubkeys, membership source (nip43; later wot)
[ingest]pending-OK window, writer connections per pubkey, queue size
[retention]seed anchors, d_reach, active-root TTL, per-author and total disk caps
[guardrails]deadlines (query and background), max degree, node and record budgets, statement / path / neighborhood limits, concurrent cold builds
[memory]process limit, the shares of graph-engine.md § Memory budget, pin policy (auto pins the hot format when it fits)
[residency]prefetch queue depth, re-touch interval for active roots, hot-page list interval
[limits]tiers, cost units, connections, in-flight, message sizes
[storage]data directory for the graph store and event store, RocksDB options, flush size, merge and base-rebuild thresholds
[admin]listen (default 127.0.0.1:<port + 1>), auth (local or nip98), alert thresholds

dtp-sync.toml (Sync Server): relays, backfill policy, target endpoints, signing key reference, [admin] for its own telemetry pages (sync-server.md § Sync Server).

Secrets (server key, writer keys) come from files or environment variables, never from the TOML.

Deployment#

  • Two static binaries for Linux x86_64 and aarch64 (dtp-global, dtp-sync); Docker images; systemd units.
  • Sizing: RAM sets how much of the graph is warm, disk sets how large it can be. A resolver with 16 cores and 32 GB RAM keeps a 1 B-edge graph fully resident; 64 GB RAM and 2 TB NVMe serve a 5 B-edge graph with the hottest part resident. Use local NVMe (not network storage) for the graph store: cold subjects are read with many small random reads. Sync Server: 4 cores, 8 GB RAM, small disk.
  • Resolver replicas behind a load balancer with root-affinity hashing and sticky connections (architecture.md § Topology); root affinity also keeps each replica's page cache focused on its roots.
  • Rolling restarts memory-map the segments and replay the delta; /readyz turns true when replay finishes, and the working set warms in the background. Clients and writers reconnect, answer AUTH, and resend.

Observability#

Telemetry pages. The resolver serves the shared admin telemetry pages, JSON, and /metrics on its admin listener (default 127.0.0.1:<port + 1>), exactly as the Personal Server does (TrustGraph telemetry.md). On a remote machine, reach them through an SSH tunnel; binding the admin listener to another interface requires admin.auth = nip98 with an operator pubkey. The Sync Server serves the same pages for what applies to it (Sources, Ingest to its targets, Memory, Logs).

KindExamples
Shared metricsThe dtp_* names in telemetry.md § Metric names: readiness, graph size and bytes, ingest by kind and result, sources, operations and latency (warm vs cold), guardrail hits, field cache, connections, memory shares, roots, alerts
Implementation metrics (dtp_impl_*)Page reads per query and prefetch latency, delta size, segment count, dead ratio, pinned vs resident bytes, compaction progress, retention class sizes
Health/healthz (process up), /readyz (segments mapped, delta replayed, ingest caught up)
LogsStructured (tracing), no identifiers at info level; the last warn / error lines appear on the Logs page

Abuse#

  • Anonymous clients can only read, within cost-unit quotas.
  • Writers are whitelisted; a misbehaving writer is removed with NIP-86 unallowpubkey or banpubkey, and every event it sent was validated anyway.
  • Root activation and want demands are queued and rate-limited per client.
  • Unreached authors are capped per author and in total on disk, and evicted first when disk is short; a walk never pages them in unless an anchor's web of trust reaches them, so they cost no RAM.