docs: first draft of NIP

This commit is contained in:
Alejandro Gómez
2026-02-27 12:07:02 +01:00
parent 36d321649d
commit b59afbaee3

202
NIP-xx.md Normal file
View File

@@ -0,0 +1,202 @@
NIP-xx
======
Spells
------
`draft` `optional`
## Abstract
This NIP defines `kind:777` events ("spells") that encode Nostr relay query filters as portable, shareable events. A spell stores a REQ or COUNT filter with optional runtime variables and relative timestamps, allowing users to publish, discover, and execute saved queries across clients.
## Event Format
A spell is a regular (non-replaceable) event with `kind:777`.
The `content` field contains a human-readable description of the query in plain text. It MAY be an empty string.
### Required Tags
| tag | values | description |
| ----- | ------------- | ---------------------- |
| `cmd` | `REQ`\|`COUNT` | Query command type |
A spell MUST contain at least one filter tag (see below).
### Filter Tags
Filter tags encode the fields of a Nostr REQ filter.
| tag | values | REQ filter field | notes |
| --------- | --------------------------------------- | ---------------- | ---------------------------------- |
| `k` | `<kind number>` | `kinds` | One tag per kind for queryability |
| `authors` | `<pubkey1>`, `<pubkey2>`, ... | `authors` | Single tag, multiple values |
| `ids` | `<id1>`, `<id2>`, ... | `ids` | Single tag, multiple values |
| `tag` | `<letter>`, `<val1>`, `<val2>`, ... | `#<letter>` | See [Tag Filters](#tag-filters) |
| `limit` | `<integer>` | `limit` | |
| `since` | `<timestamp>` or `<relative>` | `since` | See [Relative Timestamps](#relative-timestamps) |
| `until` | `<timestamp>` or `<relative>` | `until` | See [Relative Timestamps](#relative-timestamps) |
| `search` | `<query string>` | `search` | [NIP-50](50.md) |
| `relays` | `<wss://url1>`, `<wss://url2>`, ... | — | Target relay URLs |
All filter tag values are strings. Numeric values (kinds, limit, timestamps) MUST be encoded as decimal strings.
### Tag Filters
Filter conditions on event tags are encoded as `["tag", <letter>, <value>, ...]` rather than using the tag letter directly (e.g., `["e", ...]` or `["p", ...]`). This prevents semantic collision — a `["p", <pubkey>]` tag on a Nostr event normally means "this event references this pubkey," which would cause relays and clients to misinterpret filter parameters as social graph references.
The `k` tag is the exception: it uses the tag letter directly (`["k", "1"]`) to enable relay-side indexing and discovery of spells by the kinds they query.
Examples:
```
["tag", "t", "bitcoin", "nostr"] → filter: {"#t": ["bitcoin", "nostr"]}
["tag", "p", "abcd...", "ef01..."] → filter: {"#p": ["abcd...", "ef01..."]}
["tag", "e", "abcd..."] → filter: {"#e": ["abcd..."]}
```
### Metadata Tags
| tag | values | description |
| ---------------- | ---------- | ------------------------------------------------------------ |
| `name` | `<string>` | Human-readable spell name |
| `alt` | `<string>` | [NIP-31](31.md) alternative text |
| `t` | `<topic>` | Topic tag for categorization (multiple allowed) |
| `close-on-eose` | `""` | Clients SHOULD close the subscription after EOSE |
| `e` | `<event-id>` | Fork provenance: references the parent spell event |
Note: `["t", "bitcoin"]` as a top-level tag categorizes the spell itself, while `["tag", "t", "bitcoin"]` is a filter condition matching events with `#t = bitcoin`. Both may appear in the same event.
## Runtime Variables
The `authors` tag and `tag` filter values MAY contain runtime variables that are resolved at execution time.
| variable | resolves to |
| ------------ | ----------------------------------------------------- |
| `$me` | The executing user's pubkey |
| `$contacts` | All pubkeys from the executing user's kind 3 contact list |
Variables are case-sensitive and MUST be lowercase.
If a client cannot resolve a variable (no logged-in user for `$me`, no contact list for `$contacts`), it MUST NOT send the REQ and SHOULD display a message explaining the unresolved dependency.
## Relative Timestamps
The `since` and `until` tags MAY contain relative time expressions instead of Unix timestamps.
Grammar:
```
value = unix-timestamp / relative-time / "now"
relative-time = 1*DIGIT unit
unit = "s" / "m" / "h" / "d" / "w"
```
| unit | meaning | seconds |
| ---- | ------- | ------- |
| `s` | seconds | 1 |
| `m` | minutes | 60 |
| `h` | hours | 3600 |
| `d` | days | 86400 |
| `w` | weeks | 604800 |
`now` resolves to the current Unix timestamp. A relative time `Nd` resolves to `now - N * 86400`.
Clients MUST resolve relative timestamps to absolute Unix timestamps before constructing a REQ message.
## Executing a Spell
To execute a spell, a client:
1. Parses the event tags to reconstruct a filter object
2. Resolves runtime variables (`$me`, `$contacts`) using the executing user's identity
3. Resolves relative timestamps to absolute Unix timestamps
4. Constructs a REQ or COUNT message (per the `cmd` tag) with the resolved filter
5. Determines target relays (see [Relay Resolution](#relay-resolution))
6. Sends the REQ or COUNT message to the resolved relays
7. If `close-on-eose` is present, closes the subscription after receiving EOSE from all connected relays
### Relay Resolution
If the spell contains a `relays` tag, the client SHOULD send the query to those relays.
If no `relays` tag is present, the client SHOULD use [NIP-65](65.md) relay lists to determine where to send the query, falling back to the executing user's NIP-65 read relays.
## Discovering Spells
Clients can discover spells using standard Nostr queries:
- By author: `{"kinds": [777], "authors": ["<pubkey>"]}`
- By topic: `{"kinds": [777], "#t": ["bitcoin"]}`
- By queried kind: `{"kinds": [777], "#k": ["1"]}`
## Examples
A spell that finds recent notes about Bitcoin from the user's contacts:
```json
{
"kind": 777,
"content": "Notes about Bitcoin from my contacts",
"tags": [
["cmd", "REQ"],
["name", "Bitcoin from contacts"],
["alt", "Spell: notes about Bitcoin from contacts"],
["k", "1"],
["authors", "$contacts"],
["tag", "t", "bitcoin"],
["since", "7d"],
["limit", "50"],
["t", "bitcoin"],
["t", "social"]
]
}
```
When executed by a user with 3 contacts, this resolves to:
```json
["REQ", "<sub-id>", {
"kinds": [1],
"authors": ["aabb...", "ccdd...", "eeff..."],
"#t": ["bitcoin"],
"since": 1740585600,
"limit": 50
}]
```
A COUNT spell with multiple kinds and an absolute timestamp:
```json
{
"kind": 777,
"content": "",
"tags": [
["cmd", "COUNT"],
["k", "1"],
["k", "6"],
["k", "7"],
["authors", "$me"],
["since", "1704067200"],
["close-on-eose", ""]
]
}
```
A spell querying specific relays with a NIP-50 search:
```json
{
"kind": 777,
"content": "Search for Nostr dev discussions",
"tags": [
["cmd", "REQ"],
["name", "Nostr dev search"],
["k", "1"],
["search", "nostr development"],
["relays", "wss://relay.damus.io", "wss://nos.lol"],
["limit", "100"]
]
}
```