[← All NAPs](https://napplet.run/protocol/)

NAP-LISTS / PR #68

# Runtime-Mediated Nostr List Mutation

Open PR

This is an unmerged proposal. Its API and scope may change during discussion. Participate on the pull request before depending on it.

**Shipped implementation: experimental**

List mutation binding is tested; concurrent real-host mutation behavior is not established by binding tests.

Implementation proposal: [in-review ↗](https://github.com/napplet/naps/pull/68), checked 2026-10-31. This assessment does not imply that every amendment shown here is implemented. All capabilities remain subject to change.

[Maturity and opt-in policy →](https://napplet.run/docs/naps/maturity.html)

Full specification from [the fetched upstream revision ↗](https://github.com/napplet/naps/blob/7cf00cdb7bca6bb59dc5ef082e9fe8ac9278fa46/naps/NAP-LISTS.md). The document below is rendered from its original Markdown. [Follow the latest source on GitHub ↗](https://github.com/napplet/naps/pull/68/files).

## NAP-LISTS

### Runtime-Mediated Nostr List Mutation

`draft`

**NAP ID:** NAP-LISTS **Domain:** `lists` **Depends:**

-   `identity` — capability · required — list mutations use the current shell-user identity as event author.
-   `relay` — capability · required — reads and publishes NIP-51 and NIP-65 list events. **Web binding ([NIP-5D](https://github.com/nostr-protocol/nips/pull/2303)):** `window.napplet.lists`; domain presence signals availability.

### Description

NAP-LISTS lets napplets add and remove items from NIP-51 lists and NIP-65 relay lists without owning the dangerous read-modify-sign-publish path for replaceable and addressable events. The napplet names the list and items. The runtime owns current-event lookup, kind/type mapping, tag formatting, private item encryption, preservation of existing content, signing, and publishing.

This NAP does not expose a generic list event editor. It exposes safe mutations for supported NIP-51 and NIP-65 lists only.

### API Surface

| Operation | Parameters | Result | Wire |
| --- | --- | --- | --- |
| `supported` | none | list of `ListSupport` | `lists.supported` / `.result` |
| `add` | `list`, `items`, `options?` | `ListMutationResult` | `lists.add` / `.result` |
| `remove` | `list`, `items`, `options?` | `ListMutationResult` | `lists.remove` / `.result` |

#### Schemas

Enumerations:

| Name | Values |
| --- | --- |
| `ListItem.itemType` | `pubkey`, `event`, `address`, `hashtag`, `word`, `relay`, `emoji`, `server`, `url`, `group` |
| `ListItem.marker` | `read`, `write`, `read-write` |
| `ListItem.visibility` | `public`, `private` |

`ListRef`:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `kind` | `uint` | conditional | Direct NIP-51 or NIP-65 event kind. |
| `type` | `tstr` | conditional | Derived list type name. |
| `identifier` | `tstr` | no | NIP-51 `d` tag for addressable sets. |

`ListItem`:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `itemType` | `ListItem.itemType` | yes | Kind of list item to add or remove. |
| `value` | `tstr` | yes | Item value. |
| `relay` | `tstr` | no | Relay hint for item types that carry one. |
| `marker` | `ListItem.marker` | no | Relay read/write marker. |
| `label` | `tstr` | no | Human-readable label or list-specific tag field. |
| `visibility` | `ListItem.visibility` | no | Whether the item is public or private. |

`ListOptions`:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `create` | `bool` | no | Whether the runtime may create a missing list. |
| `title` | `tstr` | no | Title for a newly created list. |
| `description` | `tstr` | no | Description for a newly created list. |
| `image` | `tstr` | no | Image URL for a newly created list. |

`ListSupport`:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `kind` | `uint` | yes | Supported NIP-51 or NIP-65 event kind. |
| `type` | `tstr` | yes | Supported derived list type. |
| `addressable` | `bool` | yes | Whether the list requires an `identifier`. |
| `supportedItemTypes` | list of `tstr` | no | Item types accepted for this list. |
| `privateItems` | `bool` | no | Whether private items are supported. |

`ListMutationResult`:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `ok` | `bool` | yes | Whether the mutation succeeded. |
| `eventId` | `tstr` | no | Published list event ID. |
| `event` | Nostr event object | no | Signed list event produced by the runtime. |
| `added` | `uint` | no | Number of items added. |
| `removed` | `uint` | no | Number of items removed. |
| `skipped` | `uint` | no | Number of items skipped. |
| `error` | `tstr` | no | Machine-readable error code. |
| `reason` | `tstr` | no | Human-readable refusal or failure reason. |
| `supported` | list of `ListSupport` | no | Supported lists returned on unsupported requests. |

Exactly one of `ListRef.kind` or `ListRef.type` MUST be present. Addressable sets MUST include `identifier`; the runtime writes it as the `"d"` tag.

`type` is derived from the NIP-51 table name: lowercase, punctuation and whitespace collapsed to hyphens, trimmed. Examples: `Mute list` -> `mute-list`, `Pinned notes` -> `pinned-notes`, `Read/write relays` -> `read-write-relays`, `Follow sets` -> `follow-sets`, `Kind mute sets` -> `kind-mute-sets`. If a derived type maps to more than one NIP-51 kind, the runtime MUST reject the type-only request with `"ambiguous-list"` and include candidates in `supported`. The napplet can retry with `kind`.

NIP-65 relay list metadata is kind `10002` and type `relay-list-metadata`. Items MUST use `itemType: "relay"`. `marker: "read"` writes an `["r", url, "read"]` tag, `marker: "write"` writes `["r", url, "write"]`, and omitted or `"read-write"` writes `["r", url]`.

For `add`, omitted `visibility` means `"public"`.

### Operation Rules

| Operation | Rules |
| --- | --- |
| `supported` | Returns the NIP-51/NIP-65 list kinds/types this runtime supports by policy and implementation. |
| `add` | Loads the current list event, appends new items in chronological order, preserves unrelated tags/content, signs, publishes. With `create: true`, MAY create a missing list. |
| `remove` | Loads the current list event, removes matching items, preserves unrelated tags/content, signs, publishes. If `visibility` is omitted, removes matching public and private items. |

For NIP-65, removing `marker: "read"` from an unmarked relay tag rewrites it to `"write"`; removing `"write"` rewrites it to `"read"`. Removing omitted or `"read-write"` removes all matching relay URL tags.

The runtime maps `ListItem.itemType` to the correct list tag for the selected list. Napplets SHOULD NOT need to know whether the item is encoded as `"p"`, `"e"`, `"a"`, `"t"`, `"relay"`, `"r"`, or another expected tag.

### NIP Mapping

| Concept | NIP tie |
| --- | --- |
| `kind` | Direct NIP-51 or NIP-65 event kind. |
| `type` | Derived from the NIP-51 table `name` cell, or fixed by this NAP for NIP-65. |
| `identifier` | NIP-51 `"d"` tag for addressable sets. |
| `visibility: "public"` | Item is written to event `tags`. |
| `visibility: "private"` | Item is written to encrypted `.content` using NIP-44 private items. |
| `relay-list-metadata` | NIP-65 kind `10002`, `r` tags, optional `read`/`write` marker. |

### Wire Protocol

`lists.*` messages use [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) wire format: `{ "type": "domain.action", ...payload }`.

| Type | Direction | Payload fields |
| --- | --- | --- |
| `lists.supported` | napplet -> runtime | `id` |
| `lists.supported.result` | runtime -> napplet | `id`, `lists?`, `error?` |
| `lists.add` | napplet -> runtime | `id`, `list`, `items`, `options?` |
| `lists.add.result` | runtime -> napplet | `id`, `ok`, `eventId?`, `event?`, `added?`, `skipped?`, `error?`, `reason?`, `supported?` |
| `lists.remove` | napplet -> runtime | `id`, `list`, `items`, `options?` |
| `lists.remove.result` | runtime -> napplet | `id`, `ok`, `eventId?`, `event?`, `removed?`, `skipped?`, `error?`, `reason?`, `supported?` |

### Unsupported Lists

When a runtime does not support the requested list kind/type, it MUST return:

```json
{ "ok": false, "error": "unsupported-list", "reason": "...", "supported": [] }
```

`supported` SHOULD include the runtime's supported list set unless policy forbids revealing it. This is the primary sad path for implementation choice, NIP-51 or NIP-65 drift, deprecated list formats, and unknown type names.

### Error Handling

Common errors: `"unsupported-list"`, `"unsupported-item"`, `"invalid-list-ref"`, `"ambiguous-list"`, `"missing-identifier"`, `"invalid-item"`, `"not-signed-in"`, `"list-not-found"`, `"list-unavailable"`, `"private-items-unsupported"`, `"decrypt-failed"`, `"user-denied"`, `"publish-failed"`, `"unsupported"`.

The runtime MUST reject instead of publishing when it cannot read, decrypt, or preserve existing list state without likely data loss.

### Runtime Behavior

-   MUST perform the full read-modify-sign-publish flow.
-   MUST preserve unrelated tags, metadata tags, and private items.
-   MUST append added items after existing items.
-   MUST deduplicate exact item matches.
-   MUST validate item types against the selected list's expected NIP-51 tags.
-   MUST validate NIP-65 relay list items as relay URLs plus optional read/write marker.
-   MUST reject addressable sets without `identifier`.
-   MUST use NIP-44 for newly written private items.
-   MUST NOT require napplets to provide raw NIP-51 tags.
-   MAY prompt, deny, redact returned events, or limit supported lists by policy.

### Security Considerations

-   NIP-51 and NIP-65 mutations can overwrite replaceable/addressable events. Runtime preservation is mandatory because napplet mistakes can lose user data.
-   Private items are sensitive. Napplets supply intent; the runtime owns encryption, decryption, and failure handling.
-   Items, relay hints, URLs, and labels are untrusted input.
-   Returning the signed event is optional; success is `ok` plus `eventId`.

### References

-   [NIP-51](https://github.com/nostr-protocol/nips/blob/master/51.md) — Lists
-   [NIP-65](https://github.com/nostr-protocol/nips/blob/master/65.md) — Relay List Metadata
-   [NIP-44](https://github.com/nostr-protocol/nips/blob/master/44.md) — Versioned Encryption

### Implementations

-   None yet.

### Changelog

-   `2c244b5` - Introduced NAP-LISTS for runtime-mediated NIP-51 list mutations.
    
-   `2efc16c` - Extended NAP-LISTS to cover NIP-65 relay-list mutations.
    
-   `70d5ae4` - Adopted injected-domain availability and linked the current upstream web binding.
    

[Join the discussion · PR #68 ↗](https://github.com/napplet/naps/pull/68)[Read proposed changes ↗](https://github.com/napplet/naps/pull/68/files)

On this pageSpecification metadata

Capability domain

`lists`

Repository state

Open PR

Registry status

Not listed in the registry

Document marker

draft

Declared dependencies

-   [identity](https://napplet.run/protocol/nap-identity/)
-   [relay](https://napplet.run/protocol/nap-relay-pr-2/)

Source revision

`7cf00cdb7bca`

[View source on GitHub ↗](https://github.com/napplet/naps/pull/68/files)
