<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>nostr-core Blog</title>
    <link>https://nostr-core.netlify.app/blog/</link>
    <description>Thoughts on building with Nostr, from the team behind nostr-core.</description>
    <language>en</language>
    <lastBuildDate>Mon, 17 Aug 2026 19:00:00 GMT</lastBuildDate>
    <atom:link href="https://nostr-core.netlify.app/blog/feed.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>Connections That Heal Themselves</title>
      <link>https://nostr-core.netlify.app/blog/21-connections-that-heal-themselves.html</link>
      <guid isPermaLink="true">https://nostr-core.netlify.app/blog/21-connections-that-heal-themselves.html</guid>
      <pubDate>Mon, 17 Aug 2026 19:00:00 GMT</pubDate>
      <description>Laptops sleep. NATs time out. Relays restart. Your subscriptions no longer die with the socket.</description>
      <enclosure url="https://nostr-core.netlify.app/headers/blog-21-reconnect.png" length="260809" type="image/png"/>
      <content:encoded><![CDATA[<p align="center">
  <img src="https://nostr-core.netlify.app/headers/blog-21-reconnect.svg" alt="Connections That Heal Themselves" width="100%">
</p>
<h1 id="connections-that-heal-themselves" tabindex="-1">Connections That Heal Themselves </h1>
<p><strong>Laptops sleep. NATs time out. Relays restart. Your subscriptions no longer die with the socket.</strong></p>
<hr>
<h2 id="the-feed-that-quietly-died" tabindex="-1">The Feed That Quietly Died </h2>
<p>This one arrived as a bug report, issue #62, and it describes an afternoon most Nostr builders have lived through. A dashboard on a screen, subscribed to a few relays, working perfectly. Someone closes the laptop for lunch. The machine sleeps, the WebSocket dies, and here is the cruel part: nothing errors. No exception, no red text, no rejected promise. The subscriptions are simply gone, and the app sits there looking healthy, showing a feed that stopped at 12:47.</p>
<p>WebSockets close for a living. Laptop sleep, a NAT or proxy that drops idle connections, a relay deploy on the other end. None of these are exceptional events, and until this release, any one of them permanently killed every live subscription on the connection. The only cure was a reload, applied by a confused user.</p>
<h2 id="reconnect-is-the-default-now" tabindex="-1">Reconnect Is the Default Now </h2>
<p>As of this release, a dropped relay connection retries with exponential backoff and jitter, and when the socket comes back, every open subscription is replayed with its original id and filters. This is on by default, on <code>Relay</code> and on every relay a <code>RelayPool</code> manages, because a connection that stays down was never the behavior anyone wanted.</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { Relay } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'nostr-core'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> relay</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Relay</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'wss://relay.damus.io'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  reconnect: { initialDelay: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">500</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, maxDelay: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">15_000</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, maxAttempts: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">20</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">relay.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">ondisconnect</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">reason</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> showOffline</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(reason)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">relay.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">onreconnect</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> () </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> showLive</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()   </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// subscriptions already replayed</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">relay.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">onreconnectfailed</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">err</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> showGaveUp</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(err)</span></span></code></pre>
</div><p>Every knob has a sane default: start at a second, double up to thirty, add jitter so a relay restart doesn't get a synchronized stampede of every client it dropped, retry forever unless told otherwise. Most apps will never pass a single option and never think about this again. The callbacks exist for the apps that want an offline indicator instead of a mystery.</p>
<h2 id="replay-with-eyes-open" tabindex="-1">Replay, With Eyes Open </h2>
<p>The details are where reconnect logic earns or loses your trust, so here is exactly what happens.</p>
<p>The open subscriptions survive the drop, which is the point: those filters are precisely what must be replayed. A replayed REQ is a fresh REQ, though, so the relay resends matching history and <code>oneose</code> fires again. Dedupe incoming events by id, the same guidance that already applies when you subscribe across multiple relays.</p>
<p>Publishes are the opposite case, and they stay honest: an in-flight publish on a dead socket rejects immediately. A publish is a one-shot request, and pretending it might still succeed would just move the failure somewhere harder to see.</p>
<p>Two boundaries keep the behavior predictable. An initial <code>connect()</code> that fails still rejects, no silent background retrying against a relay that was never reachable; retries only apply to connections that had actually opened. And <code>close()</code> means closed: it cancels any pending retry and shuts the subscriptions down. Calling <code>subscribe()</code> while a retry is pending queues the REQ and fires it on reconnect, so your startup code doesn't need to care what state the socket is in.</p>
<h2 id="why-not-just-ping" tabindex="-1">Why Not Just Ping </h2>
<p>The keepalive question comes up every time. Browsers cannot send WebSocket ping frames, full stop, so client-side keepalive is not available where most Nostr apps run. Reconnect-on-close is not the workaround, it is the remedy: accept that sockets die, and make death boring.</p>
<p>A while back we wrote that Nostr needs boring infrastructure. This is the most boring feature we have ever shipped, and we mean that as high praise. Nobody will screenshot it. Nobody will notice it. Their feed will just still be moving when they get back from lunch.</p>
<hr>
<p><strong><a href="https://github.com/nostr-core-org/nostr-core" target="_blank" rel="noreferrer">GitHub</a></strong> · <strong><a href="https://nostr-core.netlify.app/api/relay.html">Relay API</a></strong> · <strong><a href="https://nostr-core.netlify.app/api/pool.html">RelayPool API</a></strong></p>
]]></content:encoded>
    </item>
    <item>
      <title>Validate Before You Trust</title>
      <link>https://nostr-core.netlify.app/blog/20-validate-before-you-trust.html</link>
      <guid isPermaLink="true">https://nostr-core.netlify.app/blog/20-validate-before-you-trust.html</guid>
      <pubDate>Mon, 17 Aug 2026 19:00:00 GMT</pubDate>
      <description>TypeScript ends at compile time. Schema checks shape at the edges. Policy judges what gets through.</description>
      <enclosure url="https://nostr-core.netlify.app/headers/blog-20-validate.png" length="267725" type="image/png"/>
      <content:encoded><![CDATA[<p align="center">
  <img src="https://nostr-core.netlify.app/headers/blog-20-validate.svg" alt="Validate Before You Trust" width="100%">
</p>
<h1 id="validate-before-you-trust" tabindex="-1">Validate Before You Trust </h1>
<p><strong>TypeScript ends at compile time. Schema checks shape at the edges. Policy judges what gets through.</strong></p>
<hr>
<h2 id="the-bug-three-layers-deep" tabindex="-1">The Bug Three Layers Deep </h2>
<p>The crash is in your rendering code. A <code>toLowerCase</code> on something that turned out to be a number, at the bottom of a stack trace that has nothing to do with the cause. The cause is that twenty minutes ago, some relay sent an event with <code>kind</code> as a string, your handler passed it along because TypeScript said the type was <code>NostrEvent</code>, and the lie traveled three layers before it hit something that cared.</p>
<p>Every type annotation on network data is a promise your compiler cannot keep. A relay, a wire message, a pasted JSON blob: TypeScript describes what they should be and has no opinion about what they are. This release adds the two modules that close that gap, one for shape and one for judgment.</p>
<h2 id="schema-shape-at-the-edges" tabindex="-1">Schema: Shape at the Edges </h2>
<p>Every validator in the <code>schema</code> namespace answers the same three ways: <code>is</code> for a type guard, <code>safeParse</code> when you want to know why, <code>parse</code> when you want to throw.</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { schema } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'nostr-core'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">socket.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">onmessage</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">ev</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> frame</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> schema.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">json</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(schema.relayMessage).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">safeParse</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(ev.data)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">frame.ok) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">return</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">   // garbage stays at the socket, where it arrived</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">  handle</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(frame.value)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> result</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> schema.nostrEvent.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">safeParse</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(incoming)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">result.ok) console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">warn</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(result.issues)</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// [{ path: 'id', message: 'expected 64 lowercase hex characters' }]</span></span></code></pre>
</div><p>Issues come with a dotted path to the offending field, so the error message points at the actual problem instead of the eventual crash site. The filter validator even rejects unknown keys, which catches the classic silent typo: <code>kind</code> where you meant <code>kinds</code>, a filter that matches everything, and a very confusing afternoon.</p>
<h2 id="the-cache-that-lied" tabindex="-1">The Cache That Lied </h2>
<p>Here is a bug from our own codebase, because this module exists for a reason. nostr-core caches signature verification on an internal symbol, so an event checked once is not checked again. Sensible. Except JavaScript's spread operator copies own symbols. Which means this:</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> tampered</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">...</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">signedEvent, content: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'changed'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }</span></span></code></pre>
</div><p>produces an event with different content and a stowaway flag still reading &quot;already verified&quot;. Every spread of a verified event inherited a passed check it never took.</p>
<p>The fix ships in this release on two levels. <code>schema.verifiedNostrEvent</code> and the policy <code>requireValidSignature()</code> always verify from scratch, deliberately ignoring the cache: their whole job is judging untrusted input, so they must not take the event's word for it. And the new <code>verifyEventSignature</code> gives you the uncached primitive directly. The cache is still there for the hot path where you control the objects. At the trust boundary, nobody rides the cache.</p>
<h2 id="policy-judgment-after-shape" tabindex="-1">Policy: Judgment After Shape </h2>
<p>A well-formed event can still be spam, a duplicate, expired, or from someone you blocked. Those are judgment calls, and the <code>policy</code> module makes them composable: each policy answers one question, <code>pipe</code> chains them, first rejection wins.</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { policy } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'nostr-core'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Built once, at startup - noDuplicates and rateLimit keep state.</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> ingress</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> policy.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">pipe</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">([</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  policy.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">requireValidSignature</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(),</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  policy.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">createdAtPolicy</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({ maxFutureSeconds: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">900</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }),</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  policy.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">notExpired</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(),</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  policy.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">sizeLimit</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({ maxContentLength: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">64_000</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, maxTags: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">2000</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }),</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  policy.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">noDuplicates</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(),</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  policy.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">rateLimit</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({ max: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">20</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, windowMs: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">60_000</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }),</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  policy.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">blockKeywords</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">([</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'spam'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> /</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">free</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\s</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF">bitcoin</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">/</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">i</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]),</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">])</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> result</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> ingress.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">check</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(event)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">result.accepted) </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">send</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">([</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'OK'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, event.id, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">false</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, result.reason])</span></span></code></pre>
</div><p>Rejections come back NIP-01 style, <code>blocked: spam</code>, ready to put on the wire. Order is your performance dial: cheap timestamp checks first, signature math after, so junk gets discarded before it costs you anything.</p>
<h2 id="the-same-tools-client-side" tabindex="-1">The Same Tools, Client Side </h2>
<p>Nothing here is relay-only. The same pieces make a feed filter with actual opinions:</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> feed</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> policy.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">pipe</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">([</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  policy.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">notExpired</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(),</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  policy.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">anyOf</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">([</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    policy.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">pubkeyAllowList</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(followList),   </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// people I follow, always</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    policy.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">requirePow</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">16</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">),                </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// strangers pay for attention</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  ]),</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  policy.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">blockKeywords</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(mutedWords),</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">])</span></span></code></pre>
</div><p>Follows get through free. Strangers spend NIP-13 proof of work to reach you. Moderation logic stops being a bespoke <code>shouldAccept()</code> that grows a new <code>if</code> every month, and becomes a list you can read aloud.</p>
<p>We once wrote that Nostr needs boring infrastructure. This is what boring looks like at the trust boundary: check the shape, judge the content, and let nothing skip the line because a type annotation vouched for it.</p>
<hr>
<p><strong><a href="https://github.com/nostr-core-org/nostr-core" target="_blank" rel="noreferrer">GitHub</a></strong> · <strong><a href="https://nostr-core.netlify.app/api/schema.html">Schema API</a></strong> · <strong><a href="https://nostr-core.netlify.app/api/policy.html">Policy API</a></strong></p>
]]></content:encoded>
    </item>
    <item>
      <title>Your Settings Follow Your Keys</title>
      <link>https://nostr-core.netlify.app/blog/19-your-settings-follow-your-keys.html</link>
      <guid isPermaLink="true">https://nostr-core.netlify.app/blog/19-your-settings-follow-your-keys.html</guid>
      <pubDate>Mon, 17 Aug 2026 19:00:00 GMT</pubDate>
      <description>Kind 30078 turns relays into a personal database. Preferences on every device, no account system.</description>
      <enclosure url="https://nostr-core.netlify.app/headers/blog-19-app-data.png" length="277674" type="image/png"/>
      <content:encoded><![CDATA[<p align="center">
  <img src="https://nostr-core.netlify.app/headers/blog-19-app-data.svg" alt="Your Settings Follow Your Keys" width="100%">
</p>
<h1 id="your-settings-follow-your-keys" tabindex="-1">Your Settings Follow Your Keys </h1>
<p><strong>Kind 30078 turns relays into a personal database. Preferences on every device, no account system.</strong></p>
<hr>
<h2 id="every-app-grows-a-backend" tabindex="-1">Every App Grows a Backend </h2>
<p>It starts with one support message: &quot;why are my muted words not on my laptop?&quot; Your Nostr client is beautifully serverless, keys in the user's hands, events on relays. And then theme choices, muted words, draft state, and column layouts pile up in local storage, chained to one browser on one machine.</p>
<p>So you do what every app eventually does. You stand up a little sync service. Which needs accounts. Which needs auth, and backups, and a privacy policy, and a monthly bill. All of it to move a JSON blob between two devices owned by the same person, for an app whose entire point was not having a backend.</p>
<p>NIP-78 is the escape hatch: the user already has an identity and already has relays. Let their settings live where their events do.</p>
<h2 id="one-document-one-event" tabindex="-1">One Document, One Event </h2>
<p>Kind 30078 is addressable app data. The <code>d</code> tag names your document, conventionally <code>&lt;app&gt;/&lt;context&gt;</code> so apps stay out of each other's way, and the newest event wins. For anything private there is an encrypted variant, NIP-44 to yourself:</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { nip78 } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'nostr-core'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Save</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> pool.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">publish</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(relays, nip78.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">createEncryptedAppDataJsonEvent</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  'my-app/prefs'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  { theme: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'dark'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, locale: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'en'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, mutedWords: [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'pump'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'airdrop'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  secretKey,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">))</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Load, on any device holding the same keys</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> events</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> pool.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">querySync</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(relays, nip78.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getAppDataFilter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(pubkey, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'my-app/prefs'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">))</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> newest</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> events.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">sort</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">((</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">a</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">b</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> b.created_at </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> a.created_at)[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">]</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> prefs</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> newest </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">?</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nip78.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">parseEncryptedAppDataJson</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(newest, secretKey) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> defaults</span></span></code></pre>
</div><p>That is the whole sync engine. Save is a publish, load is a query, conflict resolution is &quot;newest wins&quot;, and the account system is the keypair the user walked in with.</p>
<h2 id="the-relay-sees-only-the-label" tabindex="-1">The Relay Sees Only the Label </h2>
<p>With the encrypted variant, the content is opaque ciphertext. The one thing that stays public is the <code>d</code> tag itself, and that deserves a moment of thought. <code>mail/folders</code> is a fine label. <code>mail/folders/alice@example.com</code> is a data leak wearing a label's clothes. Name your documents like the filenames are public, because they are.</p>
<h2 id="not-everything-is-a-setting" tabindex="-1">Not Everything Is a Setting </h2>
<p>Two boundaries worth knowing. For append-only data, logs, entries, anything where replacing would destroy history, kind 78 is the regular-event sibling: publish many, tag them your own way. And for data other apps should understand, follow lists, mute lists, bookmarks, use the standardized NIP-51 lists instead. Kind 30078 is deliberately unstandardized, a private drawer rather than a shared shelf. That is its weakness across apps and exactly its strength within yours.</p>
<p>The best backend is the one you never built, never secured, and never got paged about. Your users' settings can follow their keys instead.</p>
<hr>
<p><strong><a href="https://github.com/nostr-core-org/nostr-core" target="_blank" rel="noreferrer">GitHub</a></strong> · <strong><a href="https://nostr-core.netlify.app/api/nip78.html">NIP-78 API</a></strong> · <strong><a href="https://nostr-core.netlify.app/api/nip44.html">NIP-44 Encryption</a></strong></p>
]]></content:encoded>
    </item>
    <item>
      <title>Mail, Minus the Mail Server</title>
      <link>https://nostr-core.netlify.app/blog/18-mail-minus-the-mail-server.html</link>
      <guid isPermaLink="true">https://nostr-core.netlify.app/blog/18-mail-minus-the-mail-server.html</guid>
      <pubDate>Mon, 17 Aug 2026 19:00:00 GMT</pubDate>
      <description>To, Cc, and a Bcc that stays blind by construction. Email-shaped messaging over gift wrap.</description>
      <enclosure url="https://nostr-core.netlify.app/headers/blog-18-nostr-mail.png" length="253684" type="image/png"/>
      <content:encoded><![CDATA[<p align="center">
  <img src="https://nostr-core.netlify.app/headers/blog-18-nostr-mail.svg" alt="Mail, Minus the Mail Server" width="100%">
</p>
<h1 id="mail-minus-the-mail-server" tabindex="-1">Mail, Minus the Mail Server </h1>
<p><strong>To, Cc, and a Bcc that stays blind by construction. Email-shaped messaging over gift wrap.</strong></p>
<hr>
<h2 id="the-last-protocol-standing" tabindex="-1">The Last Protocol Standing </h2>
<p>Email outlived every platform that was supposed to kill it, and it did that on structure alone. A subject line. To for the people who must act, Cc for the people who should know, Bcc for the people nobody else gets to see. Threads that hold together across years. That structure is why contracts, invoices, and board decisions still travel over a protocol from the seventies.</p>
<p>The infrastructure is another story. Anyone who has tried to self-host email knows the ritual: reverse DNS, SPF, DKIM, DMARC, then weeks of pleading with reputation systems anyway. Deliverability is a priesthood, so everyone rents an inbox instead, and the most consequential correspondence in a business sits in a provider's database under a provider's terms.</p>
<p>The experimental mail module in nostr-core keeps email's structure and swaps out its infrastructure: relays instead of mail servers, gift wrap instead of TLS-and-hope.</p>
<h2 id="why-dms-don-t-cut-it" tabindex="-1">Why DMs Don't Cut It </h2>
<p>The obvious question: Nostr already has private messages, why not use those? Because NIP-17 models a conversation between equals, and email's whole value is that recipients are not equal. To, Cc, and Bcc are different roles, and Bcc must be invisible to everyone else on the message. There is no way to say that in a DM. That asymmetry is the reason this is a separate module rather than a NIP-17 extension.</p>
<h2 id="one-message-many-envelopes" tabindex="-1">One Message, Many Envelopes </h2>
<p>A mail message is a rumor inside a NIP-59 gift wrap: the relay sees an anonymous kind 1059 envelope and nothing else. Not the subject, not the sender, not who is on the thread. <code>createMailMessage</code> produces one sealed copy per recipient, plus one for your own sent folder:</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { mail } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'nostr-core'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> copies</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> mail.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">createMailMessage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  subject: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'Q3 planning'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  body: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'Agenda attached.'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  to: [bobPubkey],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  cc: [carolPubkey],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  bcc: [davePubkey],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}, aliceSecretKey)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 4 copies: bob (to), carol (cc), dave (bcc), alice (sender)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> copy</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> of</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> copies) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> pool.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">publish</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">inboxRelaysFor</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(copy.recipient), copy.wrap)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
</div><p>Each copy goes to that recipient's kind 10050 DM relay list, the same delivery mechanism NIP-17 uses. Replies come with the etiquette built in: <code>createReply</code> flips the recipients, prefixes <code>Re:</code> exactly once, and carries the thread id forward. <code>replyAll</code> keeps the To and Cc lists. Bcc is never carried into a reply, because the reply-all disaster is a solved problem when the software refuses to create it.</p>
<h2 id="a-bcc-that-cannot-leak" tabindex="-1">A Bcc That Cannot Leak </h2>
<p>Classic email keeps Bcc secret by politely stripping a header, and every mail admin has a story about the time that went wrong. Here the secrecy is structural. Blind recipients are never tagged in anyone's copy. Each one receives their own rumor, with its own id, whose recipient list names only themselves. The To and Cc copies contain no trace that a blind copy exists, and only the sender's own copy records the full list.</p>
<p>There is nothing to strip and nothing to leak. A recipient cannot reconstruct the blind list, and neither can a relay, because the information was never in their copy to begin with. Privacy by construction, not by convention.</p>
<h2 id="attachments-the-host-never-reads" tabindex="-1">Attachments the Host Never Reads </h2>
<p>Attachments are encrypted with a fresh AES-256-GCM key, uploaded to a Blossom server, and referenced by hash. The decryption key travels only inside the gift wrap:</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> attachment</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> mail.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">uploadMailAttachment</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  { data: pdfBytes, name: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'agenda.pdf'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, mime: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'application/pdf'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> },</span></span>
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">  'https://blossom.example'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  aliceSecretKey,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span></code></pre>
</div><p>The file host stores ciphertext it cannot open, addressed by content, replicable to any other Blossom server. Your attachments get the same deal your messages do: the infrastructure carries them without being trusted with them.</p>
<h2 id="a-proposal-not-a-standard" tabindex="-1">A Proposal, Not a Standard </h2>
<p>Honesty section. There is no ratified NIP for mail on Nostr. The inner rumor kind, 1314, is provisional and overridable per call, chosen to dodge kind 1301, which is already taken by workout records and, awkwardly, also used by another mail experiment. The transport is entirely standard gift wrap, so nothing about your messages is stranded if the schema moves. But real interoperability needs a NIP and agreement between implementations, and we'd rather say that plainly than ship a private format wearing a standard's clothes.</p>
<p>Email's structure earned fifty years of trust. Its infrastructure is what everyone pays to avoid. Keep the first, drop the second, and your correspondence belongs to the keys that signed it.</p>
<hr>
<p><strong><a href="https://github.com/nostr-core-org/nostr-core" target="_blank" rel="noreferrer">GitHub</a></strong> · <strong><a href="https://nostr-core.netlify.app/api/mail.html">Mail API</a></strong> · <strong><a href="https://nostr-core.netlify.app/api/nip59.html">NIP-59 Gift Wrap</a></strong></p>
]]></content:encoded>
    </item>
    <item>
      <title>Book Me on Nostr</title>
      <link>https://nostr-core.netlify.app/blog/17-book-me-on-nostr.html</link>
      <guid isPermaLink="true">https://nostr-core.netlify.app/blog/17-book-me-on-nostr.html</guid>
      <pubDate>Mon, 17 Aug 2026 19:00:00 GMT</pubDate>
      <description>An availability page that is an event. Bookings that are private calendar invites. No subscription in between.</description>
      <enclosure url="https://nostr-core.netlify.app/headers/blog-17-book-me.png" length="255930" type="image/png"/>
      <content:encoded><![CDATA[<p align="center">
  <img src="https://nostr-core.netlify.app/headers/blog-17-book-me.svg" alt="Book Me on Nostr" width="100%">
</p>
<h1 id="book-me-on-nostr" tabindex="-1">Book Me on Nostr </h1>
<p><strong>An availability page that is an event. Bookings that are private calendar invites. No subscription in between.</strong></p>
<hr>
<h2 id="the-meeting-link-business" tabindex="-1">The Meeting-Link Business </h2>
<p>If you sell your time, a booking link is your storefront. Consultants, advisors, coaches, freelancers: &quot;grab a slot&quot; is how the money starts. Which is why an entire industry exists to host a page that says you're free on Wednesday afternoons.</p>
<p>Think about what that industry actually holds: your working hours, your client list, who met you and when, the notes they left while booking. All of it in a company's database, behind a monthly fee, gone if the account is. For the privilege of publishing the least secret information you own, your open calendar slots.</p>
<p>The new scheduling module in nostr-core takes that whole loop and rebuilds it on events you control.</p>
<h2 id="an-availability-page-is-just-an-event" tabindex="-1">An Availability Page Is Just an Event </h2>
<p>Your booking page becomes a kind 31926 addressable event: title, duration, timezone, and weekly rules in plain wall-clock time.</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { scheduling } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'nostr-core'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> availability</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  identifier: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'intro-call'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  title: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'30 min intro'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  timezone: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'Europe/Berlin'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  durationMinutes: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">30</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  rules: [</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    { weekday: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, start: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'09:00'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, end: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'12:00'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> },   </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Mondays</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    { weekday: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">3</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, start: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'14:00'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, end: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'17:00'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> },   </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Wednesdays</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  ],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  bufferAfterMinutes: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">10</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  minNoticeMinutes: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">120</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  maxAdvanceDays: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">60</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> pool.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">publish</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(relays, scheduling.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">createAvailabilityEvent</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(availability, secretKey))</span></span></code></pre>
</div><p>Addressable means it replaces itself. Change your hours, publish again, and every client that reads the page sees the new schedule. There is no dashboard to log into, because there is no account.</p>
<h2 id="slots-that-survive-october" tabindex="-1">Slots That Survive October </h2>
<p>Anyone who has built scheduling software has the same scar, and it's shaped like daylight saving time. Your rule says 09:00 in Berlin. In October, Berlin's UTC offset changes. Naive slot math either shifts your morning by an hour or generates a 02:30 slot on a night when 02:30 happens twice.</p>
<p><code>generateSlots</code> treats rules as local wall-clock times and resolves each date through the IANA zone, using <code>Intl</code> under the hood, so there is no timezone package in your bundle. A 09:00 rule stays 09:00 local on both sides of a transition. A time that doesn't exist on spring-forward day simply produces no slot. Boring, correct, and nobody misses a call in March.</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> now</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> Math.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">floor</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(Date.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">now</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1000</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> slots</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> scheduling.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">generateSlots</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(page, {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  from: now,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  to: now </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 14</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> *</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 86400</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  busy: scheduling.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">bookingsToBusy</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(acceptedBookings),</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span></code></pre>
</div><h2 id="the-booking-itself-is-nobody-s-business" tabindex="-1">The Booking Itself Is Nobody's Business </h2>
<p>Your open slots are public. Who books them is not. A booking request is a NIP-52 calendar event wrapped in NIP-59 gift wrap, so the only thing a relay ever sees is an anonymous kind 1059 envelope. Not the participants, not the time, not the &quot;would like to discuss the integration&quot; note.</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">wraps</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> scheduling.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">createBookingRequest</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  identifier: crypto.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">randomUUID</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(),</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  availabilityAddress: scheduling.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">buildAvailabilityAddress</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(hostPubkey, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'intro-call'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">),</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  host: hostPubkey,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  start: slots[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">].start,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  end: slots[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">].end,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  title: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'30 min intro'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  timezone: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'Europe/Berlin'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}, bookerSecretKey)</span></span></code></pre>
</div><p>You get one wrap for the host and a self-copy for the booker, so the appointment lives in both calendars. Cancellations work the same way, as a declined NIP-52 RSVP that either side can send.</p>
<h2 id="trust-the-page-not-the-request" tabindex="-1">Trust the Page, Not the Request </h2>
<p>One rule we'd underline twice: a booking request contains whatever times the booker chose to put in it. Nothing stops a hand-crafted request for Sunday at midnight. Your availability event is authoritative, the request is a wish. So the host always re-checks:</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> ok</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> scheduling.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">isSlotAvailable</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(page, { start: booking.start, end: booking.end }, {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  busy: scheduling.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">bookingsToBusy</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(acceptedBookings),</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span></code></pre>
</div><p>Same discipline as any payment flow: validate on your side of the wire.</p>
<h2 id="experimental-on-purpose" tabindex="-1">Experimental, on Purpose </h2>
<p>No NIP standardizes appointment scheduling yet, so the availability kind is provisional and overridable per call. Everything else deliberately reuses ratified pieces: bookings are ordinary NIP-52 calendar events, cancellations are ordinary RSVPs, and the transport is standard gift wrap. Your appointments stay readable by any calendar client that speaks NIP-52, whatever happens to the availability kind later.</p>
<p>Your hours were never the secret. The client list was. Now the public part is a signed event and the private part is actually private, and no one bills you monthly for the difference.</p>
<hr>
<p><strong><a href="https://github.com/nostr-core-org/nostr-core" target="_blank" rel="noreferrer">GitHub</a></strong> · <strong><a href="https://nostr-core.netlify.app/api/scheduling.html">Scheduling API</a></strong> · <strong><a href="https://nostr-core.netlify.app/api/nip52.html">NIP-52 Calendar</a></strong></p>
]]></content:encoded>
    </item>
    <item>
      <title>Finding Mints You Can Trust</title>
      <link>https://nostr-core.netlify.app/blog/16-finding-mints-you-can-trust.html</link>
      <guid isPermaLink="true">https://nostr-core.netlify.app/blog/16-finding-mints-you-can-trust.html</guid>
      <pubDate>Mon, 17 Aug 2026 19:00:00 GMT</pubDate>
      <description>Your follows already know which mints are good. NIP-87 lets you ask them instead of a hardcoded list.</description>
      <enclosure url="https://nostr-core.netlify.app/headers/blog-16-mint-discovery.png" length="271683" type="image/png"/>
      <content:encoded><![CDATA[<p align="center">
  <img src="https://nostr-core.netlify.app/headers/blog-16-mint-discovery.svg" alt="Finding Mints You Can Trust" width="100%">
</p>
<h1 id="finding-mints-you-can-trust" tabindex="-1">Finding Mints You Can Trust </h1>
<p><strong>Your follows already know which mints are good. NIP-87 lets you ask them instead of a hardcoded list.</strong></p>
<hr>
<h2 id="the-list-nobody-chose" tabindex="-1">The List Nobody Chose </h2>
<p>Every ecash wallet ships with a default mint list. Three entries, maybe five, picked by whoever cut the release. And defaults are destiny: most users tap the first entry and never think about it again. Multiply that across a few popular wallets and you get the quiet failure mode of ecash, a decentralized bearer instrument where half the network sits on the same two mints because a config file said so.</p>
<p>The mint question is the ecash question. A mint holds real money against the tokens it issues. Choosing one is a trust decision, and trust decisions don't belong in a hardcoded array.</p>
<h2 id="mints-announce-people-vouch" tabindex="-1">Mints Announce, People Vouch </h2>
<p>NIP-87 splits discovery into two halves. Mints publish announcements: kind 38172 for Cashu, kind 38173 for Fedimint, carrying the URLs, the supported NUTs or modules, and the network. The <code>d</code> tag is the mint's own pubkey, so a mint keeps a stable identity even when announcements get mirrored around.</p>
<p>The second half is the part that matters: kind 38000 recommendations, published by people. Someone who has used a mint for a year can say so, in a signed event, with a free-form review attached. And because you know who signed it, you can weigh it the way you weigh everything else on Nostr: by whether you trust the author.</p>
<p>So discovery becomes a query against your own social graph:</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { nip87 } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'nostr-core'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> recs</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> pool.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">querySync</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(relays, nip87.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getMintRecommendationFilter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  authors: followList,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  kind: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">38172</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}))</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> counts</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nip87.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">tallyRecommendations</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(recs)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> ranked</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">...</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">counts].</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">sort</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">((</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">a</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">b</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> b[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">-</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> a[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">])</span></span></code></pre>
</div><p><code>tallyRecommendations</code> counts distinct people per mint, not raw events. Someone who republishes their endorsement fifty times still counts once.</p>
<h2 id="endorsements-you-can-take-back" tabindex="-1">Endorsements You Can Take Back </h2>
<p>Recommendations are addressable, and that is not a technicality. Trust changes. A mint that ran flawlessly for two years gets sold, or gets slow, or gets weird about withdrawals. With addressable events, your endorsement is a living document: publish a new kind 38000 with the same identifier and the old one is gone. Withdraw it entirely and you stop vouching. The tally always reflects what people believe now, not what they believed once.</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> rec</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nip87.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">createMintRecommendation</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  identifier: mintPubkey,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  recommendedKind: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">38172</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  connections: [{ url: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'https://cashu.example.com'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, label: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'cashu'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  content: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'Been using this for a year, no issues.'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}, secretKey)</span></span></code></pre>
</div><h2 id="don-t-query-the-firehose" tabindex="-1">Don't Query the Firehose </h2>
<p>There is a tempting shortcut: skip the recommendations and query kind 38172 directly. Here is every mint on the network, pick one. Don't. Anyone can publish an announcement, and an announcement is just a claim. Querying announcements raw bypasses the web of trust entirely, and pointing a user at a malicious mint is about the worst thing a wallet can do. Start from people, follow their <code>a</code> tag relay hints to the announcements, and treat an unvouched mint as exactly that.</p>
<h2 id="where-this-slots-in" tabindex="-1">Where This Slots In </h2>
<p>If you read the nutzap post, you saw that a kind 10019 event lists the mints you accept ecash from. NIP-87 is how that list gets filled with something better than guesswork. Your wallet queries your follows, ranks what comes back, shows you who vouched and what they said. The trust decision stays yours. It just stops being blind.</p>
<p>Hardcoded lists were how ecash bootstrapped. Social discovery is how it grows up.</p>
<hr>
<p><strong><a href="https://github.com/nostr-core-org/nostr-core" target="_blank" rel="noreferrer">GitHub</a></strong> · <strong><a href="https://nostr-core.netlify.app/api/nip87.html">NIP-87 API</a></strong> · <strong><a href="https://nostr-core.netlify.app/api/nip61.html">NIP-61 Nutzaps</a></strong></p>
]]></content:encoded>
    </item>
    <item>
      <title>The Zap That Is Already Money</title>
      <link>https://nostr-core.netlify.app/blog/15-the-zap-that-is-already-money.html</link>
      <guid isPermaLink="true">https://nostr-core.netlify.app/blog/15-the-zap-that-is-already-money.html</guid>
      <pubDate>Mon, 17 Aug 2026 19:00:00 GMT</pubDate>
      <description>No invoice. No callback. No receipt to take on faith. The event carries the cash.</description>
      <enclosure url="https://nostr-core.netlify.app/headers/blog-15-nutzaps.png" length="299853" type="image/png"/>
      <content:encoded><![CDATA[<p align="center">
  <img src="https://nostr-core.netlify.app/headers/blog-15-nutzaps.svg" alt="The Zap That Is Already Money" width="100%">
</p>
<h1 id="the-zap-that-is-already-money" tabindex="-1">The Zap That Is Already Money </h1>
<p><strong>No invoice. No callback. No receipt to take on faith. The event carries the cash.</strong></p>
<hr>
<h2 id="a-zap-is-a-promise" tabindex="-1">A Zap Is a Promise </h2>
<p>Walk through what actually happens when someone zaps you 21 sats. Their client asks your lightning address for an invoice. That means an LNURL server, run by you or more likely by your custodial wallet, has to be online and answering at that exact moment. Their wallet pays the invoice. Then that same server writes a kind 9735 receipt, publishes it, and every client on the network treats the receipt as proof.</p>
<p>The money moved over Lightning. The proof is a signed statement from a server. Those are two different things, and the gap between them is trust. If the wallet provider miscounts, backdates, or simply invents receipts, the zap totals under a note are fiction. Most days everything is fine. But a zap remains a promise that a payment happened somewhere else.</p>
<p>There is a second cost that creators feel more than anyone: the whole flow depends on your infrastructure being up when the sender presses the button. Custodian down for maintenance at the moment your note takes off? Those tips just don't happen.</p>
<h2 id="the-nutzap-flips-it" tabindex="-1">The Nutzap Flips It </h2>
<p>NIP-61 puts the payment inside the event. A nutzap is a kind 9321 note carrying Cashu proofs: ecash minted at a mint you said you trust, locked to a key only you control. Whoever sees the event sees the money. There is no receipt to check against the payment, because there is no gap between them. The payment is the receipt.</p>
<p>Your half of the deal is one published event, kind 10019, telling the world how to pay you:</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { nip61 } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'nostr-core'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> info</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nip61.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">createNutzapInfoEvent</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  relays: [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'wss://relay.example.com'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  mints: [{ url: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'https://stablenut.umint.cash'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, units: [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'sat'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] }],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  p2pkPubkey: walletPubkey,   </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// the NIP-60 wallet key, never your identity key</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}, secretKey)</span></span></code></pre>
</div><p>One detail that matters and that we made hard to get wrong: the lock key comes from your NIP-60 wallet event, not from your Nostr identity. Cashu wants 33-byte keys where Nostr uses 32, so <code>toP2PKLockKey</code> adds the required prefix for you, and it's idempotent if the key already has one.</p>
<h2 id="sending-one" tabindex="-1">Sending One </h2>
<p>The sender reads your kind 10019, mints proofs at one of your listed mints with the Cashu library of their choice, locks them to your key, and publishes to the relays you named:</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> [</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">infoEvent</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> pool.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">querySync</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(relays, nip61.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getNutzapInfoFilter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(bobPubkey))</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> info</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nip61.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">parseNutzapInfo</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(infoEvent)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> proofs</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> mintP2PKLocked</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(info.mints[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">].url, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">21</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, info.p2pkPubkey)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> pool.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">publish</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(info.relays, nip61.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">createNutzapEvent</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  proofs,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  mint: info.mints[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">].url,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  recipient: bobPubkey,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  eventId: likedNoteId,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  eventKind: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  content: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'great post'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}, senderSecretKey))</span></span></code></pre>
</div><p>nostr-core does the protocol part: the events, the tags, the key handling. Minting and swapping stay in your Cashu library, where they belong.</p>
<h2 id="verify-before-you-thank" tabindex="-1">Verify Before You Thank </h2>
<p>Because the money travels in the event, anyone can audit a nutzap offline. <code>verifyNutzap</code> checks that the proofs come from a mint you actually listed, that every proof is P2PK-locked to your published key, and that the event names you as the recipient:</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">valid</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">errors</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nip61.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">verifyNutzap</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(nutzap, info, myPubkey)</span></span></code></pre>
</div><p>A proof with a plain secret is spendable by anyone, so it never counts as valid. The one check that needs the mint's keyset, DLEQ verification, belongs to your Cashu library. Run it before treating a nutzap as spendable.</p>
<h2 id="claim-once-not-twice" tabindex="-1">Claim Once, Not Twice </h2>
<p>Sweeping a nutzap into your wallet is a swap at the mint, and the same token must never be swapped twice. So every redemption gets recorded as a kind 7376 history event: the claimed nutzap id in plaintext, the amounts encrypted to yourself. On startup, <code>getRedeemedNutzapIds</code> tells you what's already claimed, and the newest history timestamp becomes the <code>since</code> for your next query. Publish the redemption to the sender's relays too. That is how their client shows the tip landed.</p>
<h2 id="for-the-people-running-the-numbers" tabindex="-1">For the People Running the Numbers </h2>
<p>A zap needs the recipient's Lightning stack online at send time. A nutzap needs a relay. For a creator, that is a tip jar with no uptime requirement. For an agent, it means getting paid without owning any Lightning infrastructure at all: a NIP-60 wallet is just encrypted events, and incoming money is just more events to parse.</p>
<p>The honest trade: you are trusting the mints you list. Choose them with care, and cap what sits in any one of them. How to choose them well is its own story, and NIP-87 has an answer.</p>
<p>A zap was applause with a receipt stapled to it. A nutzap is the coin itself, sitting in your feed, locked so only you can pick it up.</p>
<hr>
<p><strong><a href="https://github.com/nostr-core-org/nostr-core" target="_blank" rel="noreferrer">GitHub</a></strong> · <strong><a href="https://nostr-core.netlify.app/api/nip61.html">NIP-61 API</a></strong> · <strong><a href="https://nostr-core.netlify.app/api/nip60.html">NIP-60 Wallet</a></strong></p>
]]></content:encoded>
    </item>
    <item>
      <title>Your Photos, Your Keys</title>
      <link>https://nostr-core.netlify.app/blog/14-your-photos-your-keys.html</link>
      <guid isPermaLink="true">https://nostr-core.netlify.app/blog/14-your-photos-your-keys.html</guid>
      <pubDate>Tue, 02 Jun 2026 15:04:33 GMT</pubDate>
      <description>Picture-first feeds come to Nostr. Kind 20, full metadata, no platform in the middle.</description>
      <enclosure url="https://nostr-core.netlify.app/headers/blog-14-your-photos-your-keys.png" length="249810" type="image/png"/>
      <content:encoded><![CDATA[<p align="center">
  <img src="https://nostr-core.netlify.app/headers/blog-14-your-photos-your-keys.svg" alt="Your Photos, Your Keys" width="100%">
</p>
<h1 id="your-photos-your-keys" tabindex="-1">Your Photos, Your Keys </h1>
<p><strong>Picture-first feeds come to Nostr. Kind 20, full metadata, no platform in the middle.</strong></p>
<hr>
<h2 id="the-algorithm-you-never-hired" tabindex="-1">The Algorithm You Never Hired </h2>
<p>Every Instagram user knows the feeling. You post something you're proud of, and somewhere a ranking model decides who sees it, when, and whether it sinks under sponsored posts. You never agreed to that. You just accepted it.</p>
<p>NIP-68 describes another way: picture-first posts that live on the protocol, not on a platform. As of today, nostr-core ships a full implementation.</p>
<h2 id="the-post-is-the-event" tabindex="-1">The Post Is the Event </h2>
<p>A photo post is a single self-contained event, kind 20. It carries everything a visual feed needs: the image URL, its type, a SHA-256 hash so nobody can swap the file, pixel dimensions, a blurhash for smooth loading, and fallback mirrors in case the main host goes down. All of it rides inside one <code>imeta</code> tag. Add more images, get more tags, and one event becomes a multi-photo story.</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { nip68 } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'nostr-core'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> post</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nip68.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">createPictureEvent</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  title: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'Costa Rica'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  description: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'Sunset over the coast'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  images: [{</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    url: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'https://nostr.build/i/photo.jpg'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    mimeType: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'image/jpeg'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    alt: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'Coastline at sunset'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    dim: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'3024x4032'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">    annotations: [{ pubkey: friendPk, x: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1200</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, y: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">800</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  hashtags: [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'travel'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'photography'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  location: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'Costa Rica'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  geohash: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'d1q'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}, secretKey)</span></span></code></pre>
</div><p>That produces a signed event, ready to publish. No backend, no account, no content policy to negotiate. It leaves your machine already cryptographically yours.</p>
<h2 id="tag-a-friend-no-middleman" tabindex="-1">Tag a Friend, No Middleman </h2>
<p>Look at the <code>annotations</code> field. It pins a pubkey to pixel coordinates inside the image. Tag a friend at <code>x: 1200, y: 800</code> and any NIP-68 client can draw the dot on the photo and link it straight to their Nostr profile. No platform brokers that connection. The relationship lives in the event.</p>
<p>Place works the same way. A readable location plus a geohash gives any map-aware client spatial context for free. Build a photo map, a local discovery feed, a travel archive. The data is already in the post.</p>
<h2 id="relays-can-filter-without-unpacking" tabindex="-1">Relays Can Filter Without Unpacking </h2>
<p>Each <code>imeta</code> tag mirrors its image type and hash as top-level <code>m</code> and <code>x</code> tags. So a relay can answer &quot;every kind 20 with an <code>image/jpeg</code> from the last week&quot; without parsing the full tag tree. That keeps picture feeds fast at the relay layer, not just the client.</p>
<p>It also plays well with the rest of Nostr. The same <code>buildImetaTag</code> and <code>parseImetaTag</code> helpers read image metadata out of NIP-92 events, and the primitives carry straight over to video feeds (NIP-71, kind 22).</p>
<h2 id="for-builders-and-agents" tabindex="-1">For Builders and Agents </h2>
<p>Reading a feed is just as clean. <code>parsePicturePost(event)</code> returns a tidy object: title, description, every image with its metadata, hashtags, tagged users, location. Feed it to a vision model, a caption generator, a moderation pass, a recommender. No scraping, no pagination tricks, no rate limits. The structure is already there. You just consume it.</p>
<p>Picture what that unlocks. A photo app where the user owns their keys and their posts move between clients without losing a single tag. A portfolio where every image is a signed event, verifiably yours. An assistant that captions and publishes on your behalf.</p>
<h2 id="a-real-first-class-home" tabindex="-1">A Real First-Class Home </h2>
<p>The spec calls them picture-first feeds. What it means is that visual content on Nostr finally has a real home, with all the metadata good tooling needs to build on top of it.</p>
<p>The implementation is in main today. Go build something worth photographing.</p>
<hr>
<p><strong><a href="https://github.com/nostr-core-org/nostr-core" target="_blank" rel="noreferrer">GitHub</a></strong> · <strong><a href="https://nostr-core.netlify.app/api/nip68.html">NIP-68 API</a></strong></p>
]]></content:encoded>
    </item>
    <item>
      <title>Every Blog You've Written, Now on Nostr</title>
      <link>https://nostr-core.netlify.app/blog/13-every-blog-on-nostr.html</link>
      <guid isPermaLink="true">https://nostr-core.netlify.app/blog/13-every-blog-on-nostr.html</guid>
      <pubDate>Tue, 02 Jun 2026 15:04:33 GMT</pubDate>
      <description>Point it at a feed. Get signed long-form events back. Your archive, finally yours.</description>
      <enclosure url="https://nostr-core.netlify.app/headers/blog-13-every-blog-on-nostr.png" length="253891" type="image/png"/>
      <content:encoded><![CDATA[<p align="center">
  <img src="https://nostr-core.netlify.app/headers/blog-13-every-blog-on-nostr.svg" alt="Every Blog You've Written, Now on Nostr" width="100%">
</p>
<h1 id="every-blog-you-ve-written-now-on-nostr" tabindex="-1">Every Blog You've Written, Now on Nostr </h1>
<p><strong>Point it at a feed. Get signed long-form events back. Your archive, finally yours.</strong></p>
<hr>
<h2 id="the-web-remembers-until-it-doesn-t" tabindex="-1">The Web Remembers, Until It Doesn't </h2>
<p>There's a lot of writing scattered across the web. Substack newsletters, WordPress archives, Ghost blogs, Medium posts from before the paywall era. Most of it lives in someone else's database, behind someone else's domain, one policy change away from disappearing.</p>
<p>RSS never left. It got ignored while platforms built their walls. nostr-core puts it back to work: an import module that turns any feed into signed Nostr long-form events, with a single function call.</p>
<h2 id="one-function" tabindex="-1">One Function </h2>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { rss } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'nostr-core'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> events</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> rss.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">importFeedAsDrafts</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'https://yourblog.com/feed/'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  signer,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  limit: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">25</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  draft: { identifierPrefix: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'rss-'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> pool.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">publish</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">([</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'wss://relay.example.com'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">], </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">...</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">events)</span></span></code></pre>
</div><p>That call runs the whole chain. Fetch the feed, detect the format, parse the items, convert each HTML body to Markdown, derive a stable identifier, wrap it as a NIP-23 long-form event, and sign it. You get back signed events. You decide when they go live.</p>
<h2 id="drafts-first-then-public" tabindex="-1">Drafts First, Then Public </h2>
<p>By default each article lands as a kind 30024 draft, not a public post. Nothing surfaces until you choose to publish it. That matters when you're importing a multi-year archive and want to read it over before anything shows up on a relay.</p>
<p>Re-running the importer doesn't create duplicates. Every article gets a deterministic <code>d</code>-tag, a SHA-256 of its guid, link, and title. Import the same feed twice and you still get one clean copy of each post. When you're ready, publish the same article with <code>asDraft: false</code>. It becomes a kind 30023 post and replaces the draft on the relays automatically. Same identifier, no orphans.</p>
<h2 id="three-formats-one-parser" tabindex="-1">Three Formats, One Parser </h2>
<p>Feeds disagree on almost everything. RSS 2.0, Atom, JSON Feed, each with its own shape and its own quirks. <code>parseFeed</code> reads the first character, picks the format, and hands back one normalized result. Dates fall through a chain until something valid sticks. Numeric HTML entities get decoded before conversion. Whatever the feed calls itself, you get title, author, published date, and Markdown content.</p>
<p>Most hosts already expose a feed: WordPress at <code>/feed/</code>, Substack at <code>/feed</code>, Ghost at <code>/rss/</code>, Blogger, Tumblr. Medium too, though it only ships excerpts.</p>
<h2 id="images-that-outlive-the-source" tabindex="-1">Images That Outlive the Source </h2>
<p>Here's the part that matters for an archive. An imported post still points at images on the original host. If that WordPress install goes dark, the pictures break.</p>
<p>Turn on the Blossom option and the importer rehosts every inline image to a content-addressed media server, then rewrites the Markdown to match. In <code>mirror</code> mode the Blossom server pulls each image itself, so it works straight from the browser without CORS headaches.</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> events</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> rss.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">importFeedAsDrafts</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'https://yourblog.com/feed/'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  signer,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  blossom: { servers: [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'https://blossom.primal.net'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">], mode: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'mirror'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span></code></pre>
</div><p>Now the article and its images travel together. The source can disappear. The post survives.</p>
<h2 id="for-agents-a-pipeline-without-infrastructure" tabindex="-1">For Agents, a Pipeline Without Infrastructure </h2>
<p>Point an agent at a feed and it gets signed Nostr events. No database to run, no scraper to babysit, no auth to manage. Each event carries clean metadata: title, author, published date, content in Markdown that's ready to embed or summarize.</p>
<p>An agent that curates a topic across dozens of blogs, one that backfills a writer's full history onto Nostr, one that mirrors a newsletter in real time. All of it is <code>importFeedAsDrafts</code> in a loop.</p>
<h2 id="the-right-idea-finally-housed" tabindex="-1">The Right Idea, Finally Housed </h2>
<p>RSS was always the right idea. Structured, open content that any tool could read. It just never had a publishing layer that matched its spirit. Nostr does. Signed events, cryptographic ownership, relay distribution with no central chokepoint.</p>
<p>Your old writing deserves a better home. Now it has one.</p>
<hr>
<p><strong><a href="https://github.com/nostr-core-org/nostr-core" target="_blank" rel="noreferrer">GitHub</a></strong> · <strong><a href="https://nostr-core.netlify.app/api/rss.html">RSS API</a></strong> · <strong><a href="https://nostr-core.netlify.app/api/nip23.html">NIP-23 Long-form</a></strong></p>
]]></content:encoded>
    </item>
    <item>
      <title>Teaching Agents to Build</title>
      <link>https://nostr-core.netlify.app/blog/12-teaching-agents-to-build.html</link>
      <guid isPermaLink="true">https://nostr-core.netlify.app/blog/12-teaching-agents-to-build.html</guid>
      <pubDate>Thu, 02 Apr 2026 21:47:30 GMT</pubDate>
      <description>AI agents are only as good as the instructions they get. We wrote ten.</description>
      <enclosure url="https://nostr-core.netlify.app/headers/blog-12-teaching-agents.png" length="256618" type="image/png"/>
      <content:encoded><![CDATA[<p align="center">
  <img src="https://nostr-core.netlify.app/headers/blog-12-teaching-agents.svg" alt="Teaching Agents to Build" width="100%">
</p>
<h1 id="teaching-agents-to-build" tabindex="-1">Teaching Agents to Build </h1>
<p><strong>AI agents are only as good as the instructions they get. We wrote ten.</strong></p>
<hr>
<h2 id="agents-have-a-cold-start-problem" tabindex="-1">Agents Have a Cold Start Problem </h2>
<p>You've probably tried it. You open an AI assistant, tell it to &quot;build me a Nostr app with Lightning payments,&quot; and watch it confidently produce code that's half-right, half-hallucinated. The function names look plausible but don't exist. The imports reference packages that were deprecated two years ago. The relay URL is made up.</p>
<p>This isn't a model problem. It's a context problem. The agent doesn't know where to look, what tools are available, or how the pieces fit together. It's guessing -- and guessing doesn't work when you're dealing with cryptographic keys and real money.</p>
<p>We decided to fix that at the source.</p>
<h2 id="what-skills-are-and-why-they-matter" tabindex="-1">What Skills Are (And Why They Matter) </h2>
<p>A skill is a small document that teaches an AI agent how to do one thing well. Not a tutorial. Not a reference manual. A focused set of instructions that gives the agent the right context, the right code patterns, and the right guardrails for a specific task.</p>
<p>Think of it like briefing a new colleague. You wouldn't hand them the entire company wiki. You'd say: &quot;Here's the project. Here's how we do things. Here are the common mistakes. Go.&quot;</p>
<p>That's what each skill does for an AI agent working with nostr-core.</p>
<h2 id="ten-skills-four-layers" tabindex="-1">Ten Skills, Four Layers </h2>
<p>We organized the skills into layers that mirror how you'd actually build something:</p>
<p><strong>Start here.</strong> Before writing any code, you need to know where things are. The Nostr ecosystem spans five specification repositories with four different naming conventions. NIPs for the protocol. LUDs for LNURL. NUTs for Cashu. BUDs for Blossom media. The <code>/navigate-nostr</code> skill teaches agents (and humans) how to find the right spec, the right repo, and the right nostr-core module for any feature.</p>
<p><strong>Set up your identity.</strong> Every Nostr application starts with a keypair. The <code>/nostr-identity</code> skill covers key generation, mnemonic backup, NIP-05 verification, and the three signer options (direct key, browser extension, remote bunker). It handles the part that most tutorials skip: how to manage keys safely.</p>
<p><strong>Connect a wallet.</strong> The <code>/lnbits-mcp</code> skill walks through connecting to LNbits, a free and open-source Lightning wallet. It covers getting an NWC connection string, wiring it up to nostr-core, and optionally setting up the LNbits MCP Server so agents can manage wallets through plain conversation.</p>
<p><strong>Build things.</strong> This is where the bulk of the skills live:</p>
<ul>
<li><code>/nostr-messaging</code> -- private DMs, public notes, chat channels, groups, real-time subscriptions</li>
<li><code>/nostr-social</code> -- threads, reactions, reposts, articles, follow lists, badges, calendar events, zaps</li>
<li><code>/lightning-pay</code> -- every payment pattern: invoices, Lightning Addresses, fiat conversion, keysend</li>
<li><code>/wallet-monitor</code> -- transaction history, live notifications, analytics</li>
<li><code>/lightning-agent</code> -- a complete, copy-paste agent class that ties wallet, messaging, and identity together</li>
</ul>
<p>And for developers who need to go deeper, <code>/nwc-integrate</code> handles generic wallet setup and <code>/nostr-primitives</code> exposes the raw protocol surface across 48 NIPs.</p>
<h2 id="what-this-changes" tabindex="-1">What This Changes </h2>
<p>Without skills, an agent working with Nostr has to figure out everything from scratch. Which NIP covers zaps? What's the import path for gift-wrapped DMs? How do you check if a wallet supports a specific method before calling it? Every question is an opportunity for the agent to guess wrong.</p>
<p>With skills loaded, the same agent has the answers in context. It knows the function signatures, the error types, the security rules. It knows to check <code>getInfo().methods</code> before calling <code>payInvoice</code>. It knows to use NIP-17 for DMs instead of the deprecated NIP-04. It knows to call <code>nwc.close()</code> in error paths.</p>
<p>The difference isn't subtle. It's the difference between generated code that looks right and generated code that works.</p>
<h2 id="designed-for-humans-too" tabindex="-1">Designed for Humans Too </h2>
<p>Here's the thing about good AI instructions: they're also good human instructions.</p>
<p>Every skill is a markdown file you can open and read. The code examples are real, complete, and runnable. The patterns are the same ones you'd use in production. If you're a developer who's never worked with Nostr before, reading through <code>/nostr-identity</code> and <code>/nostr-messaging</code> will teach you the fundamentals faster than most tutorials.</p>
<p>Skills are not hidden configuration files. They're documentation with a specific audience in mind. The AI agent is one audience. You're the other.</p>
<h2 id="no-vendor-no-lock-in" tabindex="-1">No Vendor, No Lock-In </h2>
<p>A deliberate choice: every skill points to open protocols and open-source tools.</p>
<p>The wallet examples use LNbits (free, self-hosted) and standard NWC connection strings that work with any compatible wallet. The relay examples use public relays. The signing examples cover all three options (direct key, browser extension, remote signer) without assuming which one you'll choose.</p>
<p>If you swap LNbits for another NWC provider, the skills still work. If you swap relays, the skills still work. That's what building on protocols instead of products gets you.</p>
<h2 id="try-it" tabindex="-1">Try It </h2>
<p>Install the nostr-core plugin in Claude Code:</p>
<div class="language- vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span>/plugin install nostr-core-org/nostr-core</span></span></code></pre>
</div><p>Then ask your agent to build something. Create a Lightning-enabled bot. Set up an identity. Build a social feed. The skills load automatically based on what you're doing.</p>
<p>Or just read them directly in the <code>skills/</code> directory. They're short, focused, and written to be useful whether you're an AI or a person.</p>
<p>Ten skills. One protocol. Everything an agent needs to build on Nostr.</p>
<hr>
<p><strong><a href="https://github.com/nostr-core-org/nostr-core" target="_blank" rel="noreferrer">GitHub</a></strong> . <strong><a href="https://nostr-core-org.github.io/nostr-core/skills" target="_blank" rel="noreferrer">Skills Documentation</a></strong></p>
]]></content:encoded>
    </item>
    <item>
      <title>Navigate Nostr</title>
      <link>https://nostr-core.netlify.app/blog/11-navigate-nostr.html</link>
      <guid isPermaLink="true">https://nostr-core.netlify.app/blog/11-navigate-nostr.html</guid>
      <pubDate>Thu, 02 Apr 2026 21:47:30 GMT</pubDate>
      <description>Agents don't need more protocol knowledge. They need to know where to look.</description>
      <enclosure url="https://nostr-core.netlify.app/headers/blog-11-navigate-nostr.png" length="265379" type="image/png"/>
      <content:encoded><![CDATA[<p align="center">
  <img src="https://nostr-core.netlify.app/headers/blog-11-navigate-nostr.svg" alt="Navigate Nostr" width="100%">
</p>
<h1 id="navigate-nostr" tabindex="-1">Navigate Nostr </h1>
<p><strong>Agents don't need more protocol knowledge. They need to know where to look.</strong></p>
<hr>
<h2 id="the-problem-isn-t-complexity" tabindex="-1">The Problem Isn't Complexity </h2>
<p>Nostr is a big protocol. Forty-eight NIPs and counting. LNURL has its own spec repo. Cashu has NUTs. Blossom has BUDs. Each one lives in a different repository, follows a different numbering scheme, documents a different piece of the puzzle.</p>
<p>For a human developer, this is manageable. You bookmark the repos, learn the naming conventions, know which README has the event kinds table.</p>
<p>For an AI agent, it's a cold start every time. No bookmarks. No muscle memory. No sense of where to look first.</p>
<p>So the agent guesses. It hallucinates a URL. It confuses a NIP number with an event kind number. It reaches for the wrong spec repo. It wastes context on dead ends.</p>
<p>The protocol is well-documented. The documentation is just scattered across five repositories with four different naming conventions.</p>
<h2 id="a-compass-not-a-map" tabindex="-1">A Compass, Not a Map </h2>
<p>The <code>navigate-nostr</code> skill doesn't try to teach an agent the Nostr protocol. It teaches the agent where the documentation lives and how to find things in it.</p>
<p>That's a subtle but important distinction. A map is static. It goes stale the moment a NIP updates. A compass is a pattern. It tells you:</p>
<ul>
<li>NIPs live here: <code>https://github.com/nostr-protocol/nips/blob/master/{NIP}.md</code></li>
<li>LNURL LUDs live here: <code>https://github.com/lnurl/luds/blob/luds/{LUD}.md</code></li>
<li>Cashu NUTs live here: <code>https://github.com/cashubtc/nuts/blob/main/{NUT}.md</code></li>
<li>Blossom BUDs live here: <code>https://github.com/hzrd149/blossom/blob/master/buds/{BUD}.md</code></li>
</ul>
<p>Four URL patterns. That's the entire Nostr ecosystem, navigable.</p>
<h2 id="what-the-skill-actually-contains" tabindex="-1">What the Skill Actually Contains </h2>
<p>The skill has three layers:</p>
<p><strong>Layer 1: Protocol navigation.</strong> How to find NIPs, how to look up event kinds (hint: there's no NIP-to-kind mapping; you have to check the README's event kinds table), how to discover what already exists before building something new.</p>
<p><strong>Layer 2: Ecosystem pointers.</strong> The LNURL LUDs repo, the BOLT-11 spec, the Cashu NUTs, the Blossom BUDs. Each with a URL pattern and the key documents worth knowing about.</p>
<p><strong>Layer 3: The module map.</strong> A table that connects every nostr-core module to its spec. An agent that knows it needs NIP-57 zaps can look at the table and see: import <code>nip57</code>, read the spec at <code>57.md</code>. No guessing. No searching.</p>
<p>The three layers work together. The agent navigates from &quot;I need to implement zaps&quot; to the right spec and the right import in two lookups.</p>
<h2 id="why-agents-need-this" tabindex="-1">Why Agents Need This </h2>
<p>Every agent framework has the same fundamental problem: the agent knows what it wants to do but not where to find the information to do it correctly.</p>
<p>Consider an agent that needs to send a zap. Without navigation, it has to:</p>
<ol>
<li>Search for &quot;nostr zaps&quot; and hope for good results</li>
<li>Figure out which NIP covers zaps (it's 57, but that's not obvious)</li>
<li>Find the spec URL (is it on GitHub? Which repo? What path?)</li>
<li>Figure out which nostr-core module implements it</li>
<li>Hope it didn't hallucinate any of the above</li>
</ol>
<p>With <code>navigate-nostr</code> loaded, the same agent:</p>
<ol>
<li>Checks the module map: zaps are <code>nip57</code>, spec is NIP-57</li>
<li>Fetches <code>https://github.com/nostr-protocol/nips/blob/master/57.md</code></li>
<li>Imports <code>nip57</code> from <code>nostr-core</code></li>
</ol>
<p>Three steps. No guessing. No hallucination. The skill turns an open-ended search problem into a lookup.</p>
<h2 id="the-pattern" tabindex="-1">The Pattern </h2>
<p>This follows a principle that applies beyond Nostr: agents work better when you give them navigation tools rather than knowledge dumps.</p>
<p>A skill that contains the full text of every NIP would be massive, stale within weeks, and would burn context window on information the agent may never need. A skill that teaches the agent how to fetch the right NIP on demand is small, stays current (the URL patterns don't change), and lets the agent pull exactly what it needs.</p>
<p>Teach agents where to look, not what to remember.</p>
<h2 id="try-it" tabindex="-1">Try It </h2>
<p>If you have nostr-core installed as a Claude Code plugin, the skill is already available:</p>
<div class="language- vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span>/navigate-nostr</span></span></code></pre>
</div><p>Or load it directly from the repo:</p>
<div class="language- vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span>/plugin install nostr-core-org/nostr-core</span></span></code></pre>
</div><p>The skill is a single markdown file. Read it yourself if you want to see what a navigation-first skill looks like. It's in <code>skills/navigate-nostr/SKILL.md</code>.</p>
<p>The protocol is big. The compass is small. That's the point.</p>
<hr>
<p><strong><a href="https://github.com/nostr-core-org/nostr-core" target="_blank" rel="noreferrer">GitHub</a></strong> . <strong><a href="https://nostr-core-org.github.io/nostr-core/skills" target="_blank" rel="noreferrer">Skills Documentation</a></strong></p>
]]></content:encoded>
    </item>
    <item>
      <title>Media Without Middlemen</title>
      <link>https://nostr-core.netlify.app/blog/10-media-without-middlemen.html</link>
      <guid isPermaLink="true">https://nostr-core.netlify.app/blog/10-media-without-middlemen.html</guid>
      <pubDate>Thu, 19 Mar 2026 19:37:54 GMT</pubDate>
      <description>Upload, retrieve, mirror, delete. Content-addressed blobs on Blossom servers, authenticated with your Nostr key.</description>
      <enclosure url="https://nostr-core.netlify.app/headers/blog-10-media-without-middlemen.png" length="156883" type="image/png"/>
      <content:encoded><![CDATA[<p align="center">
  <img src="https://nostr-core.netlify.app/headers/blog-10-media-without-middlemen.svg" alt="Media Without Middlemen" width="100%">
</p>
<h1 id="media-without-middlemen" tabindex="-1">Media Without Middlemen </h1>
<p><strong>Upload, retrieve, mirror, delete. Content-addressed blobs on Blossom servers, authenticated with your Nostr key.</strong></p>
<hr>
<h2 id="your-media-shouldn-t-depend-on-one-server" tabindex="-1">Your Media Shouldn't Depend on One Server </h2>
<p>Every image you post on a centralized platform lives at their URL, on their servers, under their rules. The link breaks when they change their infrastructure. The file disappears when they decide it should.</p>
<p>Blossom is different. Files are identified by their SHA-256 hash. The same file on any Blossom server has the same address. If one server goes down, any other server with the file can serve it. Content-addressed, decentralized, no single point of failure.</p>
<p>nostr-core speaks Blossom natively.</p>
<h2 id="what-you-can-do" tabindex="-1">What You Can Do </h2>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { blossom } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'nostr-core'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Upload</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> auth</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> blossom.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">createAuthEvent</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  action: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'upload'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  content: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'Upload image'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  expiration: Math.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">floor</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(Date.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">now</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1000</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">+</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 300</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}, secretKey)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> blob</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> blossom.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">uploadBlob</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'https://blossom.example'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, imageData, auth, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'image/png'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// blob.url, blob.sha256, blob.size</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Retrieve</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> data</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> blossom.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getBlob</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'https://blossom.example'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, blob.sha256, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'.png'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Mirror to another server for redundancy</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> blossom.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">mirrorBlob</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'https://backup.example'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, blob.url, mirrorAuth)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Check, list, delete</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> blossom.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">checkBlob</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'https://blossom.example'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, hash)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> blossom.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">listBlobs</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'https://blossom.example'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, pubkey)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> blossom.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">deleteBlob</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'https://blossom.example'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, hash, deleteAuth)</span></span></code></pre>
</div><p>Six operations. Upload, get, check, list, delete, mirror. That covers every media workflow.</p>
<h2 id="auth-is-just-a-nostr-event" tabindex="-1">Auth Is Just a Nostr Event </h2>
<p>Blossom uses kind 24242 events for authorization. You sign an event that says what you want to do, when it expires, and optionally which files and servers it applies to. The server verifies the signature. No API keys, no tokens to manage, no OAuth.</p>
<p>Your Nostr identity is your credential. If you can sign events, you can use Blossom.</p>
<h2 id="what-this-opens-up" tabindex="-1">What This Opens Up </h2>
<p><strong>A social client with native media.</strong> Users post images and videos. Your app uploads to their preferred Blossom servers (stored as a kind 10063 server list event). Files are content-addressed. If the user switches clients, their media is still there.</p>
<p><strong>Redundant storage with one function call.</strong> Upload to your primary server. Mirror to a backup. Two servers, same hash, same file. If one dies, the other serves it.</p>
<p><strong>A marketplace with product images.</strong> Sellers upload photos. Buyers see them. The images live on Blossom servers, identified by hash, authenticated by Nostr keys. No image hosting service needed.</p>
<p><strong>An archive that can't disappear.</strong> Upload important files. Mirror them across multiple servers. The SHA-256 hash is the permanent address. As long as one server has the file, it's accessible.</p>
<h2 id="server-lists" tabindex="-1">Server Lists </h2>
<p>Users publish a kind 10063 event listing their preferred Blossom servers. Your app reads it and knows where to upload and where to look.</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Publish server preferences</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> list</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> blossom.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">createServerListEvent</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'https://blossom.example'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'https://backup.example'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  secretKey</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Read someone's server list</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> servers</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> blossom.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">parseServerList</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(event)</span></span></code></pre>
</div><p>This means media storage is user-controlled. They pick their servers. Your app respects their choice.</p>
<h2 id="run-your-own-charge-for-it" tabindex="-1">Run Your Own, Charge for It </h2>
<p>A Blossom server is infrastructure. Infrastructure you can monetize.</p>
<p>You run a server. You set the rules. Storage limits, file types, pricing. Users authenticate with their Nostr keys, upload media, and you earn for providing the service. No platform middleman taking a cut. No ad-supported model where you are the product.</p>
<p>Think of it like running a relay, but for media. Communities can run a shared Blossom server for their members. Photographers can offer premium storage. A podcast network can host episodes. A marketplace can store product images with redundancy built in.</p>
<p>The server operator controls the economics. Users control where their files go. Both sides get what they want.</p>
<h2 id="the-pattern" tabindex="-1">The Pattern </h2>
<p>nostr-core keeps giving you the same thing: protocol primitives that work without deep Nostr knowledge. You don't need to understand BUD specs or construct authorization headers by hand. Import, call, ship.</p>
<p>Payments with NWC. Ecash with NIP-60. Media with Blossom. Social with NIPs 1 through 29. Identity, encryption, relay management. It's all the same package, the same types, the same patterns.</p>
<p>Build the thing you actually want to build. nostr-core handles the protocol.</p>
<hr>
<p><strong>Content-addressed media. Nostr-authenticated. No middlemen.</strong></p>
<p><strong><a href="https://github.com/nostr-core-org/nostr-core" target="_blank" rel="noreferrer">GitHub</a></strong></p>
]]></content:encoded>
    </item>
    <item>
      <title>Two Wallets, One Protocol</title>
      <link>https://nostr-core.netlify.app/blog/09-two-wallets-one-protocol.html</link>
      <guid isPermaLink="true">https://nostr-core.netlify.app/blog/09-two-wallets-one-protocol.html</guid>
      <pubDate>Thu, 19 Mar 2026 19:37:54 GMT</pubDate>
      <description>NIP-47 connects you to a remote wallet. NIP-60 carries one with you. Together they cover every payment scenario on Nostr.</description>
      <enclosure url="https://nostr-core.netlify.app/headers/blog-09-two-wallets.png" length="162677" type="image/png"/>
      <content:encoded><![CDATA[<p align="center">
  <img src="https://nostr-core.netlify.app/headers/blog-09-two-wallets.svg" alt="Two Wallets, One Protocol" width="100%">
</p>
<h1 id="two-wallets-one-protocol" tabindex="-1">Two Wallets, One Protocol </h1>
<p><strong>NIP-47 connects you to a remote wallet. NIP-60 carries one with you. Together they cover every payment scenario on Nostr.</strong></p>
<hr>
<h2 id="different-problems-same-network" tabindex="-1">Different Problems, Same Network </h2>
<p>Payments on Nostr have two shapes.</p>
<p>Sometimes you need to talk to a wallet that lives somewhere else. Send a command, get a result. That's NIP-47. Nostr Wallet Connect.</p>
<p>Sometimes you need the wallet right here. Tokens you hold, ready to spend, no remote call. That's NIP-60. Ecash on Nostr.</p>
<p>These aren't competing approaches. They solve different things.</p>
<h2 id="nip-47-the-remote-wallet" tabindex="-1">NIP-47: The Remote Wallet </h2>
<p>NWC is how you reach Lightning infrastructure you don't run yourself. A wallet provider, a self-hosted node, or a Cashu mint bridged through <a href="https://github.com/DoktorShift/NUTbits" target="_blank" rel="noreferrer">NUTbits</a>. Your app sends requests over Nostr relays. The wallet executes them.</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> nwc</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> NWC</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'nostr+walletconnect://...'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nwc.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">connect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nwc.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">payInvoice</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'lnbc...'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span></code></pre>
</div><p>Pay invoices. Create invoices. Check balances. Listen for incoming payments. Lightning Address resolution and fiat conversion built in. The NWC class handles encryption negotiation, request matching, typed errors, the full NIP-47 surface.</p>
<p>NWC is powerful when the wallet needs to be always-on. Receiving zaps while you're offline. Routing payments through the Lightning Network. Connecting to infrastructure with liquidity you didn't have to manage.</p>
<h2 id="nip-60-the-local-wallet" tabindex="-1">NIP-60: The Local Wallet </h2>
<p>NIP-60 flips the model. The wallet lives in your Nostr identity. Ecash proofs stored as encrypted events on relays. Your keys decrypt them. Your client spends them.</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { nip60 } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'nostr-core'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Fetch wallet from relays</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> filters</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nip60.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getWalletFilters</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(pubkey)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Parse token events</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> token</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nip60.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">parseTokenEvent</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(event, secretKey)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> balance</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nip60.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getProofsBalance</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(token.proofs)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Record a transaction</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> history</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nip60.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">createHistoryEvent</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  { direction: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'out'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, amount: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'1000'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, unit: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'sat'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, events: [</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">...</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  secretKey</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span></code></pre>
</div><p>No remote service for basic operations. You hold bearer tokens. Spending means constructing a transaction with proofs you already have. The mint verifies and settles.</p>
<p>Switch clients. Switch devices. Log in with your Nostr key and your balance is there. The proofs live on relays, encrypted, waiting.</p>
<h2 id="where-each-one-fits" tabindex="-1">Where Each One Fits </h2>
<p><strong>NWC shines when:</strong></p>
<ul>
<li>You need Lightning Network access</li>
<li>Your app delegates payment execution to infrastructure</li>
<li>You need the wallet always-on for incoming payments</li>
<li>You're integrating with services that speak BOLT-11</li>
</ul>
<p><strong>NIP-60 shines when:</strong></p>
<ul>
<li>Payments should be portable across clients and devices</li>
<li>Privacy matters (bearer tokens, blind signatures)</li>
<li>You're building peer-to-peer transfers in a community</li>
<li>The wallet should follow the user's identity, not a device</li>
</ul>
<h2 id="both-at-once" tabindex="-1">Both at Once </h2>
<p>The interesting part is an app that supports both.</p>
<p>A social client uses NIP-60 for quick transfers between users. Ecash tokens, instant, private, no Lightning routing. The same client uses NWC for receiving zaps from the wider network, paying invoices, bridging to Lightning.</p>
<p>Small payments stay local. Larger payments route through Lightning. The user just sends sats. The app picks the path.</p>
<p>A marketplace where buyers hold ecash in a NIP-60 wallet for instant checkout. But the seller receives via NWC into their Lightning node for settlement. Both wallets, one transaction, seamless.</p>
<p>And here's a nice detail: a Cashu mint operator running <a href="https://github.com/DoktorShift/NUTbits" target="_blank" rel="noreferrer">NUTbits</a> can serve both sides. NIP-60 users hold ecash from that mint directly. NWC users connect to the same mint through a NUTbits connection string. Same mint infrastructure, both wallet protocols, one operator.</p>
<h2 id="one-toolkit" tabindex="-1">One Toolkit </h2>
<p>nostr-core has both. The NWC class for remote wallet operations. The nip60 module for local ecash. Same types, same relay layer, same NIP-44 encryption stack underneath.</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { NWC, nip60 } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'nostr-core'</span></span></code></pre>
</div><p>That's two wallet primitives in one import. Build apps that use either or both.</p>
<hr>
<p><strong>Remote and local. Lightning and ecash. Two wallets, one toolkit.</strong></p>
<p><strong><a href="https://github.com/nostr-core-org/nostr-core" target="_blank" rel="noreferrer">GitHub</a></strong> · <strong><a href="https://github.com/DoktorShift/NUTbits" target="_blank" rel="noreferrer">NUTbits</a></strong></p>
]]></content:encoded>
    </item>
    <item>
      <title>Ecash on Nostr</title>
      <link>https://nostr-core.netlify.app/blog/08-ecash-on-nostr.html</link>
      <guid isPermaLink="true">https://nostr-core.netlify.app/blog/08-ecash-on-nostr.html</guid>
      <pubDate>Thu, 19 Mar 2026 19:37:54 GMT</pubDate>
      <description>Your wallet state lives on relays. Your proofs travel with your keys. No sync service. No device lock-in.</description>
      <enclosure url="https://nostr-core.netlify.app/headers/blog-08-ecash-on-nostr.png" length="154215" type="image/png"/>
      <content:encoded><![CDATA[<p align="center">
  <img src="https://nostr-core.netlify.app/headers/blog-08-ecash-on-nostr.svg" alt="Ecash on Nostr" width="100%">
</p>
<h1 id="ecash-on-nostr" tabindex="-1">Ecash on Nostr </h1>
<p><strong>Your wallet state lives on relays. Your proofs travel with your keys. No sync service. No device lock-in.</strong></p>
<hr>
<h2 id="wallets-have-a-location-problem" tabindex="-1">Wallets Have a Location Problem </h2>
<p>Most wallets live on a device. Your phone, your laptop, a browser extension. The data sits in local storage. Lose the device, lose access. Switch devices, export and import. Use two devices, figure out sync.</p>
<p>NIP-60 stores wallet state as encrypted Nostr events. The same relays your social graph already lives on.</p>
<h2 id="how-it-works" tabindex="-1">How It Works </h2>
<p>A NIP-60 wallet is four event kinds working together.</p>
<p><strong>Kind 17375</strong> is the wallet itself. A replaceable event that holds the wallet's private key and a list of trusted mints. Encrypted to yourself with NIP-44. Only your Nostr key can read it.</p>
<p><strong>Kind 7375</strong> is a token event. Each one holds unspent Cashu proofs for a single mint. Encrypted to self. This is where your actual balance lives, spread across however many token events you've accumulated.</p>
<p><strong>Kind 7376</strong> is spending history. Records transactions with direction, amount, and references to the token events involved. The created and destroyed proofs are encrypted. Redeemed markers stay in plain tags so relays can filter.</p>
<p><strong>Kind 7374</strong> is a quote event. Tracks pending mint quotes while a Lightning payment is in-flight. Uses NIP-40 expiration so stale quotes clean themselves up.</p>
<p>All encrypted content uses NIP-44's self-encryption pattern: derive a conversation key from your secret key to your own public key. Relays see encrypted blobs. Nobody else can read your wallet.</p>
<h2 id="what-this-gets-you" tabindex="-1">What This Gets You </h2>
<p>Log in with your Nostr key on any client that supports NIP-60. Your proofs are on the relays. Decrypt, sum the balances, show the wallet. No import file. No cloud backup. No sync service.</p>
<p>Switch from one Nostr client to another. Your ecash follows your identity, not your device.</p>
<p>Build a social client that also holds ecash. The user's proofs are already on the relays you're connected to. Read the token events, decrypt, show a balance. Spending means constructing a transaction with proofs the user already holds.</p>
<h2 id="nostr-core-has-it" tabindex="-1">nostr-core Has It </h2>
<p>The implementation lives in <code>nip60.ts</code>. Create wallets, store tokens, record history, manage quotes, delete spent proofs, build relay filters. Typed end-to-end.</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { nip60 } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'nostr-core'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Create a wallet event</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> wallet</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nip60.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">createWalletEvent</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  { privkey: walletKey, mints: [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'https://mint.example.com'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">] },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  secretKey</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Store ecash proofs</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> token</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nip60.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">createTokenEvent</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  { mint: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'https://mint.example.com'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, proofs: [</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">...</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">], unit: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'sat'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> },</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  secretKey</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Sum a balance</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> balance</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nip60.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getProofsBalance</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(proofs)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Fetch wallet state from relays</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> filters</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nip60.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getWalletFilters</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(pubkey)</span></span></code></pre>
</div><p>Self-encryption, token deletion for spent proofs, history tracking with proper marker separation. The same NIP-44 encryption stack the rest of nostr-core uses.</p>
<h2 id="the-trust-model" tabindex="-1">The Trust Model </h2>
<p>Cashu ecash is custodial at the mint level. The mint holds the Bitcoin. NIP-60 doesn't change that. It changes where the wallet state lives, not the trust relationship.</p>
<p>Multi-mint support helps. Your wallet can hold proofs from different mints. One mint goes down, you lose that mint's ecash, not everything.</p>
<p>Ecash gives you privacy and portability. It asks you to trust a mint. Whether that works depends on your use case. Both things are true at the same time.</p>
<h2 id="the-bridge-already-exists" tabindex="-1">The Bridge Already Exists </h2>
<p>If you run a Cashu mint and want to make it available as an NWC wallet, <a href="https://github.com/DoktorShift/NUTbits" target="_blank" rel="noreferrer">NUTbits</a> does exactly that. It connects to your mint on one side and speaks Nostr Wallet Connect on the other. Any app that supports NWC sees a wallet. Your mint handles the Lightning.</p>
<p>That means a NIP-60 wallet holding ecash from your mint, and an NWC connection powered by the same mint through NUTbits, can coexist. Local ecash for direct transfers. NWC for Lightning reach. Same mint, two protocols, full coverage.</p>
<hr>
<p><strong>Ecash wallets on Nostr relays. Encrypted, portable, ready.</strong></p>
<p><strong><a href="https://github.com/nostr-core-org/nostr-core" target="_blank" rel="noreferrer">GitHub</a></strong> · <strong><a href="https://github.com/DoktorShift/NUTbits" target="_blank" rel="noreferrer">NUTbits</a></strong></p>
]]></content:encoded>
    </item>
    <item>
      <title>Lightning Without the Lightning Code</title>
      <link>https://nostr-core.netlify.app/blog/07-lightning-without-the-lightning-code.html</link>
      <guid isPermaLink="true">https://nostr-core.netlify.app/blog/07-lightning-without-the-lightning-code.html</guid>
      <pubDate>Thu, 19 Mar 2026 19:37:54 GMT</pubDate>
      <description>You don't need to understand Lightning internals to accept and send payments. A connection string and five methods cover most use cases.</description>
      <enclosure url="https://nostr-core.netlify.app/headers/blog-07-lightning-payments.png" length="168966" type="image/png"/>
      <content:encoded><![CDATA[<p align="center">
  <img src="https://nostr-core.netlify.app/headers/blog-07-lightning-payments.svg" alt="Lightning Without the Lightning Code" width="100%">
</p>
<h1 id="lightning-without-the-lightning-code" tabindex="-1">Lightning Without the Lightning Code </h1>
<p><strong>You don't need to understand Lightning internals to accept and send payments. A connection string and five methods cover most use cases.</strong></p>
<hr>
<h2 id="lightning-is-powerful-and-complicated" tabindex="-1">Lightning Is Powerful and Complicated </h2>
<p>If you've ever set up a Lightning node, you know. Channel management, liquidity, routing, watchtowers, backups. It's powerful infrastructure, but it's a lot to take on when all you wanted was &quot;let users pay for things.&quot;</p>
<p>Most app developers don't need to run a node. They need a payment API. Something that takes &quot;pay this&quot; and returns &quot;paid,&quot; without requiring a degree in Lightning Network topology.</p>
<p>That's what NWC gives you. And nostr-core makes NWC trivial.</p>
<h2 id="the-connection-string-is-the-config" tabindex="-1">The Connection String Is the Config </h2>
<p>Someone else runs the wallet infrastructure. Maybe it's a service like Alby. Maybe it's a self-hosted setup. Maybe it's a Cashu mint behind <a href="https://github.com/DoktorShift/NUTbits" target="_blank" rel="noreferrer">NUTbits</a>. You don't need to know or care.</p>
<p>They give you a connection string:</p>
<div class="language- vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span>nostr+walletconnect://pubkey?relay=wss://...&#x26;secret=hex</span></span></code></pre>
</div><p>That string contains the wallet's identity, the relay to communicate through, and your secret key. Paste it into nostr-core, and you have a wallet.</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> nwc</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> NWC</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(connectionString)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nwc.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">connect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span></code></pre>
</div><p>Done. No node setup. No channel management. No liquidity planning.</p>
<h2 id="five-methods-for-most-apps" tabindex="-1">Five Methods for Most Apps </h2>
<p>The reality is that most applications need a small set of payment operations. nostr-core covers them:</p>
<p><strong>Check the balance:</strong></p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">balance</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nwc.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">getBalance</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span></code></pre>
</div><p><strong>Pay an invoice:</strong></p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">preimage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nwc.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">payInvoice</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'lnbc...'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span></code></pre>
</div><p><strong>Create an invoice:</strong></p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">invoice</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nwc.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">makeInvoice</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({ amount: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">50000</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, description: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'Premium access'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> })</span></span></code></pre>
</div><p><strong>Pay a Lightning address:</strong></p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nwc.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">payLightningAddress</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'creator@example.com'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">500</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span></code></pre>
</div><p><strong>Listen for incoming payments:</strong></p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">nwc.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">on</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'payment_received'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, (</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">data</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">  // Unlock content, update balance, send notification</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span></code></pre>
</div><p>That's it for most apps. A membership site needs <code>makeInvoice</code> and <code>payment_received</code>. A tipping feature needs <code>payLightningAddress</code>. A wallet dashboard needs <code>getBalance</code> and <code>listTransactions</code>.</p>
<h2 id="fiat-conversion-built-in" tabindex="-1">Fiat Conversion, Built In </h2>
<p>Sometimes your users think in dollars, not sats. nostr-core handles conversion:</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">sats</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">rate</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nwc.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">payLightningAddressFiat</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'shop@example.com'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">25</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'usd'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">log</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">`Paid ${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">sats</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">} sats at $${</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">rate</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">}/BTC`</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span></code></pre>
</div><p>Twenty-five dollars, converted to sats at the current rate, paid to a Lightning address. No extra dependencies. No separate API call for the exchange rate.</p>
<h2 id="what-you-don-t-write" tabindex="-1">What You Don't Write </h2>
<p>Here's what nostr-core handles that you'd otherwise implement yourself:</p>
<ul>
<li>WebSocket connection to the Nostr relay</li>
<li>Event creation, signing, and serialization</li>
<li>Encryption negotiation (NIP-04 vs NIP-44)</li>
<li>Request/response matching over async relay messages</li>
<li>Timeout handling with specific error types</li>
<li>Lightning Address resolution (LNURL-pay flow)</li>
<li>Fiat-to-sats conversion via exchange rate API</li>
<li>Connection lifecycle and cleanup</li>
</ul>
<p>That's not a small amount of code. And it's the kind of code where subtle bugs create real problems: payments that hang, connections that leak, encryption that silently downgrades.</p>
<p>nostr-core has solved these problems once, with proper error handling and typed responses. You get the result.</p>
<h2 id="when-you-need-more" tabindex="-1">When You Need More </h2>
<p>The NWC class covers the common cases. But nostr-core also exports the building blocks for when you need finer control.</p>
<p>Want to build a custom zap flow? NIP-57 exports zap request and receipt functions. Want to manage relay connections yourself? The Relay and RelayPool classes are available. Want to handle encryption manually for a specific use case? NIP-04 and NIP-44 are exported directly.</p>
<p>The high-level API gets you moving. The low-level exports let you go wherever you need.</p>
<h2 id="payments-are-just-another-feature" tabindex="-1">Payments Are Just Another Feature </h2>
<p>The best payment integration is the one your users don't think about. They click &quot;pay,&quot; it works, they move on.</p>
<p>nostr-core makes that possible by keeping the developer side equally simple. Import the class, connect with a string, call the method. The Lightning complexity stays where it belongs: in the wallet infrastructure, not in your application code.</p>
<hr>
<p><strong>Lightning payments. One import. One connection string.</strong> <code>npm install nostr-core</code></p>
<p><strong><a href="https://github.com/nostr-core-org/nostr-core" target="_blank" rel="noreferrer">GitHub</a></strong> · <strong><a href="https://github.com/nostr-core-org/nostr-core/tree/main/docs" target="_blank" rel="noreferrer">API Docs</a></strong></p>
]]></content:encoded>
    </item>
    <item>
      <title>Nostr Needs Boring Infrastructure</title>
      <link>https://nostr-core.netlify.app/blog/06-nostr-needs-boring-infrastructure.html</link>
      <guid isPermaLink="true">https://nostr-core.netlify.app/blog/06-nostr-needs-boring-infrastructure.html</guid>
      <pubDate>Thu, 19 Mar 2026 19:37:54 GMT</pubDate>
      <description>The protocol is interesting. The libraries should be boring. That's a good thing.</description>
      <enclosure url="https://nostr-core.netlify.app/headers/blog-06-boring-infrastructure.png" length="148552" type="image/png"/>
      <content:encoded><![CDATA[<p align="center">
  <img src="https://nostr-core.netlify.app/headers/blog-06-boring-infrastructure.svg" alt="Nostr Needs Boring Infrastructure" width="100%">
</p>
<h1 id="nostr-needs-boring-infrastructure" tabindex="-1">Nostr Needs Boring Infrastructure </h1>
<p><strong>The protocol is interesting. The libraries should be boring. That's a good thing.</strong></p>
<hr>
<p>Nostr works because the protocol is simple. Events, signatures, relays. A few primitives that combine into something much larger than the sum of their parts.</p>
<p>The problem has never been the protocol. The problem is the layer between the protocol and the applications people want to build. That layer (the libraries, the tooling, the shared infrastructure) is where projects succeed or stall.</p>
<p>I've been thinking about what it means to build that layer well.</p>
<h2 id="simple-protocols-need-simple-tools" tabindex="-1">Simple Protocols Need Simple Tools </h2>
<p>There's a temptation, when building developer tools for a new protocol, to add value by adding complexity. Convenience wrappers that hide the protocol. Abstractions that &quot;simplify&quot; things by replacing one mental model with another. Integrations with specific services because that's what users want right now.</p>
<p>Each of these is a reasonable decision in isolation. Together, they produce a tool that's harder to understand than the protocol it wraps.</p>
<p>nostr-core takes a different approach. The API mirrors the protocol. NIP-01 is events and relays. NIP-19 is encoding. NIP-44 is encryption. NIP-47 is wallet connect. The library is organized by NIP number because the protocol is organized by NIP number.</p>
<p>That's boring. It's supposed to be.</p>
<p>When you read nostr-core code, you're reading the protocol with types. When something goes wrong, you can check the NIP spec directly because the library doesn't reinterpret it. When a NIP updates, the corresponding module updates. The mapping is one-to-one.</p>
<h2 id="the-cost-of-vendor-coupling" tabindex="-1">The Cost of Vendor Coupling </h2>
<p>I notice a pattern in developer ecosystems. A company builds a useful service. They release an SDK. The SDK is good (great, even) but it assumes their service. The types carry their branding. The auth flow requires their platform. The errors reference their dashboard.</p>
<p>Developers adopt it because it works. Then they're coupled to a service they chose for convenience, not for technical reasons. Switching costs accumulate quietly.</p>
<p>Nostr was designed to avoid this. The protocol is open. Relays are interchangeable. Wallets communicate through a standard protocol. But if the dominant library for that protocol is coupled to a specific wallet provider, the vendor-neutrality of the protocol doesn't matter in practice.</p>
<p>nostr-core is deliberately vendor-neutral. Not because vendor-specific tools are bad (they serve their users well) but because the ecosystem also needs a tool that serves the protocol itself.</p>
<h2 id="forty-eight-nips-is-an-investment" tabindex="-1">Forty-Eight NIPs Is an Investment </h2>
<p>Implementing forty-eight NIPs in a single, coherent package is not exciting work. Each NIP has its own edge cases, its own type requirements, its own relationship to other NIPs. Getting the types right across all of them, making them compose naturally, ensuring the encryption modules work the same way everywhere. That's infrastructure work.</p>
<p>It's the kind of work that makes everything else easier. A developer building a social client doesn't need to think about NIP-10 thread parsing or NIP-44 encryption details. They import the module and use it. The protocol complexity is resolved once, in the library, not repeatedly in every application.</p>
<p>That's what infrastructure does. It absorbs complexity so applications don't have to.</p>
<h2 id="trust-models-should-be-explicit" tabindex="-1">Trust Models Should Be Explicit </h2>
<p>nostr-core's cryptography comes from Paul Miller's noble libraries. That's a specific choice with specific properties: audited code, minimal implementations, no unnecessary dependencies.</p>
<p>Seventy-nine total dependencies. One hundred thirty-two packages in the full tree. Those are numbers you can actually audit. You can read the dependency list in a few minutes and know what you're shipping.</p>
<p>Is it perfectly minimal? No. Could the dependency count be lower? Probably. But it's in a range where a team can make informed decisions about their supply chain. That matters when you're handling keys and payments.</p>
<h2 id="what-i-think-nostr-core-gets-right" tabindex="-1">What I Think nostr-core Gets Right </h2>
<p>It maps cleanly to the protocol. It doesn't add unnecessary abstractions. It works with any wallet, any relay, any signer. It ships the full NIP surface so developers don't need to assemble it from parts. It's typed end-to-end.</p>
<p>These aren't flashy properties. They're the properties of infrastructure that ages well.</p>
<h2 id="what-it-gets-wrong-or-doesn-t-solve-yet" tabindex="-1">What It Gets Wrong, or Doesn't Solve Yet </h2>
<p>The Nostr protocol is still evolving. Some NIPs in nostr-core will change as the community refines them. That's unavoidable and healthy, but it means the library is a moving target for anyone building production software.</p>
<p>There are also NIPs that nostr-core doesn't implement yet. The long tail of Nostr proposals is long, and protocol completeness is a moving goalpost.</p>
<p>And nostr-core is one library in one language. Nostr needs the same quality of tooling in Rust, Python, Swift, Kotlin, Go. The ecosystem is bigger than any single package.</p>
<h2 id="infrastructure-ages-differently" tabindex="-1">Infrastructure Ages Differently </h2>
<p>Flashy tools get attention when they launch. Infrastructure gets attention when it breaks. Or more precisely, it earns trust by not breaking.</p>
<p>nostr-core isn't trying to be the most exciting project in the Nostr ecosystem. It's trying to be the most reliable foundation for the applications that are. A boring, complete, honest implementation of the protocol that developers can build on without thinking about it.</p>
<p>Nostr needs interesting applications. It needs boring infrastructure.</p>
<p>That's what this is.</p>
<hr>
<p><strong><a href="https://github.com/nostr-core-org/nostr-core" target="_blank" rel="noreferrer">GitHub</a></strong></p>
]]></content:encoded>
    </item>
    <item>
      <title>Forty-Eight NIPs, One Import</title>
      <link>https://nostr-core.netlify.app/blog/05-forty-eight-nips-one-import.html</link>
      <guid isPermaLink="true">https://nostr-core.netlify.app/blog/05-forty-eight-nips-one-import.html</guid>
      <pubDate>Thu, 19 Mar 2026 19:37:54 GMT</pubDate>
      <description>Most Nostr libraries give you the basics. nostr-core gives you the protocol.</description>
      <enclosure url="https://nostr-core.netlify.app/headers/blog-05-forty-eight-nips.png" length="150518" type="image/png"/>
      <content:encoded><![CDATA[<p align="center">
  <img src="https://nostr-core.netlify.app/headers/blog-05-forty-eight-nips.svg" alt="Forty-Eight NIPs, One Import" width="100%">
</p>
<h1 id="forty-eight-nips-one-import" tabindex="-1">Forty-Eight NIPs, One Import </h1>
<p><strong>Most Nostr libraries give you the basics. nostr-core gives you the protocol.</strong></p>
<hr>
<h2 id="the-nip-problem" tabindex="-1">The NIP Problem </h2>
<p>Every Nostr app starts simple. You need events, keys, and a relay connection. Any library can do that.</p>
<p>Then your app grows. You need encrypted DMs. You need zaps. You need relay list management. You need calendar events for that community feature someone requested. You need badge verification for that trust system you're building.</p>
<p>Each NIP is a separate capability. In most ecosystems, that means a separate dependency, or implementing it yourself from the spec. You end up with a patchwork of libraries at different versions with different conventions, or a growing folder of hand-rolled protocol code.</p>
<h2 id="one-package-forty-eight-nips" tabindex="-1">One Package, Forty-Eight NIPs </h2>
<p>nostr-core ships forty-eight NIP implementations in a single package. Not wrappers around other libraries. Not partial implementations. Complete, typed modules that work together.</p>
<p>Here's a sample of what's available by category:</p>
<p><strong>Core protocol:</strong> NIP-01 events, NIP-02 contact lists, NIP-09 deletions, NIP-10 threads, NIP-13 proof of work, NIP-42 relay auth</p>
<p><strong>Identity:</strong> NIP-05 DNS verification, NIP-06 mnemonic keys, NIP-07 browser extensions, NIP-19 bech32 encoding, NIP-46 remote signers</p>
<p><strong>Encryption:</strong> NIP-04 (AES-CBC), NIP-44 (ChaCha20-Poly1305), NIP-59 gift wrapping, NIP-17 private messages</p>
<p><strong>Social:</strong> NIP-18 reposts, NIP-22 comments, NIP-23 long-form content, NIP-25 reactions, NIP-27 text references, NIP-28 channels, NIP-29 groups</p>
<p><strong>Payments:</strong> NIP-47 wallet connect, NIP-57 zaps, NIP-75 zap goals, Lightning addresses, LNURL, fiat conversion</p>
<p><strong>Lists &amp; metadata:</strong> NIP-24 extended metadata, NIP-30 custom emoji, NIP-31 alt text, NIP-36 content warnings, NIP-40 expiration, NIP-51 lists, NIP-65 relay lists</p>
<p><strong>Community:</strong> NIP-52 calendar events, NIP-56 reporting, NIP-58 badges</p>
<p><strong>Infrastructure:</strong> NIP-11 relay info, NIP-48 proxy, NIP-50 search, NIP-98 HTTP auth</p>
<p>Every module exports typed functions. Every function produces valid Nostr events. Every event works with the relay and signer abstractions in the same package.</p>
<h2 id="why-this-matters" tabindex="-1">Why This Matters </h2>
<p><strong>No version conflicts.</strong> When NIP-57 zaps need NIP-01 events and NIP-19 encoding, they're all at the same version, built from the same types. No compatibility matrix to manage.</p>
<p><strong>Discoverability.</strong> Need calendar events? It's <code>nip52</code>. Need badges? It's <code>nip58</code>. The naming is the NIP number. You don't search npm for &quot;nostr calendar&quot; and hope you find a maintained package.</p>
<p><strong>Composability.</strong> NIPs reference each other constantly. Gift wrapping (NIP-59) uses encryption (NIP-44). Zaps (NIP-57) use events and encoding. Private lists (NIP-51) use encryption for private items. When these all live in one package, composition is natural, not an integration project.</p>
<p><strong>Tree-shaking.</strong> ESM-only means your bundler only includes what you import. Using three NIPs? You ship three NIPs. The other forty-five don't touch your bundle.</p>
<h2 id="what-protocol-complete-means" tabindex="-1">What &quot;Protocol Complete&quot; Means </h2>
<p>It means you can build any Nostr application from a single dependency. A social client. A wallet. A relay management tool. A marketplace. A calendar app. A badge system. A community platform.</p>
<p>It also means when a new feature requires a NIP you haven't used yet, you don't go shopping for a new library. You add an import line.</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Today: social client</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { finalizeEvent, nip10, nip25, Relay } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'nostr-core'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Tomorrow: add zaps</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { finalizeEvent, nip10, nip25, nip57, NWC, Relay } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'nostr-core'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Next week: add calendar events</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { finalizeEvent, nip10, nip25, nip57, nip52, NWC, Relay } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'nostr-core'</span></span></code></pre>
</div><p>Same package. Same types. Same patterns. Your dependency list doesn't grow with your feature set.</p>
<h2 id="not-every-nip-is-finished" tabindex="-1">Not Every NIP Is Finished </h2>
<p>Nostr is an evolving protocol. Some NIPs are stable and battle-tested. Others are newer, still being refined by the community. nostr-core tracks the specs as they develop.</p>
<p>If a NIP changes, nostr-core updates. One package to update, one changelog to read, one set of breaking changes to handle. That's simpler than tracking fifteen separate libraries.</p>
<h2 id="the-alternative" tabindex="-1">The Alternative </h2>
<p>You could assemble your own stack. Pick a core library for events. Find a separate package for NIP-44 encryption. Another for NIP-19 encoding. Write your own NIP-52 calendar implementation because nobody published one. Hope they all use compatible types.</p>
<p>That works. People do it. But it's more work, more risk, and more maintenance for the same result.</p>
<p>Or you can install nostr-core and start building.</p>
<hr>
<p><strong>Forty-eight NIPs. One package. Zero assembly required.</strong></p>
<p><strong><a href="https://github.com/nostr-core-org/nostr-core" target="_blank" rel="noreferrer">GitHub</a></strong> · <strong><a href="https://github.com/nostr-core-org/nostr-core/tree/main/docs/api" target="_blank" rel="noreferrer">API Reference</a></strong></p>
]]></content:encoded>
    </item>
    <item>
      <title>The Library That Doesn't Lock You In</title>
      <link>https://nostr-core.netlify.app/blog/04-the-library-that-doesnt-lock-you-in.html</link>
      <guid isPermaLink="true">https://nostr-core.netlify.app/blog/04-the-library-that-doesnt-lock-you-in.html</guid>
      <pubDate>Thu, 19 Mar 2026 19:37:54 GMT</pubDate>
      <description>No OAuth. No API keys. No vendor. Just the protocol.</description>
      <enclosure url="https://nostr-core.netlify.app/headers/blog-04-no-lock-in.png" length="153896" type="image/png"/>
      <content:encoded><![CDATA[<p align="center">
  <img src="https://nostr-core.netlify.app/headers/blog-04-no-lock-in.svg" alt="The Library That Doesn't Lock You In" width="100%">
</p>
<h1 id="the-library-that-doesn-t-lock-you-in" tabindex="-1">The Library That Doesn't Lock You In </h1>
<p><strong>No OAuth. No API keys. No vendor. Just the protocol.</strong></p>
<hr>
<h2 id="vendor-lock-in-is-a-quiet-problem" tabindex="-1">Vendor Lock-In Is a Quiet Problem </h2>
<p>It doesn't happen all at once. You pick an SDK because it's convenient. The docs are good. The getting-started example works in five minutes.</p>
<p>Then you notice the OAuth flow is specific to that provider. The error messages reference their dashboard. The WebSocket connection goes through their relay. The types assume their wallet service.</p>
<p>You're not building on a protocol anymore. You're building on someone's product. And the moment they change their terms, raise prices, or pivot, your app feels it.</p>
<h2 id="nostr-core-is-protocol-only" tabindex="-1">nostr-core Is Protocol-Only </h2>
<p>There's no account to create. No API key to manage. No OAuth flow to implement. No webhooks to configure on someone else's platform.</p>
<p>nostr-core implements Nostr protocols (NIPs) and nothing else. When you connect to a wallet, you use a standard NWC connection string. When you connect to relays, you use standard WebSocket URLs. When you sign events, you use standard Nostr keys.</p>
<p>Every piece is interchangeable. Switch wallets by changing a connection string. Switch relays by changing a URL. Switch signers by swapping the signer implementation. Your code doesn't change.</p>
<h2 id="the-comparison-worth-making" tabindex="-1">The Comparison Worth Making </h2>
<p>The most common alternative for NWC integration is <code>@getalby/sdk</code>. It's a good library, built by good people. But it carries Alby's product assumptions with it.</p>
<p>Here's what the numbers look like:</p>
<table tabindex="0">
<thead>
<tr>
<th></th>
<th>nostr-core</th>
<th>@getalby/sdk</th>
</tr>
</thead>
<tbody>
<tr>
<td>Install size</td>
<td>118 MB</td>
<td>159 MB</td>
</tr>
<tr>
<td>Packages</td>
<td>79</td>
<td>436</td>
</tr>
<tr>
<td>Total deps</td>
<td>132</td>
<td>698</td>
</tr>
<tr>
<td>Vendor coupling</td>
<td>None</td>
<td>Alby OAuth, webhooks</td>
</tr>
<tr>
<td>API surface</td>
<td>1 class</td>
<td>5+ classes</td>
</tr>
</tbody>
</table>
<p>nostr-core is 26% smaller, has 82% fewer packages, and 81% fewer total dependencies. Those aren't abstract metrics. Fewer dependencies mean fewer supply chain risks, faster installs, and less to audit.</p>
<p>But the real difference isn't size. It's coupling.</p>
<p><code>@getalby/sdk</code> works great if you're building for Alby users. It provides OAuth flows, webhook handling, and Alby-specific features. If that's your use case, use it.</p>
<p>If you're building for everyone (any wallet, any relay, any signer), nostr-core is the tool that doesn't assume who your users are.</p>
<h2 id="what-no-lock-in-looks-like-in-practice" tabindex="-1">What &quot;No Lock-In&quot; Looks Like in Practice </h2>
<p><strong>Wallet portability.</strong> Your users connect with any NWC-compatible wallet. Alby, Mutiny, custom setups. nostr-core doesn't care. One connection string, one API.</p>
<p><strong>Relay independence.</strong> Connect to whatever relays make sense for your app. Public relays, private relays, self-hosted relays. The relay abstraction works with all of them.</p>
<p><strong>Signer flexibility.</strong> Use a secret key directly, a NIP-07 browser extension, or a NIP-46 remote signer. The signer interface is the same regardless.</p>
<p><strong>Runtime freedom.</strong> Node 18+, Deno, Bun, Cloudflare Workers. ESM-only, no polyfills needed. Deploy wherever your app lives.</p>
<h2 id="the-trade-off" tabindex="-1">The Trade-Off </h2>
<p>Being vendor-neutral means nostr-core doesn't give you vendor-specific features. There's no Alby OAuth flow. No managed webhook endpoints. No dashboard integration.</p>
<p>If you need those things, you either build them yourself or use the vendor's SDK. That's a real trade-off, and it's worth being clear about it.</p>
<p>For most NWC use cases (connecting a wallet, making payments, listening for transactions), you don't need vendor features. You need the protocol. nostr-core gives you the protocol and gets out of the way.</p>
<h2 id="the-dependency-story" tabindex="-1">The Dependency Story </h2>
<p>nostr-core's cryptography comes from Paul Miller's noble libraries. secp256k1, SHA-256, ChaCha20, AES-CBC. All audited, all minimal, all widely trusted in the Bitcoin ecosystem.</p>
<p>That's a deliberate choice. When your dependency tree is small and audited, you can actually review what you're shipping. Seventy-nine packages is a number a team can audit. Six hundred and ninety-eight is not.</p>
<h2 id="pick-your-protocol-not-your-provider" tabindex="-1">Pick Your Protocol, Not Your Provider </h2>
<p>Nostr is a protocol. Lightning is a protocol. NWC is a protocol. The whole point of building on open protocols is that you're not dependent on any single provider.</p>
<p>nostr-core is built to preserve that property. Use it with any wallet, any relay, any runtime, any signer. Swap any piece without touching the rest.</p>
<p>That's not a feature. That's just what protocol-level tooling should be.</p>
<hr>
<p><strong>No vendor. No lock-in. Just Nostr.</strong> <code>npm install nostr-core</code></p>
<p><strong><a href="https://github.com/nostr-core-org/nostr-core" target="_blank" rel="noreferrer">GitHub</a></strong> · <strong><a href="https://github.com/nostr-core-org/nostr-core/blob/main/docs/guide/comparison.md" target="_blank" rel="noreferrer">Comparison Guide</a></strong></p>
]]></content:encoded>
    </item>
    <item>
      <title>Build a Nostr Client From Scratch</title>
      <link>https://nostr-core.netlify.app/blog/03-build-a-nostr-client-from-scratch.html</link>
      <guid isPermaLink="true">https://nostr-core.netlify.app/blog/03-build-a-nostr-client-from-scratch.html</guid>
      <pubDate>Thu, 19 Mar 2026 19:37:54 GMT</pubDate>
      <description>Everything you need for a social client lives in one import. Keys, events, relays, encryption, threads, reactions. All typed, all ready.</description>
      <enclosure url="https://nostr-core.netlify.app/headers/blog-03-build-a-nostr-client.png" length="163710" type="image/png"/>
      <content:encoded><![CDATA[<p align="center">
  <img src="https://nostr-core.netlify.app/headers/blog-03-build-a-nostr-client.svg" alt="Build a Nostr Client From Scratch" width="100%">
</p>
<h1 id="build-a-nostr-client-from-scratch" tabindex="-1">Build a Nostr Client From Scratch </h1>
<p><strong>Everything you need for a social client lives in one import. Keys, events, relays, encryption, threads, reactions. All typed, all ready.</strong></p>
<hr>
<h2 id="start-with-keys" tabindex="-1">Start With Keys </h2>
<p>Every Nostr identity is a keypair. nostr-core generates them from audited cryptography:</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { generateSecretKey, getPublicKey } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'nostr-core'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> sk</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> generateSecretKey</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> pk</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> getPublicKey</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(sk)</span></span></code></pre>
</div><p>That's your user. The secret key signs events. The public key is their identity. No accounts, no registration, no server.</p>
<p>If you want human-readable identifiers, NIP-19 encoding is built in:</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { nip19 } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'nostr-core'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> npub</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nip19.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">npubEncode</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(pk)   </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// npub1...</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> nsec</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nip19.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">nsecEncode</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(sk)   </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// nsec1...</span></span></code></pre>
</div><p>For users who want to import from a mnemonic phrase, NIP-06 handles BIP-39 derivation. For browser extensions, NIP-07 provides a signer interface that never exposes the private key to your app.</p>
<h2 id="connect-to-relays" tabindex="-1">Connect to Relays </h2>
<p>Nostr is a relay network. Your client needs to talk to relays to publish and receive events.</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { Relay, RelayPool } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'nostr-core'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Single relay</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> relay</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> Relay</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'wss://relay.damus.io'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> relay.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">connect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Or a pool of relays</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> pool</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> RelayPool</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">([</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'wss://relay.damus.io'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'wss://nos.lol'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">])</span></span></code></pre>
</div><p>Publish events, subscribe to filters, handle connections. The relay abstraction manages the WebSocket lifecycle so you can focus on your application logic.</p>
<h2 id="sign-and-publish-events" tabindex="-1">Sign and Publish Events </h2>
<p>Events are the atoms of Nostr. A social post is a kind-1 event. A reaction is kind-7. A repost is kind-6. nostr-core gives you typed event creation for all of them.</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { finalizeEvent } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'nostr-core'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> event</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> finalizeEvent</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  kind: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  content: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'Hello from nostr-core'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  created_at: Math.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">floor</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(Date.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">now</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1000</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">),</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  tags: [],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}, sk)</span></span></code></pre>
</div><p><code>finalizeEvent</code> signs the event, computes the hash, and returns a complete, valid Nostr event ready to publish.</p>
<h2 id="threads-and-mentions" tabindex="-1">Threads and Mentions </h2>
<p>Social apps need threading. NIP-10 defines how replies reference parent events:</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { nip10 } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'nostr-core'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Parse thread references from an event</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> refs</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nip10.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">parse</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(event)</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// refs.root - the top-level event</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// refs.reply - the immediate parent</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// refs.mentions - other referenced events</span></span></code></pre>
</div><p>Build reply chains, render conversation trees, show quoted posts. The tag parsing is handled for you.</p>
<h2 id="reactions-and-reposts" tabindex="-1">Reactions and Reposts </h2>
<p>NIP-25 reactions let users respond to content:</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { nip25 } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'nostr-core'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> reaction</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nip25.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">createReaction</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(originalEvent, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'+'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> signed</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> finalizeEvent</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(reaction, sk)</span></span></code></pre>
</div><p>NIP-18 reposts work the same way: create the event template, sign it, publish it. The NIP modules give you correctly structured events every time.</p>
<h2 id="encrypted-messages" tabindex="-1">Encrypted Messages </h2>
<p>Private messaging is where nostr-core's encryption stack comes together. NIP-17 provides fully sealed, metadata-protected messages using gift wrapping:</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { nip17 } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'nostr-core'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> wrapped</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nip17.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">wrapEvent</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(senderSk, recipientPk, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'secret message'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span></code></pre>
</div><p>Under the hood, this uses NIP-44 encryption (ChaCha20-Poly1305) and NIP-59 gift wrapping to protect both the content and the metadata. The recipient unwraps it with their key. Relays see nothing useful.</p>
<p>For simpler use cases, NIP-04 encryption is also available, though NIP-44 is recommended for new applications.</p>
<h2 id="long-form-content" tabindex="-1">Long-Form Content </h2>
<p>Building a blog platform or article reader? NIP-23 handles long-form content with metadata:</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { nip23 } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'nostr-core'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> article</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nip23.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">createArticle</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  title: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'My First Post'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  content: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'Full markdown content here...'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  summary: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'A brief summary'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  image: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'https://example.com/header.jpg'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  publishedAt: Math.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">floor</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(Date.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">now</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">/</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> 1000</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">),</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  tags: [</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'nostr'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'development'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">],</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  slug: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'my-first-post'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span></code></pre>
</div><p>Titles, summaries, images, tags, slugs. Everything a publishing platform needs.</p>
<h2 id="payments-built-in" tabindex="-1">Payments Built In </h2>
<p>Your social client probably wants zaps. nostr-core's NWC class handles wallet integration:</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { NWC, nip57 } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'nostr-core'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> nwc</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> NWC</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'nostr+walletconnect://...'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nwc.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">connect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Pay an invoice from a zap flow</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nwc.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">payInvoice</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(zapInvoice)</span></span></code></pre>
</div><p>NIP-57 zap requests and receipts are also exported, so you can build the full zap UI: request creation, receipt verification, zap counts on posts.</p>
<h2 id="the-point" tabindex="-1">The Point </h2>
<p>A Nostr client needs a lot of protocol pieces. Key management, relay connections, event signing, threading, reactions, encryption, payments. In most ecosystems, that's five or six different libraries with incompatible types and different release cycles.</p>
<p>With nostr-core, it's one package. The pieces are designed to work together because they were built together. Types flow through naturally. Modules compose without adapters.</p>
<p>You focus on what makes your client unique. nostr-core handles the protocol.</p>
<hr>
<p><strong>Start building.</strong> <code>npm install nostr-core</code> and go.</p>
<p><strong><a href="https://github.com/nostr-core-org/nostr-core" target="_blank" rel="noreferrer">GitHub</a></strong> · <strong><a href="https://nostr-core-demo.netlify.app/" target="_blank" rel="noreferrer">Demo</a></strong></p>
]]></content:encoded>
    </item>
    <item>
      <title>One Class, One Connection String</title>
      <link>https://nostr-core.netlify.app/blog/02-one-class-one-connection-string.html</link>
      <guid isPermaLink="true">https://nostr-core.netlify.app/blog/02-one-class-one-connection-string.html</guid>
      <pubDate>Thu, 19 Mar 2026 19:37:54 GMT</pubDate>
      <description>The entire NWC spec in five lines of code. That's not a simplification; it's the actual API.</description>
      <enclosure url="https://nostr-core.netlify.app/headers/blog-02-one-class-one-connection-string.png" length="166189" type="image/png"/>
      <content:encoded><![CDATA[<p align="center">
  <img src="https://nostr-core.netlify.app/headers/blog-02-one-class-one-connection-string.svg" alt="One Class, One Connection String" width="100%">
</p>
<h1 id="one-class-one-connection-string" tabindex="-1">One Class, One Connection String </h1>
<p><strong>The entire NWC spec in five lines of code. That's not a simplification; it's the actual API.</strong></p>
<hr>
<h2 id="payment-integration-shouldn-t-be-hard" tabindex="-1">Payment Integration Shouldn't Be Hard </h2>
<p>Every payment integration story starts the same way. You read the docs. You install the SDK. You create an account. You get API keys. You configure webhooks. You handle OAuth flows. You write error handling for seventeen different failure modes.</p>
<p>By the time you've sent your first payment, you've written more infrastructure code than product code.</p>
<p>NWC was designed to be simpler than that. And nostr-core takes that simplicity seriously.</p>
<h2 id="five-lines" tabindex="-1">Five Lines </h2>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { NWC } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'nostr-core'</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> nwc</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> NWC</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'nostr+walletconnect://...'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nwc.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">connect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">preimage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nwc.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">payInvoice</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'lnbc...'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">nwc.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">close</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span></code></pre>
</div><p>That's a complete payment. Import, connect, pay, close.</p>
<p>The connection string contains everything: the wallet's public key, the relay to communicate through, and your secret key. No separate config files. No environment variables for client IDs and secrets. One string.</p>
<h2 id="what-the-nwc-class-handles" tabindex="-1">What the NWC Class Handles </h2>
<p>Behind those five lines, nostr-core is doing real work.</p>
<p><strong>Encryption auto-detection.</strong> Some wallets use NIP-04 (AES-CBC). Others support NIP-44 (ChaCha20). nostr-core checks what the wallet supports and uses the best available option. You don't configure this. It just works.</p>
<p><strong>Relay management.</strong> The class connects to the relay specified in the connection string, subscribes to the right filters, and handles the WebSocket lifecycle. Reconnection, subscription management, cleanup on close.</p>
<p><strong>Request/response matching.</strong> NWC is asynchronous over Nostr relays. nostr-core matches each response to its request, handles timeouts, and surfaces errors through a typed hierarchy.</p>
<p><strong>Event signing.</strong> Every NWC request is a signed Nostr event. nostr-core handles the signing, serialization, and verification internally.</p>
<p>You don't see any of this. You see <code>payInvoice()</code> and get a preimage back.</p>
<h2 id="the-full-surface" tabindex="-1">The Full Surface </h2>
<p>The NWC class exposes every NIP-47 method:</p>
<ul>
<li><code>getBalance()</code> - how much is in the wallet</li>
<li><code>payInvoice()</code> - pay a BOLT-11 invoice</li>
<li><code>makeInvoice()</code> - create an invoice to receive</li>
<li><code>listTransactions()</code> - payment history</li>
<li><code>payKeysend()</code> - pay a node directly</li>
<li><code>payLightningAddress()</code> - resolve and pay a Lightning address</li>
<li><code>payLightningAddressFiat()</code> - convert fiat to sats and pay</li>
<li><code>getInfo()</code> - wallet metadata</li>
<li><code>getBudget()</code> - spending limits</li>
<li><code>signMessage()</code> - sign with the wallet key</li>
</ul>
<p>Plus event listeners for real-time payment notifications:</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">nwc.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">on</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'payment_received'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, (</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">notification</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  console.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">log</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'Incoming:'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, notification.notification.amount, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'msats'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span></span></code></pre>
</div><p>That's the full wallet API. One class, one import.</p>
<h2 id="errors-that-tell-you-what-happened" tabindex="-1">Errors That Tell You What Happened </h2>
<p>When something goes wrong, you get a specific error, not a generic exception with a string message.</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">try</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nwc.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">payInvoice</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'lnbc...'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">} </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">catch</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (err) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (err </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">instanceof</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> NWCWalletError</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // The wallet said no: insufficient balance, expired invoice, etc.</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">else</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (err </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">instanceof</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> NWCTimeoutError</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // Wallet didn't respond in time</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">else</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (err </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">instanceof</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> NWCConnectionError</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) {</span></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">    // Couldn't reach the relay</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  }</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre>
</div><p>Eight error classes in a clean hierarchy. Your error handling can be as specific or as general as you need.</p>
<h2 id="lightning-addresses-in-one-call" tabindex="-1">Lightning Addresses in One Call </h2>
<p>Lightning Address resolution normally means fetching a well-known URL, parsing the LNURL response, creating an invoice, then paying it. nostr-core wraps that entire flow:</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nwc.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">payLightningAddress</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'user@example.com'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1000</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span></code></pre>
</div><p>One thousand sats to a Lightning address. One line.</p>
<p>Need fiat conversion? That's built in too:</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nwc.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">payLightningAddressFiat</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'user@example.com'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">5</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'usd'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span></code></pre>
</div><p>Five dollars, converted to sats at the current rate, paid to a Lightning address. Still one line.</p>
<h2 id="the-dx-argument" tabindex="-1">The DX Argument </h2>
<p>Developer experience isn't about making things pretty. It's about reducing the distance between your intention and your code.</p>
<p>If you want to pay an invoice, the code should say &quot;pay this invoice.&quot; If you want to listen for payments, the code should say &quot;when a payment arrives, do this.&quot; The protocol complexity should be invisible unless you need to see it.</p>
<p>That's what one class and one connection string gives you. The intent is the code.</p>
<hr>
<p><strong>Start with <code>npm install nostr-core</code> and a connection string.</strong> That's all you need.</p>
<p><strong><a href="https://github.com/nostr-core-org/nostr-core" target="_blank" rel="noreferrer">GitHub</a></strong> · <strong><a href="https://github.com/nostr-core-org/nostr-core/tree/main/docs" target="_blank" rel="noreferrer">API Docs</a></strong></p>
]]></content:encoded>
    </item>
    <item>
      <title>What nostr-core Actually Is</title>
      <link>https://nostr-core.netlify.app/blog/01-what-nostr-core-actually-is.html</link>
      <guid isPermaLink="true">https://nostr-core.netlify.app/blog/01-what-nostr-core-actually-is.html</guid>
      <pubDate>Thu, 19 Mar 2026 19:37:54 GMT</pubDate>
      <description>You want to build on Nostr. You don't want to fight the protocol to get there.</description>
      <enclosure url="https://nostr-core.netlify.app/headers/blog-01-what-nostr-core-actually-is.png" length="161490" type="image/png"/>
      <content:encoded><![CDATA[<p align="center">
  <img src="https://nostr-core.netlify.app/headers/blog-01-what-nostr-core-actually-is.svg" alt="What nostr-core Actually Is" width="100%">
</p>
<h1 id="what-nostr-core-actually-is" tabindex="-1">What nostr-core Actually Is </h1>
<p><strong>You want to build on Nostr. You don't want to fight the protocol to get there.</strong></p>
<hr>
<h2 id="the-problem-is-familiar" tabindex="-1">The Problem Is Familiar </h2>
<p>You've seen Nostr. You like the idea: decentralized social, payments over Lightning, identity without platforms. You want to build something.</p>
<p>So you look at the ecosystem. There are libraries, but they tend to fall into two camps: minimal utilities that leave you wiring things together yourself, or vendor-specific SDKs that come with someone else's opinions baked in.</p>
<p>You just want the protocol. All of it. In one package. With types.</p>
<h2 id="that-s-nostr-core" tabindex="-1">That's nostr-core </h2>
<p>nostr-core is a JavaScript/TypeScript toolkit that implements 48 NIPs in a single package. Core protocol, encryption, encoding, social features, payments, identity, calendar events, badges, moderation. It's all there.</p>
<p>One install. One import. The protocol is ready to use.</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">import</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { NWC, nip19, finalizeEvent, Relay } </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">from</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'nostr-core'</span></span></code></pre>
</div><p>That gives you wallet payments, bech32 encoding, event signing, and relay connections. No glue code. No adapter packages. No &quot;install these seven other things first.&quot;</p>
<h2 id="who-it-s-for" tabindex="-1">Who It's For </h2>
<p>Developers building Nostr applications. That's broad on purpose.</p>
<p>If you're building a social client, you need events, reactions, reposts, threads, long-form content, encrypted DMs, relay management. nostr-core has all of that.</p>
<p>If you're building a payment flow, you need NWC, Lightning addresses, zaps, invoices, fiat conversion. nostr-core has all of that too.</p>
<p>If you're building something weird that touches badges and calendar events and proof of work? Still covered.</p>
<p>The point is: whatever combination of NIPs your app needs, they're already here, already typed, already working together.</p>
<h2 id="what-it-s-built-on" tabindex="-1">What It's Built On </h2>
<p>The cryptography comes from Paul Miller's noble libraries, the same audited primitives the wider Bitcoin ecosystem trusts. secp256k1, schnorr signatures, SHA-256, ChaCha20, AES-CBC. Peer-reviewed, minimal, no surprises.</p>
<p>The rest is protocol implementation. Each NIP is its own module, but they share types and compose naturally. NIP-59 gift wrapping uses NIP-44 encryption. NIP-57 zaps use events and relays. Everything connects because it was designed to.</p>
<h2 id="what-it-doesn-t-do" tabindex="-1">What It Doesn't Do </h2>
<p>nostr-core is headless. No UI components, no framework bindings, no opinions about how your app should look or work. It gives you the protocol layer and gets out of the way.</p>
<p>It also doesn't phone home, require API keys, or depend on any specific service. It connects to whatever relays you point it at, signs with whatever keys you give it, and talks to whatever NWC wallet you configure.</p>
<p>If your app needs React components or a specific hosting platform, that's your call. nostr-core just handles the Nostr part.</p>
<h2 id="the-shape-of-the-api" tabindex="-1">The Shape of the API </h2>
<p>The high-level entry point is the <code>NWC</code> class. One connection string, and you have a working wallet interface. Pay invoices, create invoices, check balances, listen for payments.</p>
<p>Below that, everything is exported as building blocks. Key generation, event signing, relay connections, encryption, encoding. Use the high-level class or reach for the primitives directly. Both paths are supported and typed.</p>
<div class="language-ts vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// High level</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> nwc</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> NWC</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'nostr+walletconnect://...'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nwc.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">connect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> nwc.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">payInvoice</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'lnbc...'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span class="line"></span>
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// Low level</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> sk</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> generateSecretKey</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> event</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> finalizeEvent</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">({ kind: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, content: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'hello'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">...</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }, sk)</span></span></code></pre>
</div><h2 id="try-it" tabindex="-1">Try It </h2>
<div class="language-sh vp-adaptive-theme"><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0" v-pre=""><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> install</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> nostr-core</span></span></code></pre>
</div><p>Node 18+, Deno, Bun, Cloudflare Workers. ESM-only, tree-shakeable, fully typed.</p>
<p>Read the code, check the types, build something. The protocol is ready when you are.</p>
<hr>
<p><strong><a href="https://github.com/nostr-core-org/nostr-core" target="_blank" rel="noreferrer">GitHub</a></strong> · <strong><a href="https://nostr-core-demo.netlify.app/" target="_blank" rel="noreferrer">Demo</a></strong></p>
]]></content:encoded>
    </item>
  </channel>
</rss>
