Reply Conventions (e and p tags)
NIP-10 standardizes how to construct threaded conversations on Nostr using e tags (event references) and p tags (pubkey mentions), enabling proper reply chains and notifications.
NIP-10: Reply Conventions
Status: Final Authors: fiatjaf, arcbtc Category: Social Features
Overview
NIP-10 defines conventions for using e and p tags in text notes (kind 1 events) to create threaded conversations, enable proper replies, and notify mentioned users.
Core Purpose:
- ✅ Create reply chains (threaded conversations)
- ✅ Reference root posts (original post in a thread)
- ✅ Tag mentioned users (for notifications)
- ✅ Enable conversation navigation (find replies, find root)
Without NIP-10, Nostr would be a flat timeline. With NIP-10, clients can build Twitter-like threaded conversations.
Why Reply Conventions Matter
The Problem: Flat Events
Without conventions, events are isolated:
Event A: "Hello Nostr!"
Event B: "Nice to meet you!" (reply to A? Who knows!)
Event C: "Agreed!" (reply to B? A? Unknown!)
The Solution: Structured Tags
With NIP-10 conventions:
Event A: { "content": "Hello Nostr!", "tags": [] }
Event B: {
"content": "Nice to meet you!",
"tags": [
["e", "event_A_id", "", "root"],
["p", "event_A_author"]
]
}
Event C: {
"content": "Agreed!",
"tags": [
["e", "event_A_id", "", "root"],
["e", "event_B_id", "", "reply"],
["p", "event_A_author"],
["p", "event_B_author"]
]
}
Now clients know:
- Event B replies to Event A
- Event C replies to Event B, in a thread started by Event A
- Authors of A and B should be notified
Tag Types
E Tags (Event References)
The e tag references other events:
["e", <event-id>, <relay-url>, <marker>]
Fields:
- event-id (required): ID of referenced event
- relay-url (optional): Hint where to find this event
- marker (optional): Relationship type (
root,reply,mention)
Markers:
root: The original post that started this threadreply: The event we’re directly replying tomention: An event mentioned but not replied to
P Tags (Pubkey Mentions)
The p tag mentions users:
["p", <pubkey>, <relay-url>]
Fields:
- pubkey (required): Public key of mentioned user
- relay-url (optional): Hint where to find this user
Purpose:
- Notify mentioned users
- Attribute quotes/references
- Build mention graphs
Reply Patterns
Pattern 1: Direct Reply (No Thread)
Replying to a standalone post:
// Original post
const originalPost = {
id: "event_id_A",
kind: 1,
content: "What's everyone working on?",
tags: [],
...
};
// Your reply
const reply = {
kind: 1,
content: "Building a Nostr client!",
tags: [
["e", "event_id_A", "wss://relay.damus.io", "root"],
["p", "original_post_author_pubkey"]
],
...
};
E tag: References original post as root
P tag: Mentions original author (for notifications)
Pattern 2: Reply in Existing Thread
Replying to a post that’s already a reply:
// Original thread structure:
// Event A (root)
// └─ Event B (reply to A)
// └─ Your reply (reply to B)
const yourReply = {
kind: 1,
content: "Great idea!",
tags: [
["e", "event_A_id", "", "root"], // Original post
["e", "event_B_id", "", "reply"], // Direct parent
["p", "event_A_author"], // Root author
["p", "event_B_author"] // Parent author
],
...
};
Two E tags:
- First: Root event (start of thread)
- Second: Direct parent (what you’re replying to)
Two P tags:
- Both authors get notified
Pattern 3: Mentioning Without Replying
Mentioning an event or user without replying:
const post = {
kind: 1,
content: "Check out this awesome post: nostr:note1abc...",
tags: [
["e", "mentioned_event_id", "", "mention"],
["p", "mentioned_user_pubkey"]
],
...
};
Marker mention: Indicates reference, not reply
Deprecated Positional Convention
Legacy behavior (still supported for compatibility):
Before markers were added, tag position indicated meaning:
- First e tag: Root event
- Last e tag: Direct reply parent
- Middle e tags: Mentions
Example (deprecated):
{
"tags": [
["e", "root_event_id"], // First = root
["e", "mentioned_event_id"], // Middle = mention
["e", "parent_event_id"] // Last = reply
]
}
Modern approach: Use explicit markers instead of relying on position.
Building Threaded Conversations
Thread Structure Visualization
Root Event (A)
├─ Reply (B)
│ ├─ Reply (C)
│ │ └─ Reply (D)
│ └─ Reply (E)
└─ Reply (F)
└─ Reply (G)
Event Tags:
A: [] (no tags, it's the root)
B: [["e", A, "", "root"]]
C: [["e", A, "", "root"], ["e", B, "", "reply"]]
D: [["e", A, "", "root"], ["e", C, "", "reply"]]
E: [["e", A, "", "root"], ["e", B, "", "reply"]]
F: [["e", A, "", "root"]]
G: [["e", A, "", "root"], ["e", F, "", "reply"]]
Pattern: Always include root + direct parent (if not root).
Client Implementation
Creating a Reply
import { getPublicKey, signEvent } from 'nostr-tools';
function createReply(parentEvent, replyText, privateKey) {
const pubkey = getPublicKey(privateKey);
// Determine root event
const rootTag = parentEvent.tags.find(tag =>
tag[0] === "e" && tag[3] === "root"
);
const rootEventId = rootTag ? rootTag[1] : parentEvent.id;
// Build tags
const tags = [];
// Add root event (if not replying to root itself)
if (rootEventId !== parentEvent.id) {
tags.push(["e", rootEventId, "", "root"]);
}
// Add parent event
tags.push(["e", parentEvent.id, "", "reply"]);
// Add p tags for all authors in thread
const authors = new Set();
// Add root author
if (rootTag) {
const rootEvent = findEvent(rootEventId);
if (rootEvent) authors.add(rootEvent.pubkey);
}
// Add parent author
authors.add(parentEvent.pubkey);
// Add p tags (skip self)
authors.forEach(author => {
if (author !== pubkey) {
tags.push(["p", author]);
}
});
// Create reply event
const replyEvent = {
kind: 1,
created_at: Math.floor(Date.now() / 1000),
content: replyText,
tags: tags,
pubkey: pubkey
};
return signEvent(replyEvent, privateKey);
}
// Usage
const reply = createReply(
parentEvent,
"Great point! I agree.",
privateKey
);
relay.publish(reply);
Fetching a Thread
async function fetchThread(rootEventId, relays) {
const filters = {
kinds: [1],
"#e": [rootEventId] // All events referencing this root
};
// Fetch from all relays
const events = await Promise.all(
relays.map(relay => relay.list([filters]))
);
// Flatten and deduplicate
const allEvents = [...new Set(events.flat())];
// Build thread tree
const thread = buildThreadTree(allEvents, rootEventId);
return thread;
}
function buildThreadTree(events, rootId) {
const tree = { id: rootId, replies: [] };
const lookup = { [rootId]: tree };
events.forEach(event => {
// Find parent (last e tag with "reply" marker, or last e tag)
const replyTag = event.tags.findLast(tag =>
tag[0] === "e" && (tag[3] === "reply" || !tag[3])
);
if (replyTag) {
const parentId = replyTag[1];
const parent = lookup[parentId] || { id: parentId, replies: [] };
const node = { id: event.id, event: event, replies: [] };
parent.replies.push(node);
lookup[event.id] = node;
}
});
return tree;
}
Rendering a Thread
function ThreadView({ thread }) {
return (
<div className="thread">
<Post event={thread.event} />
{thread.replies.length > 0 && (
<div className="replies">
{thread.replies.map(reply => (
<ThreadView key={reply.id} thread={reply} />
))}
</div>
)}
</div>
);
}
// Recursive rendering creates nested thread UI
Notification System
Detecting Mentions
Clients should notify users when mentioned:
function shouldNotify(event, userPubkey) {
// Check if user is mentioned in p tags
return event.tags.some(tag =>
tag[0] === "p" && tag[1] === userPubkey
);
}
// Subscribe to mentions
const filters = {
kinds: [1],
"#p": [myPubkey] // Events mentioning me
};
relay.sub([filters], {
onEvent: (event) => {
if (shouldNotify(event, myPubkey)) {
showNotification(event);
}
}
});
Notification Types
- Direct Reply: Someone replied to your post
- Thread Reply: Someone replied in your thread
- Mention: Someone mentioned you in a post
- Quote: Someone quoted your post
function getNotificationType(event, userPubkey) {
const eTags = event.tags.filter(tag => tag[0] === "e");
const pTags = event.tags.filter(tag => tag[0] === "p" && tag[1] === userPubkey);
// Find if they replied to your event
const repliedToMe = eTags.some(tag => {
const referencedEvent = findEvent(tag[1]);
return referencedEvent?.pubkey === userPubkey;
});
if (repliedToMe) {
const isDirectReply = eTags.find(tag => tag[3] === "reply");
if (isDirectReply) {
return "reply";
}
return "thread_reply";
}
if (pTags.length > 0) {
return "mention";
}
return null;
}
Best Practices
For Users
- Reply in threads: Keep conversations organized
- Tag relevant people: Notify those you’re talking to/about
- Quote properly: Use e+p tags when referencing others
- Check context: Read the thread before replying
For Developers (Clients)
- Implement threading: Show conversations hierarchically
- Highlight mentions: Make it obvious when user is mentioned
- Show context: Display parent post when showing replies
- Optimize fetches: Use
#efilters for thread queries - Handle orphans: What to do when parent event is missing
- Support markers: Use root/reply/mention markers
- Deduplicate authors: Don’t tag same person multiple times
For Developers (Advanced)
- Cache thread structures: Avoid re-fetching entire threads
- Lazy load deep threads: Fetch replies on-demand
- Highlight new replies: Show unread indicators
- Mute threads: Allow users to unfollow noisy threads
- Pin important: Let users pin important conversations
Edge Cases
Missing Root Event
If root event is deleted or unavailable:
function handleMissingRoot(event) {
// Option 1: Show as standalone (no thread context)
// Option 2: Show with "[root unavailable]" placeholder
// Option 3: Try to reconstruct from replies
}
Circular References
Prevent infinite loops from malformed tags:
function detectCircularReferences(event, visited = new Set()) {
if (visited.has(event.id)) {
return true; // Circular reference detected
}
visited.add(event.id);
event.tags.forEach(tag => {
if (tag[0] === "e") {
const referencedEvent = findEvent(tag[1]);
if (referencedEvent) {
detectCircularReferences(referencedEvent, visited);
}
}
});
return false;
}
Multiple Roots
Event incorrectly tags multiple roots:
const rootTags = event.tags.filter(tag =>
tag[0] === "e" && tag[3] === "root"
);
if (rootTags.length > 1) {
// Use first root tag
const rootId = rootTags[0][1];
}
Security Considerations
Tag Bombing
Attacker mentions thousands of users:
{
"tags": [
["p", "user1"], ["p", "user2"], ["p", "user3"], ... (1000 users)
]
}
Mitigation:
- Limit displayed mentions
- Rate-limit notifications
- Flag suspicious events
Fake Thread Roots
Attacker claims reply to famous post:
{
"tags": [
["e", "famous_event_id", "", "root"]
]
}
Mitigation:
- Verify thread continuity
- Show warning for suspicious threads
- Check event timestamps
Advanced Patterns
Quote Posts
Quoting another post (like Twitter quote tweets):
const quote = {
kind: 1,
content: "This is so true!\n\nnostr:note1abc...",
tags: [
["e", "quoted_event_id", "", "mention"],
["p", "quoted_author_pubkey"]
],
...
};
Marker mention: Indicates quote, not reply.
Multi-Reply
Replying to multiple posts:
const multiReply = {
kind: 1,
content: "Responding to both of you...",
tags: [
["e", "event_A_id", "", "reply"],
["e", "event_B_id", "", "reply"],
["p", "author_A"],
["p", "author_B"]
],
...
};
Multiple reply markers: Reply to several posts at once.
Client Support
Full Threading Support
- Damus - Beautiful threaded conversations
- Primal - Fast thread loading with caching
- Amethyst - Comprehensive threading with notifications
- Snort - Clean thread UI
- Iris - Nested replies with context
- Nostrudel - Advanced thread navigation
Notification Support
- All major clients: Notify on mentions and replies
- Filtering: Most support notification preferences
- Threading: See entire context from notification
Check our Client Directory for details.
Common Questions
Why both root and reply tags?
Root: Helps clients fetch entire thread Reply: Shows immediate parent for context
Both together enable efficient thread construction.
What if parent is deleted?
Clients typically:
- Show reply anyway (orphaned)
- Display “[parent deleted]” placeholder
- Try to find thread via root tag
Can I reply to multiple events?
Yes, use multiple e tags with reply marker. Not all clients handle this well yet.
Do I need to tag everyone in the thread?
No, only tag:
- Root author (if different from parent)
- Direct parent author
- Anyone explicitly mentioned in your reply
Related NIPs
- NIP-01 - Basic protocol (event structure, tags)
- NIP-27 - Text note references (nostr:note1… mentions)
Technical Specification
For the complete technical specification, see NIP-10 on GitHub.
Summary
NIP-10 enables threaded conversations on Nostr:
✅ E tags reference events (root, reply, mention) ✅ P tags mention users (notifications) ✅ Markers clarify relationships (root/reply/mention) ✅ Threading creates organized conversations
Tag Structure:
- Root event:
["e", root_id, "", "root"] - Parent event:
["e", parent_id, "", "reply"] - Mentioned users:
["p", pubkey]
Best practice: Always tag root (if exists) + direct parent + all relevant authors.
Next Steps:
- Learn about reactions in NIP-25
- Explore text mentions in NIP-27
- Understand event deletion in NIP-09
- Browse all NIPs in our reference
Last updated: January 2024 Official specification: GitHub
Client Support
This NIP is supported by the following clients: