1 Overview
This NIP defines the event scheme for polls on Nostr. A poll is a question with a fixed list of options. A response is a vote for one or more of those options. Both are ordinary signed events, as defined in NIP-01.
The poll author publishes one kind 1068 event. It carries the question, the options, the poll type, an end time, and the relays where votes belong. Each voter publishes one kind 1018 event to those relays. No server counts the votes. Every client fetches the responses and computes the same result with one shared rule: one vote per pubkey, latest event wins.
Two poll types exist. A singlechoice poll accepts one option per voter. A multiplechoice poll accepts several options per voter.
There is no ballot box on Nostr. The "result" of a poll is whatever a client computes from the response events it can see. Pick your poll relays with care — see §5.
2 The Poll Event — Kind 1068
The poll event is a kind:1068 event. The content field holds the label for the poll — the question text. The options and the settings live in tags:
{
"kind": 1068,
"content": <label for the poll>,
"tags": [
["option", <OptionId, any alphanumeric>, <option label>],
["option", <OptionId>, <option label>],
["relay", <relay url>],
["polltype", "singlechoice" | "multiplechoice"],
["endsAt", <unix timestamp in seconds>]
]
}
| Tag | Values | Rule |
|---|---|---|
| option | OptionId, label | One tag per option. The OptionId is any alphanumeric string. The label is the text shown to the voter. |
| relay | relay URL | One or multiple tags. These are the relays where the poll expects respondents to respond. |
| polltype | singlechoice or multiplechoice | Selects how many options a voter may pick. A poll with no polltype tag is a singlechoice poll. |
| endsAt | unix timestamp, seconds | The time when the poll is meant to end. Ignore votes with a later created_at. |
The source spec leaves two rules open. First, it does not say whether endsAt is required, or what happens when it is absent — a poll without it never ends. Second, it does not say how to treat a response tag whose OptionId matches no option tag in the poll; the spec's own response example votes for an id (m3agjsdq1) that no example poll defines. Counters should decide, and most discard unknown ids.
3 The Response Event — Kind 1018
The response event is a kind:1018 event. It contains an e tag with the poll event it references, followed by one or more response tags. Publish responses to the relays that the poll's relay tags specify.
| Tag | Values | Rule |
|---|---|---|
| e | poll event id | Points to the kind 1068 poll this vote answers. See NIP-01. |
| response | OptionId | The first positional argument is the literal string response. The second is the selected OptionId. Repeat the tag to select several options. |
The polltype of the poll dictates how a counter handles multiple response tags in one kind 1018 event:
- singlechoice — the first
responsetag is the actual response. Ignore the rest. - multiplechoice — the first
responsetag that points to each id is the actual response for that id. The order of the tags does not matter.
4 Counting Results
Query results by fetching kind:1018 events from the relays specified in the poll. Then apply two rules:
- Display only one vote event per pubkey.
- When one pubkey has multiple events, use the event with the largest
created_atwithin the poll limits. A voter can change a vote by publishing again beforeendsAt.
The spec shows a reference query. Use a NIP-01 filter on the #e index, with until set to the poll expiration:
const fetchVoteEvents = (filterPubkeys: string[]) => { let resultFilter: Filter = { "#e": [pollEvent.id], kinds: [1018], }; if (filterPubkeys?.length) { resultFilter.authors = filterPubkeys; // curated count, see §5 } if (pollExpiration) { resultFilter.until = Number(pollExpiration); } pool.subscribeMany(relays, [resultFilter], { onevent: handleResultEvent, }); };
const oneVotePerPubkey = (events: Event[]) => { const eventMap = new Map<string, Event>(); events.forEach((event) => { if ( !eventMap.has(event.pubkey) || event.created_at > eventMap.get(event.pubkey)!.created_at ) { eventMap.set(event.pubkey, event); // keep the latest vote } }); return Array.from(eventMap.values()); };
Kind 1018 sits in the regular storage range of NIP-01, so relays keep every vote and the client deduplicates. A 2024 draft made the response a parameterized replaceable event instead; the reinstated version reverted to regular events. Note also that created_at is set by the voter, so the "latest vote wins" rule trusts voter timestamps — the relay advice below exists to limit that.
5 Relays and Curation
Poll results must stay stable after the poll ends. It is advisable for poll authors to use relays that:
- do not allow backdated events, and
- do not honor
kind:5delete requests (NIP-09) for vote events.
On a relay that accepts backdated events, a voter can publish a "late" vote with a created_at just before endsAt. On a relay that honors deletes, voters can erase votes after the poll closes. Both break the integrity of a finished poll.
Anyone can create a pubkey, so raw counts are open to sybil votes. Clients may configure fetching results by specific people. To do this, create a kind:30000 follow set (NIP-51) and fetch results only from the authors in the set — that is the filterPubkeys argument in §4. Clients can also employ other curation algorithms, such as Proof of Work (NIP-13) and Web of Trust scores.
6 Worked Example
The poll below asks one question with two options. It is a singlechoice poll and it names two relays for votes. This is the spec's own example event:
{
"content": "Pineapple on pizza",
"created_at": 1719888496,
"id": "9d1b6b9562e66f2ecf35eb0a3c2decc736c47fddb13d6fb8f87185a153ea3634",
"kind": 1068,
"pubkey": "dee45a23c4f1d93f3a2043650c5081e4ac14a778e0acbef03de3768e4f81ac7b",
"sig": "7fa93bf3c430eaef784b0dacc217d3cd5eff1c520e7ef5d961381bc0f014dde6286618048d924808e54d1be03f2f2c2f0f8b5c9c2082a4480caf45a565ca9797",
"tags": [
["option", "qj518h583", "Yay"],
["option", "gga6cdnqj", "Nay"],
["relay", "<relay url1>"],
["relay", "<relay url2>"],
["polltype", "singlechoice"],
["endsAt", "<unix timestamp in seconds>"]
]
}
A voter answers with a kind 1018 event. The content stays empty. This is the spec's example response; it carries two response tags, so on a singlechoice poll only the first one counts:
{
"content": "",
"created_at": 1720097117,
"id": "60a005e32e9596c3f544a841a9bc4e46d3020ca3650d6a739c95c1568e33f6d8",
"kind": 1018,
"pubkey": "1bc70a0148b3f316da33fe7e89f23e3e71ac4ff998027ec712b905cd24f6a411",
"sig": "30071a633c65db8f3a075c7a8de757fbd8ce65e3607f4ba287fe6d7fbf839a380f94ff4e826fbba593f6faaa13683b7ea9114ade140720ecf4927010ebf3e44f",
"tags": [
["e", "1fc80cf813f1af33d5a435862b7ef7fb96b47e68a48f1abcadf8081f5a545550"],
["response", "gga6cdnqj"],
["response", "m3agjsdq1"]
]
}
To count, a client connects to the poll relays and requests every vote for the poll:
→ ["REQ", "tally", {"kinds":[1018], "#e":["9d1b6b9562e6…"], "until":1720500000}] ← ["EVENT", "tally", {"kind":1018, "pubkey":"1bc70a01…", "tags":[["e","9d1b…"],["response","gga6cdnqj"]], …}] ← ["EVENT", "tally", {"kind":1018, "pubkey":"5fe20ab9…", "tags":[["e","9d1b…"],["response","qj518h583"]], …}] ← ["EVENT", "tally", {"kind":1018, "pubkey":"1bc70a01…", "created_at":1720099000, "tags":[["e","9d1b…"],["response","qj518h583"]], …}] // same voter, later — replaces the first ← ["EOSE", "tally"] -- dedup per pubkey, latest wins: Yay 2, Nay 0 --
7 Version History
NIP-88 had a bumpy childhood: drafted in mid-2024, renumbered, removed, then reinstated in 2025. Notable changes, newest first:
- 20252025-04..12Typo, title, and link fixes across the repository touched this file.
- 20252025-01Reinstated as NIP-88 with responses as regular kind 1018 events (PR #1507).
- 20242024-09fiatjaf removed the polls NIP from the repository, temporarily.
- 20242024-09Renumbered to 88. Alex Gleason briefly made the poll response a parameterized replaceable event; the reinstated version reverted this.
- 20242024-07Draft finalized; the settings tag was removed in fine-tuning.
- Origin2024-06abhay-raizada published the first draft, "Polls On Nostr".
Original Proposal - Credits
| Author | abhay-raizada |
| First commit | 2024-06-25 — “Polls On Nostr” (renumbered to 88 on 2024-09-17) |
| Reinstated | 2025-01-14 — PR #1507 |
| Status | draft - optional |
| Source | 88.md on GitHub - full change history |
8 See Also
- NIP-01 — the event format, the
etag, and the#e/untilfilters used to fetch votes. - NIP-09 — kind 5 delete requests; poll relays should not honor them for votes.
- NIP-51 — kind 30000 follow sets, used to curate whose votes count.
- NIP-13 — Proof of Work, one of the suggested sybil-resistance curation tools.
- NIP-25 — reactions; the other lightweight "express an opinion on an event" scheme.