NIP-10

Reply Conventions (e and p tags)

final social features

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.

Author
fiatjaf, arcbtc
Last Updated
15 January 2024
Official Spec
View on GitHub →

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 thread
  • reply: The event we’re directly replying to
  • mention: 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

  1. Direct Reply: Someone replied to your post
  2. Thread Reply: Someone replied in your thread
  3. Mention: Someone mentioned you in a post
  4. 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

  1. Reply in threads: Keep conversations organized
  2. Tag relevant people: Notify those you’re talking to/about
  3. Quote properly: Use e+p tags when referencing others
  4. Check context: Read the thread before replying

For Developers (Clients)

  1. Implement threading: Show conversations hierarchically
  2. Highlight mentions: Make it obvious when user is mentioned
  3. Show context: Display parent post when showing replies
  4. Optimize fetches: Use #e filters for thread queries
  5. Handle orphans: What to do when parent event is missing
  6. Support markers: Use root/reply/mention markers
  7. Deduplicate authors: Don’t tag same person multiple times

For Developers (Advanced)

  1. Cache thread structures: Avoid re-fetching entire threads
  2. Lazy load deep threads: Fetch replies on-demand
  3. Highlight new replies: Show unread indicators
  4. Mute threads: Allow users to unfollow noisy threads
  5. 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

  • 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:


Last updated: January 2024 Official specification: GitHub

Client Support

This NIP is supported by the following clients:

damus primal amethyst snort iris coracle nostrudel satellite nos nostur plebstr current
View all clients →

Related NIPs

NIP-01 NIP-27
← Browse All NIPs