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

NAP-IDENTITY / Merged specification

# Read-Only User Identity Queries

Merged

**Shipped implementation: supported**

Read-only identity queries and updates have focused binding and reference-runtime coverage.

Implementation proposal: [merged ↗](https://github.com/napplet/naps/pull/102), 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/43cb3a4e855570ec4b90b9cc05f0c721e5ad0aab/naps/NAP-IDENTITY.md). The document below is rendered from its original Markdown. [Follow the latest source on GitHub ↗](https://github.com/napplet/naps/blob/master/naps/NAP-IDENTITY.md).

## NAP-IDENTITY

### Read-Only User Identity Queries

`draft`

**NAP ID:** NAP-IDENTITY **Domain:** `identity` **Depends:**

-   `resource` — capability · optional — `picture` / `banner` URL bytes are fetched via `resource.bytes` **Web binding ([NIP-5D](https://github.com/nostr-protocol/nips/pull/2303)):** `window.napplet.identity`; domain presence signals availability.

### Description

NAP-IDENTITY provides read-only access to the shell-user identity: the currently connected user/signer pubkey and related public identity data. Napplets that need to display the user's profile, check their follow list, or read their relay preferences query the shell for this data. The shell resolves these queries against its local cache or connected relays and returns the results.

Napplets do not have direct access to the user's private key. They cannot sign events, encrypt, or decrypt. Identity queries are strictly read-only -- napplets learn *about* the user but cannot act *as* the user. Signing is delegated to the shell via `relay.publish()` (NAP-RELAY). Encryption is delegated via `relay.publishEncrypted()`.

This shell-user identity is distinct from napplet identity. The runtime binds an authenticated endpoint to its verified manifest and artifact before execution, as defined by the [web projection](https://github.com/napplet/naps/blob/43cb3a4e855570ec4b90b9cc05f0c721e5ad0aab/projections/web.md). NAP-IDENTITY only reports the user's connected signer identity; it does not establish or change the napplet's identity.

### API Surface

| Operation | Parameters | Result | Wire |
| --- | --- | --- | --- |
| `getPublicKey` | none | `tstr` public key, or `""` when no signer is connected | `identity.getPublicKey` / `identity.getPublicKey.result` |
| `onChanged` | handler for `tstr` public key | `Subscription` handle | `identity.changed` |
| `getRelays` | none | `RelayMap` | `identity.getRelays` / `identity.getRelays.result` |
| `getProfile` | none | `ProfileData` or `null` | `identity.getProfile` / `identity.getProfile.result` |
| `getFollows` | none | list of `tstr` pubkeys | `identity.getFollows` / `identity.getFollows.result` |
| `getList` | `type` (`tstr`) | list of `tstr` entries | `identity.getList` / `identity.getList.result` |
| `getZaps` | none | list of `ZapReceipt` | `identity.getZaps` / `identity.getZaps.result` |
| `getMutes` | none | list of `tstr` pubkeys | `identity.getMutes` / `identity.getMutes.result` |
| `getBlocked` | none | list of `tstr` pubkeys | `identity.getBlocked` / `identity.getBlocked.result` |
| `getBadges` | none | list of `Badge` | `identity.getBadges` / `identity.getBadges.result` |

#### Schemas

`RelayPermissions` fields:

| Field | Required | Type |
| --- | --- | --- |
| `read` | yes | boolean |
| `write` | yes | boolean |

`RelayMap` is a map from relay URL text to `RelayPermissions`.

`ProfileData` fields:

| Field | Required | Type |
| --- | --- | --- |
| `name` | no | text |
| `displayName` | no | text |
| `about` | no | text |
| `picture` | no | text |
| `banner` | no | text |
| `nip05` | no | text |
| `lud16` | no | text |
| `website` | no | text |

**Resource resolution.** `picture` and `banner` are URL strings. Napplets that need their bytes MUST use `resource.bytes` when the optional `resource` domain is exposed, and SHOULD use local placeholder artwork when it is absent. The [web projection](https://github.com/napplet/naps/blob/43cb3a4e855570ec4b90b9cc05f0c721e5ad0aab/projections/web.md) applies the runtime's network policy; an opaque sandbox origin alone does not block network requests. URLs do not grant network authority. Standard resource policy applies to mediated fetches.

`ZapReceipt` fields:

| Field | Required | Type |
| --- | --- | --- |
| `eventId` | yes | text |
| `sender` | yes | text |
| `amount` | yes | unsigned integer |
| `content` | no | text |

`Badge` fields:

| Field | Required | Type |
| --- | --- | --- |
| `id` | yes | text |
| `name` | no | text |
| `description` | no | text |
| `image` | no | text |
| `thumbs` | no | list of text |
| `awardedBy` | yes | text |

**`getPublicKey()`** -- Returns the user's hex-encoded public key, or the empty string when no user/signer is connected. This is the most basic identity query. Every shell that implements NAP-IDENTITY MUST support this method.

**`onChanged(handler)`** -- Registers a handler for shell-pushed user identity changes. The handler receives the same public-key string shape as `getPublicKey()`: a non-empty hex pubkey when a user/signer is connected, or `""` when the shell signs out or disconnects the signer.

**`getRelays()`** -- Returns the user's relay list as a record mapping relay URLs to read/write permissions (NIP-65 relay list metadata).

**`getProfile()`** -- Returns the user's kind 0 profile metadata. Returns `null` if no profile is found. The shell resolves this from its cache or relays.

**`getFollows()`** -- Returns an array of hex-encoded public keys from the user's kind 3 contact list.

**`getList(type)`** -- Returns entries from a user's categorized list. The `type` parameter specifies the list kind (e.g., `"bookmarks"`, `"interests"`, `"pins"`). Returns the tag values from the matching parameterized replaceable event.

**`getZaps()`** -- Returns zap receipts (kind 9735) sent to the user. Each receipt includes the sender, amount in millisats, and optional content.

**`getMutes()`** -- Returns the user's mute list (kind 10000) as an array of hex-encoded public keys.

**`getBlocked()`** -- Returns the user's block list as an array of hex-encoded public keys. Semantically distinct from mutes -- blocks are hard exclusions.

**`getBadges()`** -- Returns badges awarded to the user (NIP-58). Each badge includes the badge definition ID, metadata, and the awarding pubkey.

### Wire Protocol

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

| Type | Direction | Payload fields |
| --- | --- | --- |
| `identity.getPublicKey` | napplet -> shell | `id` |
| `identity.getPublicKey.result` | shell -> napplet | `id`, `pubkey` |
| `identity.changed` | shell -> napplet | `pubkey` |
| `identity.getRelays` | napplet -> shell | `id` |
| `identity.getRelays.result` | shell -> napplet | `id`, `relays`, `error?` |
| `identity.getProfile` | napplet -> shell | `id` |
| `identity.getProfile.result` | shell -> napplet | `id`, `profile?`, `error?` |
| `identity.getFollows` | napplet -> shell | `id` |
| `identity.getFollows.result` | shell -> napplet | `id`, `pubkeys`, `error?` |
| `identity.getList` | napplet -> shell | `id`, `listType` |
| `identity.getList.result` | shell -> napplet | `id`, `entries`, `error?` |
| `identity.getZaps` | napplet -> shell | `id` |
| `identity.getZaps.result` | shell -> napplet | `id`, `zaps`, `error?` |
| `identity.getMutes` | napplet -> shell | `id` |
| `identity.getMutes.result` | shell -> napplet | `id`, `pubkeys`, `error?` |
| `identity.getBlocked` | napplet -> shell | `id` |
| `identity.getBlocked.result` | shell -> napplet | `id`, `pubkeys`, `error?` |
| `identity.getBadges` | napplet -> shell | `id` |
| `identity.getBadges.result` | shell -> napplet | `id`, `badges`, `error?` |

Key design notes:

-   All methods are request/result pairs using `id` for correlation.
-   `identity.changed` is a push message. It has no `id` and is not a response to a request.
-   All results include an optional `error` field for failure cases.
-   `getPublicKey` is the simplest query and MUST always succeed (no `error` field). It returns `pubkey: ""` when no shell-user identity is currently connected.
-   `getProfile` returns `profile: null` if no kind 0 is found -- this is not an error.
-   `getList` includes a `listType` field to specify which categorized list to query.

Recommended handshake:

1.  A napplet that needs user identity calls `identity.getPublicKey` once on startup to get a snapshot.
2.  The shell returns `identity.getPublicKey.result` with `pubkey` set to either a hex pubkey or `""`.
3.  The napplet then listens for `identity.changed` and re-runs identity-dependent work when the pushed pubkey changes.

Napplets SHOULD NOT poll `identity.getPublicKey` waiting for a signer to appear. Shells MUST use `identity.changed` to notify already-loaded napplets when the shell-user identity changes.

#### Examples

**Get public key:**

```
-> { "type": "identity.getPublicKey", "id": "q1" }
<- { "type": "identity.getPublicKey.result", "id": "q1", "pubkey": "ab12cd..." }
```

**No user connected:**

```
-> { "type": "identity.getPublicKey", "id": "q1b" }
<- { "type": "identity.getPublicKey.result", "id": "q1b", "pubkey": "" }
```

**User identity changed:**

```
<- { "type": "identity.changed", "pubkey": "ab12cd..." }
<- { "type": "identity.changed", "pubkey": "" }
```

**Get profile:**

```
-> { "type": "identity.getProfile", "id": "q2" }
<- { "type": "identity.getProfile.result", "id": "q2", "profile": { "name": "Alice", "about": "Nostr dev", "picture": "https://..." } }
```

**Profile not found:**

```
-> { "type": "identity.getProfile", "id": "q3" }
<- { "type": "identity.getProfile.result", "id": "q3", "profile": null }
```

**Get follows:**

```
-> { "type": "identity.getFollows", "id": "q4" }
<- { "type": "identity.getFollows.result", "id": "q4", "pubkeys": ["ab12...", "cd34...", "ef56..."] }
```

**Get relay list:**

```
-> { "type": "identity.getRelays", "id": "q5" }
<- { "type": "identity.getRelays.result", "id": "q5", "relays": { "wss://relay.example.com": { "read": true, "write": true }, "wss://readonly.example.com": { "read": true, "write": false } } }
```

**Get categorized list:**

```
-> { "type": "identity.getList", "id": "q6", "listType": "bookmarks" }
<- { "type": "identity.getList.result", "id": "q6", "entries": ["note1abc...", "note1def..."] }
```

**Get mutes:**

```
-> { "type": "identity.getMutes", "id": "q7" }
<- { "type": "identity.getMutes.result", "id": "q7", "pubkeys": ["spam1...", "spam2..."] }
```

**Get zap receipts:**

```
-> { "type": "identity.getZaps", "id": "q8" }
<- { "type": "identity.getZaps.result", "id": "q8", "zaps": [{ "eventId": "ev1...", "sender": "ab12...", "amount": 21000, "content": "Great post!" }] }
```

**Get badges:**

```
-> { "type": "identity.getBadges", "id": "q9" }
<- { "type": "identity.getBadges.result", "id": "q9", "badges": [{ "id": "badge1...", "name": "Early Adopter", "awardedBy": "cd34..." }] }
```

**Error case:**

```
-> { "type": "identity.getFollows", "id": "q10" }
<- { "type": "identity.getFollows.result", "id": "q10", "pubkeys": [], "error": "relay timeout" }
```

#### Error Handling

Result messages include an `error` field (string) when the shell cannot fulfill the request. Common errors: `"relay timeout"`, `"not found"`, `"unsupported list type"`. When `error` is present, the primary result field contains a sensible default (empty array, null, etc.).

`identity.getPublicKey.result` MUST NOT include an `error` field. The shell returns `pubkey: ""` when no user/signer is connected.

### Shell Behavior

-   The shell MUST respond to every `identity.*` request with the corresponding `*.result` message carrying the same `id`.
-   The shell MUST return `pubkey` for `identity.getPublicKey` without error. If no user/signer is connected, `pubkey` MUST be the empty string.
-   The shell MUST emit `identity.changed` to loaded napplets whenever the shell-user identity changes.
-   The shell MUST emit `identity.changed` with `pubkey: ""` when the shell-user identity is cleared, the signer disconnects, or the user signs out.
-   The shell MAY emit an initial `identity.changed` after napplet load, but napplets MUST NOT depend on that initial push and SHOULD use `identity.getPublicKey` for the startup snapshot.
-   The shell SHOULD resolve identity queries from its local cache when available.
-   The shell MAY fetch data from relays if not cached.
-   The shell MAY return stale data with a best-effort freshness policy.
-   The shell MAY enforce ACL checks on identity capabilities (e.g., restricting which napplets can query follows or mutes).
-   The shell MAY return empty results for queries it does not support (e.g., badges) rather than an error.
-   The shell MUST NOT expose the user's private key or any signing capability through this interface.
-   The shell MUST NOT provide encrypt or decrypt operations through this interface.

### Security Considerations

-   NAP-IDENTITY is strictly read-only. No method modifies user state, signs events, or performs cryptographic operations.
-   `identity.changed` reports only the shell-user identity. It does not change or renegotiate the napplet's [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) session identity.
-   The user's public key is not secret, but follow lists, mute lists, and block lists reveal social graph information. Shells MAY restrict access to sensitive lists based on napplet trust level.
-   Zap receipts reveal financial information. Shells SHOULD consider whether to expose zap data to all napplets or restrict it to trusted napplets.
-   Profile data may contain URLs (picture, banner, website). Napplets that render these URLs should sanitize them. The shell is not responsible for sanitizing profile content.
-   This interface intentionally excludes signing, encryption, and decryption. These operations are security-critical and belong to the shell. See NAP-RELAY for shell-mediated publish and encrypted publish.
-   Block lists and mute lists are sensitive. A malicious napplet could use this data to target blocked users through side channels. Shells MAY redact or restrict access to these lists.

### Implementations

-   (none yet)

### Changelog

-   `a802d35` - Introduced read-only user identity queries and optional resource fetching.
-   `06b1a1a` - Adopted injected-domain availability, verified manifest and artifact identity, and graceful fallback for optional resource fetching.

[Read specification on GitHub ↗](https://github.com/napplet/naps/blob/master/naps/NAP-IDENTITY.md)

On this pageSpecification metadata

Capability domain

`identity`

Repository state

Merged

Registry status

Draft

Document marker

draft

Declared dependencies

-   [resource](https://napplet.run/protocol/nap-resource-pr-80/)

Source revision

`43cb3a4e8555`

[View source on GitHub ↗](https://github.com/napplet/naps/blob/master/naps/NAP-IDENTITY.md)
