12<html lang="en">3<head>4<meta charset="utf-8">5<meta name="viewport" content="width=device-width, initial-scale=1">6<title>NIP-88 - Polls — Nostr Instruction Manual</title>7<meta name="description" content="NIP-88: polls on Nostr. Kind 1068 poll events, kind 1018 response events, single and multiple choice, and how clients count votes. Complete manual page.">8<link rel="preload" href="../assets/fonts/michroma-400-latin.woff2" as="font" type="font/woff2" crossorigin>9<link rel="preload" href="../assets/fonts/exo2-var-latin.woff2" as="font" type="font/woff2" crossorigin>10<link rel="preload" href="../assets/fonts/sairacondensed-600-latin.woff2" as="font" type="font/woff2" crossorigin>11<link rel="preload" href="../assets/fonts/sairacondensed-700-latin.woff2" as="font" type="font/woff2" crossorigin>12<link rel="preload" href="../assets/fonts/sairacondensed-500-latin.woff2" as="font" type="font/woff2" crossorigin>13<link rel="preload" href="../assets/fonts/sharetechmono-400-latin.woff2" as="font" type="font/woff2" crossorigin>14<link rel="preload" href="../assets/fonts/exo2-400i-latin.woff2" as="font" type="font/woff2" crossorigin>15<link rel="icon" href="../assets/favicon.svg" type="image/svg+xml">16<link rel="stylesheet" href="../assets/manual.css">17</head>18<body>1920<svg width="0" height="0" style="position:absolute">21 <defs>22 <marker id="arr" markerWidth="9" markerHeight="9" refX="7" refY="4.5" orient="auto">23 <path d="M1,1 L8,4.5 L1,8 Z" fill="#f26a00"/>24 </marker>25 <marker id="arrb" markerWidth="9" markerHeight="9" refX="7" refY="4.5" orient="auto">26 <path d="M1,1 L8,4.5 L1,8 Z" fill="#2e5f9e"/>27 </marker>28 </defs>29</svg>3031<div class="page">32 <span class="staple s1"></span><span class="staple s2"></span>3334 <header class="band">35 <a class="logo" href="../index.html">NOSTR<b>▸</b>PROTOCOL MANUAL</a>36 <span class="booklet-note">Chapter 88 - Social</span>37 </header>38 <nav class="tabs">39 <a href="../index.html">Contents</a>40 <a href="#overview" class="on">Overview</a>41 <a href="#spec">Spec</a>42 <a href="#examples">Examples</a>43 <a href="#history">History</a>44 </nav>4546 <div class="chapter">47 <div class="nipno" data-n="88">88</div>48 <div class="titles">49 <div class="kicker">Nostr Implementation Possibility - Hex № 88</div>50 <h1>Polls</h1>51 <p class="sub">A signed question, signed answers, and a counting rule: kind 1068 asks, kind 1018 votes, and any client can tally the result.</p>52 </div>53 </div>5455 <main class="spread">56 <div class="main">5758 <h2 class="bar" id="overview"><span class="num">1</span> Overview</h2>59 <p>This NIP defines the event scheme for polls on Nostr. A <strong>poll</strong> is a question with a fixed list of options. A <strong>response</strong> is a vote for one or more of those options. Both are ordinary signed events, as defined in <a href="01.html">NIP-01</a>.</p>60 <p>The poll author publishes one <strong>kind 1068</strong> event. It carries the question, the options, the poll type, an end time, and the relays where votes belong. Each voter publishes one <strong>kind 1018</strong> 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.</p>61 <p>Two poll types exist. A <strong>singlechoice</strong> poll accepts one option per voter. A <strong>multiplechoice</strong> poll accepts several options per voter.</p>6263 <div class="box hint">64 <span class="tag">Hint!</span>65 <p>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.</p>66 </div>6768 <figure class="diagram">69 <div class="frame">70 <svg viewBox="0 0 560 200" role="img" aria-label="Author publishes a kind 1068 poll; voters publish kind 1018 responses to the poll relays; a client queries the responses to count the result">71 <rect class="d-actor" x="10" y="20" width="110" height="50" rx="8"/>72 <text class="d-actor-label" x="65" y="42" text-anchor="middle">Author</text>73 <text class="d-note" x="65" y="59" text-anchor="middle">kind 1068</text>74 <rect class="d-actor" x="10" y="126" width="110" height="50" rx="8"/>75 <text class="d-actor-label" x="65" y="148" text-anchor="middle">Voters</text>76 <text class="d-note" x="65" y="165" text-anchor="middle">kind 1018</text>77 <rect class="d-actor" x="225" y="72" width="110" height="50" rx="8"/>78 <text class="d-actor-label" x="280" y="94" text-anchor="middle">Poll relays</text>79 <text class="d-note" x="280" y="111" text-anchor="middle">from relay tags</text>80 <rect class="d-actor" x="440" y="72" width="110" height="50" rx="8"/>81 <text class="d-actor-label" x="495" y="94" text-anchor="middle">Client</text>82 <text class="d-note" x="495" y="111" text-anchor="middle">counts votes</text>83 <path class="d-msg d-flow" d="M122,45 C170,50 180,72 223,84"/>84 <path class="d-msg d-flow" d="M122,151 C170,146 180,124 223,112"/>85 <path class="d-msg blue d-flow" d="M337,98 L438,98"/>86 <text class="d-label" x="160" y="46" text-anchor="middle">POLL</text>87 <text class="d-label" x="160" y="160" text-anchor="middle">VOTE</text>88 <text class="d-label" x="388" y="90" text-anchor="middle">REQ #e</text>89 <circle r="4" fill="#f26a00">90 <animateMotion dur="2.6s" repeatCount="indefinite" path="M122,151 C170,146 180,124 223,112"/>91 </circle>92 <circle r="4" fill="#2e5f9e">93 <animateMotion dur="2.6s" begin="1.3s" repeatCount="indefinite" path="M337,98 L438,98"/>94 </circle>95 </svg>96 </div>97 <figcaption>Fig. 88-1 — <b>Poll lifecycle.</b> The poll names its relays. Votes go there. Counters read from there.</figcaption>98 </figure>99100 <h2 class="bar" id="spec"><span class="num">2</span> The Poll Event — Kind 1068</h2>101 <p>The poll event is a <code>kind:1068</code> event. The <code>content</code> field holds the label for the poll — the question text. The options and the settings live in tags:</p>102103 <div class="crt">104 <div class="titlebar">Wire format - kind 1068 poll event</div>105<pre>{106 <span class="k">"kind"</span>: 1068,107 <span class="k">"content"</span>: <span class="c"><label for the poll></span>,108 <span class="k">"tags"</span>: [109 [<span class="s">"option"</span>, <span class="c"><OptionId, any alphanumeric></span>, <span class="c"><option label></span>],110 [<span class="s">"option"</span>, <span class="c"><OptionId></span>, <span class="c"><option label></span>],111 [<span class="s">"relay"</span>, <span class="c"><relay url></span>],112 [<span class="s">"polltype"</span>, <span class="s">"singlechoice"</span> | <span class="s">"multiplechoice"</span>],113 [<span class="s">"endsAt"</span>, <span class="c"><unix timestamp in seconds></span>]114 ]115}</pre>116 </div>117118 <div class="tscroll"><table class="ref">119 <caption>Major tags of the poll event</caption>120 <tr><th>Tag</th><th>Values</th><th>Rule</th></tr>121 <tr><td class="btn">option</td><td>OptionId, label</td><td>One tag per option. The OptionId is any alphanumeric string. The label is the text shown to the voter.</td></tr>122 <tr><td class="btn">relay</td><td>relay URL</td><td>One or multiple tags. These are the relays where the poll expects respondents to respond.</td></tr>123 <tr><td class="btn">polltype</td><td><code>singlechoice</code> or <code>multiplechoice</code></td><td>Selects how many options a voter may pick. A poll with no polltype tag is a <code>singlechoice</code> poll.</td></tr>124 <tr><td class="btn">endsAt</td><td>unix timestamp, seconds</td><td>The time when the poll is meant to end. Ignore votes with a later <code>created_at</code>.</td></tr>125 </table></div>126127 <div class="box tech">128 <span class="tag">Tech Note</span>129 <p>The source spec leaves two rules open. First, it does not say whether <code>endsAt</code> is required, or what happens when it is absent — a poll without it never ends. Second, it does not say how to treat a <code>response</code> tag whose OptionId matches no <code>option</code> tag in the poll; the spec's own response example votes for an id (<code>m3agjsdq1</code>) that no example poll defines. Counters should decide, and most discard unknown ids.</p>130 </div>131132 <h2 class="bar" id="response"><span class="num">3</span> The Response Event — Kind 1018</h2>133 <p>The response event is a <code>kind:1018</code> event. It contains an <code>e</code> tag with the poll event it references, followed by one or more <code>response</code> tags. Publish responses to the relays that the poll's <code>relay</code> tags specify.</p>134135 <div class="tscroll"><table class="ref">136 <caption>Tags of the response event</caption>137 <tr><th>Tag</th><th>Values</th><th>Rule</th></tr>138 <tr><td class="btn">e</td><td>poll event id</td><td>Points to the kind 1068 poll this vote answers. See <a href="01.html">NIP-01</a>.</td></tr>139 <tr><td class="btn">response</td><td>OptionId</td><td>The first positional argument is the literal string <code>response</code>. The second is the selected OptionId. Repeat the tag to select several options.</td></tr>140 </table></div>141142 <p>The <code>polltype</code> of the poll dictates how a counter handles multiple <code>response</code> tags in one kind 1018 event:</p>143 <ul>144 <li><strong>singlechoice</strong> — the first <code>response</code> tag is the actual response. Ignore the rest.</li>145 <li><strong>multiplechoice</strong> — the first <code>response</code> tag that points to each id is the actual response for that id. The order of the tags does not matter.</li>146 </ul>147148 <h2 class="bar" id="counting"><span class="num">4</span> Counting Results</h2>149 <p>Query results by fetching <code>kind:1018</code> events from the relays specified in the poll. Then apply two rules:</p>150 <ol>151 <li>Display only <strong>one vote event per pubkey</strong>.</li>152 <li>When one pubkey has multiple events, use the event with the largest <code>created_at</code> within the poll limits. A voter can change a vote by publishing again before <code>endsAt</code>.</li>153 </ol>154155 <figure class="diagram">156 <div class="frame">157 <svg viewBox="0 0 560 120" role="img" aria-label="Counting pipeline: fetch kind 1018 responses, cut off at endsAt, keep the latest event per pubkey, apply the polltype, then tally">158 <rect class="d-actor" x="10" y="35" width="100" height="50" rx="8"/>159 <text class="d-actor-label" x="60" y="57" text-anchor="middle">Fetch</text>160 <text class="d-note" x="60" y="74" text-anchor="middle">#e + kind 1018</text>161 <rect class="d-actor" x="150" y="35" width="100" height="50" rx="8"/>162 <text class="d-actor-label" x="200" y="57" text-anchor="middle">Cut off</text>163 <text class="d-note" x="200" y="74" text-anchor="middle">until endsAt</text>164 <rect class="d-actor" x="290" y="35" width="110" height="50" rx="8"/>165 <text class="d-actor-label" x="345" y="57" text-anchor="middle">Dedup</text>166 <text class="d-note" x="345" y="74" text-anchor="middle">latest per pubkey</text>167 <rect class="d-actor" x="440" y="35" width="110" height="50" rx="8"/>168 <text class="d-actor-label" x="495" y="57" text-anchor="middle">Tally</text>169 <text class="d-note" x="495" y="74" text-anchor="middle">apply polltype</text>170 <path class="d-msg d-flow" d="M112,60 L147,60"/>171 <path class="d-msg d-flow" d="M252,60 L287,60"/>172 <path class="d-msg d-flow" d="M402,60 L437,60"/>173 </svg>174 </div>175 <figcaption>Fig. 88-2 — <b>Counting pipeline.</b> Every client that follows these steps computes the same result from the same events.</figcaption>176 </figure>177178 <p>The spec shows a reference query. Use a <a href="01.html">NIP-01</a> filter on the <code>#e</code> index, with <code>until</code> set to the poll expiration:</p>179 <div class="crt">180 <div class="titlebar">Reference - querying vote events (TypeScript)</div>181<pre><span class="k">const</span> fetchVoteEvents = (filterPubkeys: string[]) => {182 <span class="k">let</span> resultFilter: Filter = {183 <span class="s">"#e"</span>: [pollEvent.id],184 kinds: [1018],185 };186 <span class="k">if</span> (filterPubkeys?.length) {187 resultFilter.authors = filterPubkeys; <span class="c">// curated count, see §5</span>188 }189 <span class="k">if</span> (pollExpiration) {190 resultFilter.until = Number(pollExpiration);191 }192 pool.subscribeMany(relays, [resultFilter], {193 onevent: handleResultEvent,194 });195};</pre>196 </div>197198 <div class="crt">199 <div class="titlebar">Reference - one vote per pubkey (TypeScript)</div>200<pre><span class="k">const</span> oneVotePerPubkey = (events: Event[]) => {201 <span class="k">const</span> eventMap = <span class="k">new</span> Map<string, Event>();202203 events.forEach((event) => {204 <span class="k">if</span> (205 !eventMap.has(event.pubkey) ||206 event.created_at > eventMap.get(event.pubkey)!.created_at207 ) {208 eventMap.set(event.pubkey, event); <span class="c">// keep the latest vote</span>209 }210 });211212 <span class="k">return</span> Array.from(eventMap.values());213};</pre>214 </div>215216 <div class="box tech">217 <span class="tag">Tech Note</span>218 <p>Kind 1018 sits in the regular storage range of <a href="01.html">NIP-01</a>, 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 <code>created_at</code> is set by the voter, so the "latest vote wins" rule trusts voter timestamps — the relay advice below exists to limit that.</p>219 </div>220221 <h2 class="bar" id="relays"><span class="num">5</span> Relays and Curation</h2>222 <p>Poll results must stay stable after the poll ends. It is advisable for poll authors to use relays that:</p>223 <ul>224 <li>do <strong>not</strong> allow backdated events, and</li>225 <li>do <strong>not</strong> honor <code>kind:5</code> delete requests (<a href="09.html">NIP-09</a>) for vote events.</li>226 </ul>227 <div class="box warn">228 <span class="tag">Warning!</span>229 <p>On a relay that accepts backdated events, a voter can publish a "late" vote with a <code>created_at</code> just before <code>endsAt</code>. On a relay that honors deletes, voters can erase votes after the poll closes. Both break the integrity of a finished poll.</p>230 </div>231 <p>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 <code>kind:30000</code> follow set (<a href="51.html">NIP-51</a>) and fetch results only from the authors in the set — that is the <code>filterPubkeys</code> argument in §4. Clients can also employ other curation algorithms, such as Proof of Work (<a href="13.html">NIP-13</a>) and Web of Trust scores.</p>232233 <h2 class="bar" id="examples"><span class="num">6</span> Worked Example</h2>234 <p>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:</p>235 <div class="crt">236 <div class="titlebar">Example - kind 1068 poll</div>237<pre>{238 <span class="k">"content"</span>: <span class="s">"Pineapple on pizza"</span>,239 <span class="k">"created_at"</span>: 1719888496,240 <span class="k">"id"</span>: <span class="s">"9d1b6b9562e66f2ecf35eb0a3c2decc736c47fddb13d6fb8f87185a153ea3634"</span>,241 <span class="k">"kind"</span>: 1068,242 <span class="k">"pubkey"</span>: <span class="s">"dee45a23c4f1d93f3a2043650c5081e4ac14a778e0acbef03de3768e4f81ac7b"</span>,243 <span class="k">"sig"</span>: <span class="s">"7fa93bf3c430eaef784b0dacc217d3cd5eff1c520e7ef5d961381bc0f014dde6286618048d924808e54d1be03f2f2c2f0f8b5c9c2082a4480caf45a565ca9797"</span>,244 <span class="k">"tags"</span>: [245 [<span class="s">"option"</span>, <span class="s">"qj518h583"</span>, <span class="s">"Yay"</span>],246 [<span class="s">"option"</span>, <span class="s">"gga6cdnqj"</span>, <span class="s">"Nay"</span>],247 [<span class="s">"relay"</span>, <span class="c">"<relay url1>"</span>],248 [<span class="s">"relay"</span>, <span class="c">"<relay url2>"</span>],249 [<span class="s">"polltype"</span>, <span class="s">"singlechoice"</span>],250 [<span class="s">"endsAt"</span>, <span class="c">"<unix timestamp in seconds>"</span>]251 ]252}</pre>253 </div>254 <p>A voter answers with a kind 1018 event. The <code>content</code> stays empty. This is the spec's example response; it carries two <code>response</code> tags, so on a singlechoice poll only the first one counts:</p>255 <div class="crt">256 <div class="titlebar">Example - kind 1018 response</div>257<pre>{258 <span class="k">"content"</span>: <span class="s">""</span>,259 <span class="k">"created_at"</span>: 1720097117,260 <span class="k">"id"</span>: <span class="s">"60a005e32e9596c3f544a841a9bc4e46d3020ca3650d6a739c95c1568e33f6d8"</span>,261 <span class="k">"kind"</span>: 1018,262 <span class="k">"pubkey"</span>: <span class="s">"1bc70a0148b3f316da33fe7e89f23e3e71ac4ff998027ec712b905cd24f6a411"</span>,263 <span class="k">"sig"</span>: <span class="s">"30071a633c65db8f3a075c7a8de757fbd8ce65e3607f4ba287fe6d7fbf839a380f94ff4e826fbba593f6faaa13683b7ea9114ade140720ecf4927010ebf3e44f"</span>,264 <span class="k">"tags"</span>: [265 [<span class="s">"e"</span>, <span class="s">"1fc80cf813f1af33d5a435862b7ef7fb96b47e68a48f1abcadf8081f5a545550"</span>],266 [<span class="s">"response"</span>, <span class="s">"gga6cdnqj"</span>],267 [<span class="s">"response"</span>, <span class="s">"m3agjsdq1"</span>]268 ]269}</pre>270 </div>271 <p>To count, a client connects to the poll relays and requests every vote for the poll:</p>272 <div class="crt">273 <div class="titlebar">Session transcript - counting on wss://relay.example.com</div>274<pre><span class="send">→ ["REQ", "tally", {"kinds":[1018], "#e":["9d1b6b9562e6…"], "until":1720500000}]</span>275<span class="recv">← ["EVENT", "tally", {"kind":1018, "pubkey":"1bc70a01…", "tags":[["e","9d1b…"],["response","gga6cdnqj"]], …}]</span>276<span class="recv">← ["EVENT", "tally", {"kind":1018, "pubkey":"5fe20ab9…", "tags":[["e","9d1b…"],["response","qj518h583"]], …}]</span>277<span class="recv">← ["EVENT", "tally", {"kind":1018, "pubkey":"1bc70a01…", "created_at":1720099000,278 "tags":[["e","9d1b…"],["response","qj518h583"]], …}] <span class="c">// same voter, later — replaces the first</span></span>279<span class="recv">← ["EOSE", "tally"]</span>280<span class="c">-- dedup per pubkey, latest wins: Yay 2, Nay 0 --</span></pre>281 </div>282283 <h2 class="bar" id="history"><span class="num">7</span> Version History</h2>284 <p>NIP-88 had a bumpy childhood: drafted in mid-2024, renumbered, removed, then reinstated in 2025. Notable changes, newest first:</p>285 <ul class="patchnotes">286 <li class="minor"><span class="ver">2025</span><span class="when">2025-04..12</span>Typo, title, and link fixes across the repository touched this file.</li>287 <li><span class="ver">2025</span><span class="when">2025-01</span>Reinstated as NIP-88 with responses as regular kind 1018 events (PR #1507).</li>288 <li><span class="ver">2024</span><span class="when">2024-09</span>fiatjaf removed the polls NIP from the repository, temporarily.</li>289 <li><span class="ver">2024</span><span class="when">2024-09</span>Renumbered to 88. Alex Gleason briefly made the poll response a parameterized replaceable event; the reinstated version reverted this.</li>290 <li><span class="ver">2024</span><span class="when">2024-07</span>Draft finalized; the settings tag was removed in fine-tuning.</li>291 <li><span class="ver">Origin</span><span class="when">2024-06</span>abhay-raizada published the first draft, "Polls On Nostr".</li>292 </ul>293294 <div class="credits">295 <h3 class="cardhead">Original Proposal - Credits</h3>296 <table>297 <tr><td>Author</td><td>abhay-raizada</td></tr>298 <tr><td>First commit</td><td>2024-06-25 — “Polls On Nostr” (renumbered to 88 on 2024-09-17)</td></tr>299 <tr><td>Reinstated</td><td>2025-01-14 — <a href="https://github.com/nostr-protocol/nips/pull/1507">PR #1507</a></td></tr>300 <tr><td>Status</td><td>draft - optional</td></tr>301 <tr><td>Source</td><td><a href="https://github.com/nostr-protocol/nips/blob/master/88.md">88.md on GitHub</a> - <a href="https://github.com/nostr-protocol/nips/commits/master/88.md">full change history</a></td></tr>302 </table>303 </div>304305 <h2 class="bar"><span class="num">8</span> See Also</h2>306 <ul>307 <li><a href="01.html">NIP-01</a> — the event format, the <code>e</code> tag, and the <code>#e</code>/<code>until</code> filters used to fetch votes.</li>308 <li><a href="09.html">NIP-09</a> — kind 5 delete requests; poll relays should not honor them for votes.</li>309 <li><a href="51.html">NIP-51</a> — kind 30000 follow sets, used to curate whose votes count.</li>310 <li><a href="13.html">NIP-13</a> — Proof of Work, one of the suggested sybil-resistance curation tools.</li>311 <li><a href="25.html">NIP-25</a> — reactions; the other lightweight "express an opinion on an event" scheme.</li>312 </ul>313 </div>314315 <aside>316 <div class="cartridge">317 <div class="inner">318 <h3 class="cardhead">Spec Card</h3>319 <div class="lights"><span class="led on"></span><span class="led on"></span><span class="led on"></span></div>320 <dl>321 <dt>Status</dt><dd><span class="chip draft">draft</span><span class="chip optional">optional</span></dd>322 <dt>Defines</dt><dd>poll + response events - option/relay/polltype/endsAt/response tags</dd>323 <dt>Kinds</dt><dd>1068 (poll) - 1018 (response)</dd>324 <dt>Requires</dt><dd><a href="01.html">NIP-01</a></dd>325 <dt>Uses</dt><dd><a href="51.html">NIP-51</a> follow sets - <a href="13.html">NIP-13</a> PoW (curation)</dd>326 <dt>First commit</dt><dd>2024-09-17</dd>327 <dt>Last updated</dt><dd>2025-12-30</dd>328 </dl>329 </div>330 </div>331 </aside>332 </main>333334 <nav class="navbtns">335 <a href="87.html">◀ NIP-87: Cashu and Fedimint Discoverability</a>336 <a href="89.html">NIP-89: Recommended Application Handlers ▶</a>337 </nav>338339 <footer class="pagefoot">340 <a href="../index.html">Nostr Protocol Manual</a>341 <span class="pageno">NIP 88 / F4</span>342 </footer>343</div>344345<script src="../assets/webmcp.js" defer></script>346</body>347</html>