Nostr WoT

Documentation

Everything you need to integrate Web of Trust into your application.

Oracle API

Version 0.3.0: directed follow distance and separate public mute evidence over HTTP. No extension required.

Public server and request format

Base URL: https://wot-oracle.mappingbitcoin.com. No API key is required by the Oracle.

Send pubkeys as full 64-character lowercase hexadecimal strings, not npubs. The examples use synthetic pubkeys to illustrate response shapes; your graph results will differ. POST requests use Content-Type: application/json.

The root endpoint GET / lists the service version, documentation and available endpoints. For your own instance, see the self-hosting guide.

Distances use directed kind-3 follow edges. Public kind-10000 mute lists are separate observations. The Oracle does not combine them into a trust score or remove muted accounts from follow paths.

Endpoints

GET/health

Process liveness and release version.

json
{
  "status": "healthy",
  "version": "0.3.0"
}
terminal
$curl "https://wot-oracle.mappingbitcoin.com/health"

A healthy process can still be waiting for relay events or unable to persist updates. Use /ready for ingestion readiness.

GET/ready

Ingestion snapshot: HTTP 200 when ready, HTTP 503 otherwise.

Readiness requires running ingestion, no current database failure and a follow or mute event received within five minutes. A quiet private relay can therefore yield 503 even while the process is operational. Example while waiting for the first event:

json
{
  "running": true,
  "ready": false,
  "last_event_received_at": 0,
  "last_persisted_at": 0,
  "persisted_events": 0,
  "lagged_notifications": 0,
  "persistence_errors": 0,
  "coverage": "configured_relays_only"
}

Timestamps are Unix seconds, with zero meaning not yet observed. persisted_events counts accepted author updates written to storage after batch coalescing. lagged_notifications and persistence_errors expose ingestion problems.

GET/stats

Indexed graph counts, cache settings, lock metrics and ingestion status.

json
{
  "node_count": 3,
  "edge_count": 2,
  "nodes_with_follows": 2,
  "mute_edge_count": 0,
  "nodes_with_mute_lists": 1,
  "sync": {
    "running": true,
    "ready": false,
    "last_event_received_at": 0,
    "last_persisted_at": 0,
    "persisted_events": 0,
    "lagged_notifications": 0,
    "persistence_errors": 0,
    "coverage": "configured_relays_only"
  },
  "cache": {
    "size": 0,
    "capacity": 100000,
    "ttl_secs": 300
  },
  "locks": {
    "write_lock_count": 0,
    "write_lock_avg_us": 0,
    "write_lock_max_us": 0,
    "read_lock_count": 0,
    "read_lock_avg_us": 0,
    "read_lock_max_us": 0
  }
}

Counts and settings above are illustrative. edge_count counts follow edges; mute_edge_count counts public pubkey mute edges. nodes_with_mute_lists includes known lists with no public pubkey entries. Lock timings are in microseconds. The sync object has the same fields as /ready.

GET/distance

Shortest directed follow distance from one pubkey to another.

  • from and to: required source and target pubkeys.
  • max_hops: 1–5, default 3.
  • include_bridges: boolean, default false. Includes search meeting nodes when available.
  • bypass_cache: boolean, default false. Recomputes from the current indexed graph; does not fetch new relay data.
json
{
  "from": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  "to": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
  "hops": 2,
  "path_count": 1,
  "mutual_follow": false
}

hops is zero for self-distance and one for a direct follow. A null value means no route was found within the requested depth in the indexed graph. It does not prove that no connection exists elsewhere on Nostr.

path_count counts shortest directed paths, including when bridges are omitted; counts saturate at the maximum unsigned 64-bit integer. mutual_follow indicates a direct follow in both directions. Optional bridges contains search meeting nodes, not a full path or proof of disjoint paths.

terminal
$curl "https://wot-oracle.mappingbitcoin.com/distance?from=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&to=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb&max_hops=3"

POST/distance/batch

Query up to 100 targets from one source, preserving target order and duplicates.

Required JSON fields: from and targets. Optional max_hops, include_bridges and bypass_cache use the same defaults as /distance.

Request

json
{
  "from": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  "targets": [
    "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
  ],
  "max_hops": 3
}

Response

json
{
  "from": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  "results": [
    {
      "from": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
      "to": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
      "hops": 2,
      "path_count": 1,
      "mutual_follow": false
    }
  ]
}

Each result includes both from and to.

GET/path

Return intermediate pubkeys on one shortest directed follow path.

Required from and to; optional max_hops (1–5, default 3).

json
{
  "from": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  "to": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
  "path": [
    "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
  ]
}

The example represents two follow edges, from source to bridge to target. Source and target are excluded from path. Self and direct-follow paths return an empty array; no route within the requested depth returns null. This response has no hops field.

GET/follows

Paginate the currently indexed follow list for a pubkey.

Required pubkey; optional offset (default 0) and limit (default 500, capped at 5000). total is the full indexed list size, independent of the page size.

json
{
  "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  "follows": [
    "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
  ],
  "total": 1
}

An unknown pubkey returns an empty list and total: 0.

GET/common-follows

Return pubkeys directly followed by both accounts.

Required parameters: from and to.

json
{
  "from": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  "to": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
  "common_follows": [
    "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
  ]
}
terminal
$curl "https://wot-oracle.mappingbitcoin.com/common-follows?from=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&to=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"

GET/mutes

Paginate public pubkey entries from an indexed kind-10000 mute list.

Required pubkey; optional offset (default 0) and limit (default 500, capped at 5000). total is the full indexed list size, independent of the page size.

json
{
  "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  "mutes": [],
  "total": 0,
  "public_list_known": true
}

public_list_known: false means no mute-list event was indexed for this pubkey. A known event can have no public pubkey entries, as shown above. Encrypted mute entries are unavailable to the Oracle, and a known empty public list may still contain encrypted entries. Word, hashtag and thread mute tags are excluded from pubkey evidence.

GET/trust

Return follow distance alongside separate public mute observations.

  • from and to: required source and target pubkeys.
  • max_hops: 1–5, default 3.
  • include_bridges: boolean, default false. Includes search meeting nodes when available.
  • bypass_cache: boolean, default false. Recomputes from the current indexed graph; does not fetch new relay data.
json
{
  "follow_distance": {
    "from": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
    "to": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
    "hops": 2,
    "path_count": 1,
    "mutual_follow": false
  },
  "public_mute_evidence": {
    "source_mutes_target": false,
    "target_mutes_source": false,
    "followed_muters": [
      "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
    ],
    "source_mute_list_known": true,
    "target_mute_list_known": false
  }
}

followed_muters lists accounts directly followed by the source whose indexed public mute lists contain the target. The two direct-mute booleans describe the source and target relationship; the known-list flags distinguish missing lists from known public lists.

Mutes can express personal preference. Missing public evidence is not an endorsement. Clients decide how to use these observations: the response applies no weight, aggregate score or automatic exclusion. Follow distance and mute evidence may be read at slightly different instants during ingestion.

Coverage and freshness

sync.coverage is configured_relays_only. Results describe events indexed from the server's configured relays, with no guarantee of global or complete coverage. Cache entries become invalid when the graph revision changes. Neither readiness nor bypassing the cache guarantees that relays have returned the latest event.

Kind-0 profile caching, /profiles and include_profiles are not implemented in v0.3.0.

Limits and errors

Data endpoints use a per-IP token bucket configured by RATE_LIMIT_PER_MINUTE. Limits depend on the deployment; requests can be rejected after a burst even before a minute has elapsed. The root, /health and /ready routes are exempt from this limiter. Request bodies are capped at 1 MiB. Do not assume rate-limit headers are present on every response.

Application validation and computation errors return JSON with error and code:

json
{
  "error": "Invalid pubkey format",
  "code": "INVALID_PUBKEY"
}
  • 400: INVALID_PUBKEY, INVALID_MAX_HOPS or TOO_MANY_TARGETS.
  • 413: request body exceeds the size limit.
  • 429: per-IP rate limit exceeded. Back off before retrying; honor retry timing if provided.
  • 500: INTERNAL_ERROR.
  • 503: QUERY_BUSY when query capacity is exhausted, or a readiness snapshot with ready: false from /ready.

Malformed query strings, malformed JSON and middleware rejections may use a different body format. Check the HTTP status before parsing a successful response. Use bounded retries with backoff for temporary overload.