draft optional
This NIP defines an extension to the REQ message filter that enables efficient social graph traversal queries without requiring clients to fetch and decode large numbers of events.
Nostr's social graph is encoded in event tags:
p tags listing followed pubkeyse tags linking replies, reactions, reposts to their targetsp tags in any event kind referencing other usersClients building social features (timelines, notifications, discovery) must currently:
p tagsThis is inefficient, especially for:
Relays with graph-indexed storage can answer these queries orders of magnitude faster by traversing indexes directly without event decoding.
_graphThe _graph field is added to REQ filters. Per NIP-01, unknown fields are ignored by relays that don't support this extension, ensuring backward compatibility.
["REQ", "<subscription_id>", {
"_graph": {
"method": "<method>",
"seed": "<hex>",
"depth": <number>,
"inbound_refs": [<ref_spec>, ...],
"outbound_refs": [<ref_spec>, ...]
},
"kinds": [<kind>, ...]
}]
method (required)The graph traversal method to execute:
| Method | Seed Type | Description |
|---|---|---|
follows | pubkey | Traverse outbound follow relationships via kind-3 p tags |
followers | pubkey | Find pubkeys whose kind-3 events contain p tag to seed |
mentions | pubkey | Find events with p tag referencing seed pubkey |
thread | event ID | Traverse reply chain via e tags |
seed (required)64-character hex string. Interpretation depends on method:
follows, followers, mentions: pubkey hexthread: event ID hexdepth (optional)Maximum traversal depth. Integer from 1-16. Default: 1.
depth: 1 returns direct connections onlydepth: 2 returns connections and their connections (friends-of-friends)Early termination: Traversal stops before reaching depth if two consecutive depth levels yield no new pubkeys. This prevents unnecessary work when the graph is exhausted.
inbound_refs (optional)Array of reference specifications for finding events that reference discovered events (via e tags). Used to find reactions, replies, reposts, zaps, etc.
"inbound_refs": [
{"kinds": [7], "from_depth": 1},
{"kinds": [1, 6], "from_depth": 0}
]
outbound_refs (optional)Array of reference specifications for finding events referenced by discovered events (via e tags). Used to find what posts are being replied to, quoted, etc.
"outbound_refs": [
{"kinds": [1], "from_depth": 1}
]
ref_spec){
"kinds": [<kind>, ...],
"from_depth": <number>
}
kinds: Event kinds to match (required, non-empty array)from_depth: Only apply this filter from this depth onwards (optional, default: 0)Semantics:
ref_spec objects in an array have AND semantics (all must match)ref_spec have OR semantics (any kind matches)from_depth: 0 includes references to/from the seed itselffrom_depth: 1 starts from first-hop connectionskinds (standard filter field)When present alongside _graph, specifies which event kinds to return for discovered pubkeys (e.g., kind-0 profiles, kind-1 notes).
All graph query responses are returned as signed Nostr events created by the relay using its identity key. This design provides several benefits:
| Kind | Name | Description |
|---|---|---|
| 39000 | Graph Follows | Response for follows/followers queries |
| 39001 | Graph Mentions | Response for mentions queries |
| 39002 | Graph Thread | Response for thread traversal queries |
These are application-specific kinds in the 39000-39999 range.
When a REQ contains only the _graph field (no kinds, authors, or other filter fields), the relay returns a single signed event containing the graph traversal results organized by depth.
["REQ", "<sub>", {
"_graph": {
"method": "follows",
"seed": "<pubkey_hex>",
"depth": 3
}
}]
{
"kind": 39000,
"pubkey": "<relay_identity_pubkey>",
"created_at": <timestamp>,
"tags": [
["method", "follows"],
["seed", "<seed_hex>"],
["depth", "3"]
],
"content": "{\"pubkeys_by_depth\":[[\"pubkey1\",\"pubkey2\"],[\"pubkey3\",\"pubkey4\"]],\"total_pubkeys\":4}",
"id": "<event_id>",
"sig": "<relay_signature>"
}
The content field contains a JSON object with depth arrays:
{
"pubkeys_by_depth": [
["<pubkey_depth_1>", "<pubkey_depth_1>", ...],
["<pubkey_depth_2>", "<pubkey_depth_2>", ...],
["<pubkey_depth_3>", "<pubkey_depth_3>", ...]
],
"total_pubkeys": 150
}
For event-based queries (mentions, thread), the structure is:
{
"events_by_depth": [
["<event_id_depth_1>", ...],
["<event_id_depth_2>", ...]
],
"total_events": 42
}
Key properties:
Alice follows Bob and Carol. Bob follows Dave. Carol follows Dave and Eve.
Request:
["REQ", "follow-net", {
"_graph": {
"method": "follows",
"seed": "<alice_pubkey>",
"depth": 2
}
}]
Response:
["EVENT", "follow-net", {
"kind": 39000,
"pubkey": "<relay_pubkey>",
"created_at": 1704067200,
"tags": [
["method", "follows"],
["seed", "<alice_pubkey>"],
["depth", "2"]
],
"content": "{\"pubkeys_by_depth\":[[\"<bob_pubkey>\",\"<carol_pubkey>\"],[\"<dave_pubkey>\",\"<eve_pubkey>\"]],\"total_pubkeys\":4}",
"sig": "<signature>"
}]
["EOSE", "follow-net"]
Interpretation:
When the REQ includes both _graph AND other filter fields (like kinds), the relay:
["REQ", "<sub>", {
"_graph": {
"method": "follows",
"seed": "<pubkey_hex>",
"depth": 2
},
"kinds": [0, 1]
}]
["EVENT", "<sub>", <kind-39000 graph result event>]
["EVENT", "<sub>", <kind-0 profile for depth-1 pubkey>]
["EVENT", "<sub>", <kind-1 note for depth-1 pubkey>]
... (all depth-1 events)
["EVENT", "<sub>", <kind-0 profile for depth-2 pubkey>]
["EVENT", "<sub>", <kind-1 note for depth-2 pubkey>]
... (all depth-2 events)
["EOSE", "<sub>"]
The graph result event (kind 39000) is sent first, allowing clients to know the complete graph structure before receiving individual events.
Note: Reference aggregation is planned for a future implementation phase. The following describes the intended behavior.
When inbound_refs or outbound_refs are specified, the response will include aggregated reference data sorted by count descending (most referenced first).
["REQ", "popular-posts", {
"_graph": {
"method": "follows",
"seed": "<pubkey_hex>",
"depth": 1,
"inbound_refs": [
{"kinds": [7], "from_depth": 1}
]
}
}]
["EVENT", "popular-posts", <kind-39000 graph result with ref summaries>]
["EVENT", "popular-posts", <aggregated ref event with 523 reactions>]
["EVENT", "popular-posts", <aggregated ref event with 312 reactions>]
...
["EVENT", "popular-posts", <aggregated ref event with 1 reaction>]
["EOSE", "popular-posts"]
Used for mentions queries. Contains events that mention the seed pubkey:
{
"kind": 39001,
"pubkey": "<relay_pubkey>",
"created_at": <timestamp>,
"tags": [
["method", "mentions"],
["seed", "<seed_pubkey_hex>"],
["depth", "1"]
],
"content": "{\"events_by_depth\":[[\"<event_id_1>\",\"<event_id_2>\",...]],\"total_events\":42}",
"sig": "<signature>"
}
Used for thread queries. Contains events in a reply thread:
{
"kind": 39002,
"pubkey": "<relay_pubkey>",
"created_at": <timestamp>,
"tags": [
["method", "thread"],
["seed", "<seed_event_id_hex>"],
["depth", "10"]
],
"content": "{\"events_by_depth\":[[\"<reply_id_1>\",...],[\"<reply_id_2>\",...]],\"total_events\":156}",
"sig": "<signature>"
}
When inbound_refs or outbound_refs are specified, the response includes aggregated reference data sorted by count descending. This feature is planned for a future implementation phase.
Get Alice's 2-hop follow network as a single signed event:
["REQ", "follow-network", {
"_graph": {
"method": "follows",
"seed": "abc123...def456",
"depth": 2
}
}]
Response:
["EVENT", "follow-network", {
"kind": 39000,
"pubkey": "<relay_pubkey>",
"tags": [
["method", "follows"],
["seed", "abc123...def456"],
["depth", "2"]
],
"content": "{\"pubkeys_by_depth\":[[\"pub1\",\"pub2\",...150 pubkeys],[\"pub151\",\"pub152\",...3420 pubkeys]],\"total_pubkeys\":3570}",
"sig": "<signature>"
}]
["EOSE", "follow-network"]
The content JSON object contains:
pubkeys_by_depth[0]: 150 pubkeys (depth 1 - direct follows)pubkeys_by_depth[1]: 3420 pubkeys (depth 2 - friends-of-friends, excluding depth 1)total_pubkeys: 3570 (total unique pubkeys discovered)["REQ", "follow-profiles", {
"_graph": {
"method": "follows",
"seed": "abc123...def456",
"depth": 2
},
"kinds": [0]
}]
Response:
["EVENT", "follow-profiles", <kind-39000 graph result>]
["EVENT", "follow-profiles", <kind-0 for depth-1 follow>]
... (150 depth-1 profiles)
["EVENT", "follow-profiles", <kind-0 for depth-2 follow>]
... (3420 depth-2 profiles)
["EOSE", "follow-profiles"]
Find reactions to posts by Alice's follows, sorted by popularity:
["REQ", "popular-posts", {
"_graph": {
"method": "follows",
"seed": "abc123...def456",
"depth": 1,
"inbound_refs": [
{"kinds": [7], "from_depth": 1}
]
}
}]
Response: Most-reacted posts first, down to posts with only 1 reaction.
Fetch a complete reply thread:
["REQ", "thread", {
"_graph": {
"method": "thread",
"seed": "root_event_id_hex",
"depth": 10,
"inbound_refs": [
{"kinds": [1], "from_depth": 0}
]
}
}]
Find pubkeys that follow Alice:
["REQ", "my-followers", {
"_graph": {
"method": "followers",
"seed": "alice_pubkey_hex",
"depth": 1
}
}]
Response: Single kind-39000 event with follower pubkeys in content.
Find posts with both reactions and reposts:
["REQ", "engaged-posts", {
"_graph": {
"method": "follows",
"seed": "abc123...def456",
"depth": 1,
"inbound_refs": [
{"kinds": [7], "from_depth": 1},
{"kinds": [6], "from_depth": 1}
]
}
}]
This returns only posts that have both kind-7 reactions AND kind-6 reposts.
Find posts with either reactions or reposts:
["REQ", "any-engagement", {
"_graph": {
"method": "follows",
"seed": "abc123...def456",
"depth": 1,
"inbound_refs": [
{"kinds": [6, 7], "from_depth": 1}
]
}
}]
Graph result events are signed by the relay's identity key. Clients should:
content JSON to extract depth-organized resultsBecause graph results are standard signed events, clients can:
method, seed, and depth tags to identify equivalent queriescreated_at timestamps to determine freshnessThe relay is asserting "this is what the graph looks like from my perspective." Clients may want to:
Efficient implementation requires bidirectional graph indexes:
Pubkey Graph:
p tag references)Event Graph:
e tag references)Both indexes should include:
inbound_refs/outbound_refs specified, scan reference indexes_graph field per NIP-01Relays supporting this NIP should advertise it:
{
"supported_nips": [1, "XX"],
"limitation": {
"graph_query_max_depth": 16
}
}