{"id":"ba5a85def3e11e2ce82a8deb005c306d6798f1768a107027260adcc333b3e43b","pubkey":"da19f1cd34beca44be74da4b306d9d1dd86b6343cef94ce22c49c6f59816e5bd","created_at":1774618301,"kind":30817,"tags":[["title","NIP-CHANNELS: Context-Scoped Messaging Primitives"],["k","20502"],["k","30565"],["d","nip-channels"]],"content":"NIP-CHANNELS\n============\n\nMessage Status & Typing Indicators\n-------------------------------------\n\n`draft` `optional`\n\nTwo event kinds for message status tracking and real-time typing indicators on Nostr, designed to complement NIP-17 private direct messages with context-scoping and delivery state.\n\n> **Standalone.** This NIP works independently on any Nostr application.\n\n## Motivation\n\nNIP-17 handles encrypted message delivery between participants via gift wrap. It solves the hard problem of private, metadata-protected messaging. However, two coordination primitives are missing from the messaging stack:\n\n1. **Delivery and read state.** Senders have no way to know whether a message was delivered or read. In transactional contexts (marketplace orders, freelance projects, service coordination), this uncertainty creates friction and duplicated messages.\n2. **Real-time typing signals.** Typing indicators are a standard UX expectation in messaging. Without them, participants cannot tell whether the other party is actively engaged or has walked away.\n\nFor context-scoped messaging (messages tied to a specific order, project, or task), NIP-17 messages can carry a `context_id` tag inside the sealed rumour to scope conversations. This NIP provides the status and presence primitives that complete the picture.\n\nUse cases:\n\n- **Marketplace:** Buyer and seller coordinate delivery details for an order.\n- **Freelance:** Client and contractor discuss project requirements.\n- **Events:** Organiser and vendor coordinate setup logistics.\n- **Local services:** Requester and provider share access codes, arrival updates.\n\n## Relationship to Existing NIPs\n\n- **[NIP-17](https://github.com/nostr-protocol/nips/blob/master/17.md) (Private Direct Messages):** NIP-17 handles encrypted message delivery via gift wrap (NIP-59). For context-scoped messaging, add a `context_id` tag to the inner rumour event. This NIP provides the status and UX primitives that NIP-17 lacks. See [Context-Scoped Messaging with NIP-17](#context-scoped-messaging-with-nip-17) for concrete examples.\n- **[NIP-29](https://github.com/nostr-protocol/nips/blob/master/29.md) (Relay-based Groups):** Relay-managed groups are persistent, relay-hosted communities with membership controls. Context-scoped channels are ephemeral, participant-specific, and tied to a shared context (transaction, project, order). They auto-expire when the context completes.\n- **[NIP-44](https://github.com/nostr-protocol/nips/blob/master/44.md) (Encryption):** Message Status events use NIP-44 encryption for private delivery state.\n- **[NIP-59](https://github.com/nostr-protocol/nips/blob/master/59.md) (Gift Wrap):** NIP-17 uses gift wrap for sender metadata protection. Status events MAY also be gift-wrapped for maximum privacy.\n- **[NIP-38](https://github.com/nostr-protocol/nips/blob/master/38.md) (User Statuses):** User statuses (kind 30315) are long-lived, public, addressable events for general status updates (e.g. \"listening to music\"). Typing indicators are ephemeral (kind 20502), context-scoped, sent to specific recipients, and expire within seconds. Different event type, lifecycle, and audience.\n\n## Kinds\n\n| kind  | description      |\n| ----- | ---------------- |\n| 30565 | Message Status   |\n| 20502 | Typing Indicator |\n\n---\n\n## Message Status (`kind:30565`)\n\nRead receipts and delivery confirmation. Addressable; the latest status for a given context replaces previous status events.\n\n```json\n{\n    \"kind\": 30565,\n    \"pubkey\": \"<reader-hex-pubkey>\",\n    \"created_at\": 1698766100,\n    \"tags\": [\n        [\"d\", \"order_abc123:status:<reader-pubkey>:<counterparty-pubkey>\"],\n        [\"context_id\", \"order_abc123\"],\n        [\"last_read\", \"<event-id-of-last-read-message>\"],\n        [\"p\", \"<counterparty-pubkey>\"],\n        [\"expiration\", \"1701358100\"],\n        [\"alt\", \"Message read status for context order_abc123\"]\n    ],\n    \"content\": \"<NIP-44 encrypted: {\\\"unread_count\\\": 0, \\\"last_read_at\\\": 1698766095}>\",\n    \"id\": \"<32-byte-hex>\",\n    \"sig\": \"<64-byte-hex>\"\n}\n```\n\nTags:\n\n* `d` (REQUIRED): Unique status identifier. Format: `<context_id>:status:<reader-pubkey>:<counterparty-pubkey>`. The counterparty pubkey discriminates between pairwise streams in the same context. Without it, a participant in multiple streams would overwrite their read position when reading messages from a different stream.\n* `context_id` (REQUIRED): Shared context identifier.\n* `last_read` (REQUIRED): Event ID of the most recently read message.\n* `p` (REQUIRED): Counterparty who should see this receipt.\n* `expiration` (RECOMMENDED): NIP-40 expiration timestamp. Implementations SHOULD set `expiration` to context completion plus 30 days.\n\nContent:\n\nThe `content` field is NIP-44 encrypted to the counterparty. It MAY contain a JSON object with additional status metadata:\n\n| Field          | Type   | Description                                   |\n| -------------- | ------ | --------------------------------------------- |\n| `unread_count` | number | Number of unread messages (0 = all read)      |\n| `last_read_at` | number | Unix timestamp of when the message was read   |\n\nClients that do not need rich status metadata MAY leave the content empty.\n\n### REQ Filters\n\nSubscribe to status updates for a context:\n\n```json\n[\"REQ\", \"sub-status\", {\n    \"kinds\": [30565],\n    \"#p\": [\"<my-pubkey>\"]\n}]\n```\n\nFilter by `kinds` and `#p` at the relay, then post-filter by `context_id` tag client-side. NIP-01 defines relay-side filters for single-letter tag names only.\n\n---\n\n## Typing Indicator (`kind:20502`)\n\nEphemeral real-time typing signal. Relays MUST NOT persist these events.\n\n```json\n{\n    \"kind\": 20502,\n    \"pubkey\": \"<typer-hex-pubkey>\",\n    \"created_at\": 1698766200,\n    \"tags\": [\n        [\"context_id\", \"order_abc123\"],\n        [\"p\", \"<recipient-pubkey>\"],\n        [\"expiration\", \"1698766205\"],\n        [\"alt\", \"Typing indicator\"]\n    ],\n    \"content\": \"\",\n    \"id\": \"<32-byte-hex>\",\n    \"sig\": \"<64-byte-hex>\"\n}\n```\n\nTags:\n\n* `context_id` (REQUIRED): Shared context identifier.\n* `p` (REQUIRED): Recipient(s) of the typing signal.\n* `expiration` (REQUIRED): Short NIP-40 expiration (5-10 seconds). Safety net; if the sender stops typing, the indicator expires automatically.\n\nClients SHOULD send typing indicators at most once every 3 seconds to avoid relay spam. When a user stops typing, the client SHOULD NOT send a \"stopped typing\" event; the expiration handles cleanup.\n\n### REQ Filters\n\nSubscribe to typing indicators for a context:\n\n```json\n[\"REQ\", \"sub-typing\", {\n    \"kinds\": [20502],\n    \"#p\": [\"<my-pubkey>\"]\n}]\n```\n\nFilter by `kinds` and `#p` at the relay, then post-filter by `context_id` tag client-side. NIP-01 defines relay-side filters for single-letter tag names only.\n\n---\n\n## Context-Scoped Messaging with NIP-17\n\nNIP-17 handles message content delivery. This section shows how to compose NIP-17 messages with a `context_id` tag for context-scoped channels, and how these combine with the status and typing primitives defined above.\n\n### Adding `context_id` to NIP-17 Messages\n\nThe `context_id` tag is placed inside the **sealed rumour** (the kind 14 event that NIP-17 wraps). Relays and observers cannot see it; only the recipient can decrypt it.\n\n**Rumour (kind 14, unsigned inner event):**\n\n```json\n{\n    \"kind\": 14,\n    \"pubkey\": \"<sender-hex-pubkey>\",\n    \"created_at\": 1698766000,\n    \"tags\": [\n        [\"p\", \"<recipient-pubkey>\"],\n        [\"context_id\", \"order_abc123\"],\n        [\"message_type\", \"text\"],\n        [\"subject\", \"Order #ABC123\"]\n    ],\n    \"content\": \"I'm at the back entrance, look for the red door.\"\n}\n```\n\nThis rumour is then sealed (kind 13) and gift-wrapped (kind 1059) per the NIP-17 specification. The `context_id` and `message_type` tags are only visible after decryption.\n\n### Message Types (Application-Level Convention)\n\n> **Note:** The `message_type` and `template` tags extend NIP-17 rumours at the application layer. They are not part of the NIP-17 specification. Applications that do not need message classification MAY omit them entirely.\n\nThe `message_type` tag classifies the message content. Clients SHOULD support the following types:\n\n| Type              | Description                                        | Example content                                    |\n| ----------------- | -------------------------------------------------- | -------------------------------------------------- |\n| `text`            | Free-text message                                  | `\"Running 5 minutes late\"`                         |\n| `system`          | System-generated notification                      | `\"Order dispatched\"`                               |\n| `location_share`  | Shared location coordinates                        | `\"{\\\"lat\\\": 51.5074, \\\"lon\\\": -0.1278}\"`          |\n| `media`           | Photo or file reference                            | `\"{\\\"url\\\": \\\"https://...\\\", \\\"mime\\\": \\\"image/jpeg\\\"}\"` |\n| `payment_update`  | Payment status notification                        | `\"{\\\"status\\\": \\\"paid\\\", \\\"amount\\\": 1500, \\\"currency\\\": \\\"GBP\\\"}\"` |\n\nClients that do not recognise a `message_type` SHOULD fall back to rendering the content as plain text.\n\n### Structured Message Templates (Application-Level Convention)\n\nWhen `message_type` is `system`, the `content` field MAY use structured JSON for machine-parseable notifications:\n\n```json\n{\n    \"kind\": 14,\n    \"pubkey\": \"<sender-hex-pubkey>\",\n    \"created_at\": 1698766300,\n    \"tags\": [\n        [\"p\", \"<recipient-pubkey>\"],\n        [\"context_id\", \"order_abc123\"],\n        [\"message_type\", \"system\"],\n        [\"template\", \"eta_update\"]\n    ],\n    \"content\": \"{\\\"eta_minutes\\\": 5, \\\"message\\\": \\\"Driver is 5 minutes away\\\"}\"\n}\n```\n\nCommon templates:\n\n| Template               | Description                    | Example content                                  |\n| ---------------------- | ------------------------------ | ------------------------------------------------ |\n| `eta_update`           | Updated arrival estimate       | `{\"eta_minutes\": 5}`                             |\n| `running_late`         | Delay notification             | `{\"delay_minutes\": 10, \"reason\": \"traffic\"}`     |\n| `access_code`          | Entry code                     | `{\"code\": \"1234\", \"type\": \"gate\"}`               |\n| `arrival_notification` | Arrived at location            | `{\"location\": \"front door\"}`                     |\n| `status_update`        | General status notification    | `{\"status\": \"Order being prepared\"}`             |\n\nApplications MAY define additional templates. Clients that do not recognise a template SHOULD render the `message` field from the content JSON, or fall back to the raw content string.\n\n### Multi-Party Channels\n\nNIP-17 supports sending gift-wrapped copies to multiple recipients. For a three-party context (e.g. buyer, courier, and recipient), the sender creates a separate gift-wrapped copy for each participant:\n\n**Rumour (kind 14, same inner event for all recipients):**\n\n```json\n{\n    \"kind\": 14,\n    \"pubkey\": \"<courier-hex-pubkey>\",\n    \"created_at\": 1698766400,\n    \"tags\": [\n        [\"p\", \"<buyer-pubkey>\"],\n        [\"p\", \"<recipient-pubkey>\"],\n        [\"context_id\", \"delivery_xyz789\"],\n        [\"message_type\", \"text\"]\n    ],\n    \"content\": \"Package picked up, heading to the drop-off point now.\"\n}\n```\n\nThe sender gift-wraps this rumour separately for each `p`-tagged recipient. Each recipient receives their own gift-wrapped copy and can independently decrypt it.\n\nFor privacy-sensitive contexts where not all participants should see all messages, use **separate pairwise streams** with the same `context_id`:\n\n| Stream              | Participants        | Typical content                           |\n| ------------------- | ------------------- | ----------------------------------------- |\n| Buyer - Courier     | Customer, courier   | ETA updates, access codes                 |\n| Courier - Recipient | Courier, recipient  | \"I'm at the door\", \"Leave at reception\"   |\n\nEach stream uses the same `context_id` but different `p` tags. A buyer-to-courier message is NOT visible to the recipient unless explicitly `p`-tagged.\n\n### Channel Lifecycle\n\n1. **Creation:** A channel begins when the first NIP-17 message with a given `context_id` is sent. There is no explicit \"create channel\" event.\n2. **Active messaging:** Participants exchange NIP-17 messages with the shared `context_id`. Message Status (kind 30565) and Typing Indicator (kind 20502) events provide coordination signals.\n3. **Archival:** Messages SHOULD include an `expiration` tag (NIP-40) for automatic cleanup. Implementations SHOULD set expiration to context completion plus 30 days. Pre-context enquiries SHOULD use standard NIP-17 messages without a `context_id`.\n\n### Retrieving Channel History\n\nRecipients reconstruct channel history by filtering their decrypted NIP-17 messages (kind 14 rumours) by `context_id`. Since NIP-17 messages are gift-wrapped, there is no relay-side filter for `context_id`; clients MUST decrypt messages first, then filter locally.\n\n---\n\n## Protocol Flow\n\n\n![Messaging Flow](https://raw.githubusercontent.com/forgesworn/nip-drafts/main/images/channels-1.png)\n\n```mermaid\nsequenceDiagram\n    participant A as Alice (Buyer)\n    participant R as Relay\n    participant B as Bob (Seller)\n\n    Note over A,B: Context established (order_abc123)\n\n    A->>R: NIP-17 gift wrap (kind 1059)<br/>inner rumour has context_id: order_abc123\n    R->>B: Deliver gift wrap\n    B->>B: Decrypt rumour, extract context_id\n\n    B->>R: Message Status (kind 30565)<br/>last_read: <alice-message-id>\n    R->>A: Deliver status (Alice sees \"read\")\n\n    B->>R: Typing Indicator (kind 20502)<br/>context_id: order_abc123, expiration: +5s\n    R->>A: Deliver typing signal (Alice sees \"typing...\")\n\n    B->>R: NIP-17 gift wrap (kind 1059)<br/>inner rumour: reply message\n    R->>A: Deliver gift wrap\n\n    A->>R: Message Status (kind 30565)<br/>last_read: <bob-message-id>\n    R->>B: Deliver status (Bob sees \"read\")\n\n    Note over A,B: Context completes, messages expire via NIP-40\n```\n\n## Security Considerations\n\n* **End-to-end encryption.** Message content is delivered via NIP-17 gift wrap, providing NIP-44 encryption and sender metadata protection. Relays cannot read message content or determine the true sender.\n* **Scoped channels.** Messages are bound to a `context_id` inside the encrypted rumour. There is no cross-context message leakage. The `context_id` is not visible to relays.\n* **Automatic expiration.** NIP-40 `expiration` tags ensure messages and status events do not persist indefinitely. Implementations SHOULD set expiration to context completion plus 30 days for GDPR right-to-erasure compliance.\n* **No retroactive access.** Adding a new participant to `p` tags on future messages does not grant access to historical messages. Each gift-wrapped message is independently encrypted.\n* **Ephemeral indicators.** Typing indicators are ephemeral events; relays MUST NOT persist them.\n* **Status metadata leakage.** Message Status events (kind 30565) are addressable and visible to relays. The `context_id` and `p` tags reveal that two pubkeys are communicating within a context. For maximum privacy, implementations MAY gift-wrap status events as well.\n\n## Privacy\n\nMessage content and `context_id` values are encrypted inside NIP-17 gift wraps. Relays see only the outer gift-wrap metadata (random sender pubkey, recipient pubkey, kind 1059).\n\nMessage Status events (kind 30565) expose the `context_id` and participant pubkeys in plaintext tags. The `context_id` tag is client-side metadata; relays cannot filter on it because NIP-01 only indexes single-letter tag names. Clients filter by `kinds` and `#p` at the relay, then match `context_id` after retrieval. Applications with strict metadata privacy requirements SHOULD either:\n\n1. Use opaque `context_id` values (UUIDs or hashes) that do not reveal the nature of the context.\n2. Gift-wrap status events using NIP-59 for full metadata protection.\n\nTyping indicators expose `context_id` and participant pubkeys but are ephemeral and short-lived (5-10 second expiration).\n\n## Test Vectors\n\n### Message Status (kind 30565)\n\n```json\n{\n    \"kind\": 30565,\n    \"pubkey\": \"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2\",\n    \"created_at\": 1698766100,\n    \"tags\": [\n        [\"d\", \"order_abc123:status:a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2:f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5\"],\n        [\"context_id\", \"order_abc123\"],\n        [\"last_read\", \"aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344\"],\n        [\"p\", \"f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5\"],\n        [\"expiration\", \"1701358100\"],\n        [\"alt\", \"Message read status for context order_abc123\"]\n    ],\n    \"content\": \"\",\n    \"id\": \"...\",\n    \"sig\": \"...\"\n}\n```\n\n**Validation rules:**\n\n1. `kind` MUST be `30565`.\n2. `d` tag MUST follow the format `<context_id>:status:<reader-pubkey>:<counterparty-pubkey>`.\n3. `context_id` tag MUST be present and non-empty.\n4. `last_read` tag MUST contain a valid 32-byte hex event ID.\n5. At least one `p` tag MUST be present.\n\n### Typing Indicator (kind 20502)\n\n```json\n{\n    \"kind\": 20502,\n    \"pubkey\": \"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2\",\n    \"created_at\": 1698766200,\n    \"tags\": [\n        [\"context_id\", \"order_abc123\"],\n        [\"p\", \"f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5\"],\n        [\"expiration\", \"1698766205\"],\n        [\"alt\", \"Typing indicator\"]\n    ],\n    \"content\": \"\",\n    \"id\": \"...\",\n    \"sig\": \"...\"\n}\n```\n\n**Validation rules:**\n\n1. `kind` MUST be `20502`.\n2. `context_id` tag MUST be present and non-empty.\n3. At least one `p` tag MUST be present.\n4. `expiration` tag MUST be present with a value no more than 30 seconds in the future.\n5. `content` MUST be empty.\n\n---\n\n## Dependencies\n\n* [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md): Basic protocol flow, addressable events, ephemeral events\n* [NIP-17](https://github.com/nostr-protocol/nips/blob/master/17.md): Private direct messages (message content delivery)\n* [NIP-40](https://github.com/nostr-protocol/nips/blob/master/40.md): Expiration timestamps\n* [NIP-44](https://github.com/nostr-protocol/nips/blob/master/44.md): Versioned encrypted payloads\n* [NIP-59](https://github.com/nostr-protocol/nips/blob/master/59.md): Gift wrap (used by NIP-17 for sender metadata protection)\n\n## Reference Implementation\n\nNo public reference implementation exists yet. Implementors SHOULD refer to the kind definitions above.\n\nA minimal implementation requires:\n\n1. A NIP-17 compatible client for sending and receiving gift-wrapped messages with `context_id` tags.\n2. A NIP-44 encryption library for encrypting Message Status content to the counterparty.\n3. A Nostr client that supports addressable and ephemeral event publishing.\n4. Context management logic to scope messages and status events to a shared `context_id`.","sig":"9a7386a0b12ce8d168569ded3e51d417b17372fa6df5172cc03ed0a693b6c6eb478539999bd52decfd57beef9e46c07668c05c80f8144eb563c81bfc83f776e5"}