<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Zeke</title>
    <description>The latest articles on DEV Community by Zeke (@zekebuilds).</description>
    <link>https://dev.to/zekebuilds</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3866714%2F688cd691-92a0-4825-af29-ca57b7b020bb.png</url>
      <title>DEV Community: Zeke</title>
      <link>https://dev.to/zekebuilds</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zekebuilds"/>
    <language>en</language>
    <item>
      <title>Chaintip freshness cert: signed identity scores that age with the chain</title>
      <dc:creator>Zeke</dc:creator>
      <pubDate>Tue, 28 Apr 2026 13:31:51 +0000</pubDate>
      <link>https://dev.to/zekebuilds/chaintip-freshness-cert-signed-identity-scores-that-age-with-the-chain-59o8</link>
      <guid>https://dev.to/zekebuilds/chaintip-freshness-cert-signed-identity-scores-that-age-with-the-chain-59o8</guid>
      <description>&lt;p&gt;A schnorr signature tells you who. A timestamp tells you when. A chaintip tells you when honestly. A chaintip with a TTL tells you when, and how long it's still good for. That last piece is what shipped today on the Depth-of-Identity Oracle.&lt;/p&gt;

&lt;p&gt;If you follow the softwar conversation (Lowery's &lt;em&gt;Softwar&lt;/em&gt;, Paparo's INDOPACOM testimony, the Bitcoin Policy Institute's recent press cycle), the framing matters. Bitcoin proof-of-work costs real energy. A signature anchored to that cost weighs more than a wall-clock timestamp does. We've been doing the binding side for a couple weeks. Every signed DoI score envelope has carried &lt;code&gt;bitcoin_tip {height, hash}&lt;/code&gt; since MR !171. What was missing was the expiration.&lt;/p&gt;

&lt;p&gt;A signed score from yesterday still validated cryptographically today. The signature was fine. But fine is not what you want from an identity score. You want to know if it's current. So we added one field to the signed payload and one new endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pubkey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"depth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"social"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"access"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"vouch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"economic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"composite"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rank"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"established"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bitcoin_tip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;946892&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0000...e0b3"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"freshness_blocks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"signed_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1761579180&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"valid_until"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1761582780&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"signed_by"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"b4b12dfb..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"signature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;128-hex schnorr sig over canonical JSON&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;freshness_blocks&lt;/code&gt; is now part of what the signature covers. Default 6, which is roughly 60 minutes on bitcoin. The verifier rejects scores where &lt;code&gt;current_tip_height - bitcoin_tip.height &amp;gt; freshness_blocks&lt;/code&gt;. Replay an hour-old score against today's tip and it fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a window, not a wall-clock
&lt;/h2&gt;

&lt;p&gt;Wall-clock TTL is the obvious choice, and it's the wrong one here. A wall-clock says "expires at 2:30pm." Whose 2:30pm? Whose clock? An attacker can lie about local time. A verifier with a skewed clock reads the lie as truth.&lt;/p&gt;

&lt;p&gt;A chaintip-block window says "expires 6 blocks past height N." You can't fake that. To advance the chain by 6 blocks past height N, the bitcoin network has to actually mine 6 blocks. There's real-world energy spent across miners globally, and no centralized clock to lie about. Either the chaintip is past the threshold or it isn't.&lt;/p&gt;

&lt;p&gt;Same idea Lowery uses for proof-of-work as a filter. Wall time is cheap to manipulate. Block time costs power, distributed and provable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The verify endpoint
&lt;/h2&gt;

&lt;p&gt;Two ways to check freshness. Server side: &lt;code&gt;POST /oracle/freshness&lt;/code&gt; with the signed envelope and the current chaintip height. You get back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"valid_signature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fresh"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"current_tip_height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;946895&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"signed_tip_height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;946892&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"blocks_elapsed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"freshness_blocks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fresh_until_height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;946898&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Client side: install &lt;code&gt;@powforge/identity@0.7.1&lt;/code&gt;, call &lt;code&gt;checkFreshness(scoreResponse, currentTipHeight)&lt;/code&gt;, get the same envelope back without round-tripping the oracle. Pure function. Signature must already be verified by the caller, either via the SDK's existing verify path or by hitting the oracle's &lt;code&gt;/verify&lt;/code&gt; endpoint.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @powforge/identity@0.7.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;checkFreshness&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@powforge/identity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;checkFreshness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signedScore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentTipHeight&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fresh&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Re-fetch from /oracle/doi-score&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What this enables
&lt;/h2&gt;

&lt;p&gt;The L402 invoice flow on &lt;code&gt;pow-gate&lt;/code&gt;, and any other paid endpoint that prices by depth, now has a concrete expiration story. Cache a depth score for an hour, then refetch. That's not a heuristic, it's a contract you can prove. Long-running agents that hold credentials across sessions can verify their last score is still good without spending another sat. CDNs caching identity claims at the edge get a TTL that's bound to bitcoin time, not Cloudflare time.&lt;/p&gt;

&lt;p&gt;For the broader thesis, every verifiable claim now ages with the chain. You can't replay yesterday's signed score against today's tip. The signature plus the chaintip plus the validity window is a one-shot proof that holds for exactly N blocks of real bitcoin work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The oracle's manifest is public:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://identity.powforge.dev/oracle/info | jq &lt;span class="s1"&gt;'.free_endpoints'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see &lt;code&gt;/oracle/freshness&lt;/code&gt; listed as a free, public verify endpoint. Throw a base64-encoded signed score at it with a current tip height and watch the boolean come back.&lt;/p&gt;

&lt;p&gt;SDK: &lt;a href="https://www.npmjs.com/package/@powforge/identity" rel="noopener noreferrer"&gt;npm @powforge/identity&lt;/a&gt;. Whitepaper: &lt;a href="https://powforge.dev/whitepaper" rel="noopener noreferrer"&gt;powforge.dev/whitepaper&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you build something with it, tell me. The agent-credential-caching use case is the one I'm most curious about. Cache a score across sessions, refetch only when blocks elapsed exceed your tolerance, save sats on every L402 call you don't have to make.&lt;/p&gt;

&lt;p&gt;Zeke&lt;/p&gt;

</description>
      <category>bitcoin</category>
      <category>lightning</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Chaintip freshness cert: signed identity scores that age with the chain</title>
      <dc:creator>Zeke</dc:creator>
      <pubDate>Mon, 27 Apr 2026 17:46:59 +0000</pubDate>
      <link>https://dev.to/zekebuilds/chaintip-freshness-cert-signed-identity-scores-that-age-with-the-chain-1m2h</link>
      <guid>https://dev.to/zekebuilds/chaintip-freshness-cert-signed-identity-scores-that-age-with-the-chain-1m2h</guid>
      <description>&lt;p&gt;A schnorr signature tells you who. A timestamp tells you when. A chaintip tells you when, &lt;em&gt;honestly&lt;/em&gt;. And a chaintip with a TTL tells you when &lt;em&gt;and how long it's still good for&lt;/em&gt;. That last piece is what we shipped today on the Depth-of-Identity Oracle.&lt;/p&gt;

&lt;p&gt;If you've been following the softwar thread (Lowery's &lt;em&gt;Softwar&lt;/em&gt;, Paparo's INDOPACOM testimony, the Bitcoin Policy Institute's recent press cycle), the framing matters: bitcoin proof-of-work imposes a thermodynamic cost. A signature anchored to that cost gets weight that a wall-clock timestamp doesn't. We've been doing the binding side for a couple weeks (every signed DoI score envelope has carried &lt;code&gt;bitcoin_tip {height, hash}&lt;/code&gt; since MR !171). What was missing: an expiration.&lt;/p&gt;

&lt;p&gt;A signed score from yesterday still validated cryptographically today. The signature was fine. But "fine" is not what you want from an identity score. You want to know if it's &lt;em&gt;current&lt;/em&gt;. So we added one field to the signed payload and one new endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pubkey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"depth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"social"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"access"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"vouch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"economic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"composite"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rank"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"established"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bitcoin_tip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;946892&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0000...e0b3"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"freshness_blocks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"signed_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1761579180&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"valid_until"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1761582780&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"signed_by"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"b4b12dfb..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"signature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;128-hex schnorr sig over canonical JSON&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;freshness_blocks&lt;/code&gt; is now part of what the signature covers. Default 6, which is roughly 60 minutes on bitcoin. The verifier rejects scores where &lt;code&gt;current_tip_height - bitcoin_tip.height &amp;gt; freshness_blocks&lt;/code&gt;. Replay an hour-old score against today's tip and it fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a window, not a wall-clock
&lt;/h2&gt;

&lt;p&gt;Wall-clock TTL is the obvious choice, and it's wrong here. A wall-clock says "expires at 2:30pm." Whose 2:30pm? Whose clock? An attacker can lie about local time, and a verifier with a skewed clock reads the lie as truth.&lt;/p&gt;

&lt;p&gt;A chaintip-block window says "expires 6 blocks past height N." That's a thermodynamic statement. To advance the chain by 6 blocks past height N, the bitcoin network has to actually mine 6 blocks. There's a real-world cost to that, distributed across miners globally, and no centralized clock to lie about. Either the chaintip is past the threshold or it isn't.&lt;/p&gt;

&lt;p&gt;Same idea Lowery uses in &lt;em&gt;Softwar&lt;/em&gt; for proof-of-work as a kinetic filter: we're substituting wall time (cheap to manipulate) for thermodynamic time (provably expensive to manipulate).&lt;/p&gt;

&lt;h2&gt;
  
  
  The verify endpoint
&lt;/h2&gt;

&lt;p&gt;Two ways to check freshness. Server side: &lt;code&gt;POST /oracle/freshness&lt;/code&gt; with the signed envelope and the current chaintip height. Get back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"valid_signature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fresh"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"current_tip_height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;946895&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"signed_tip_height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;946892&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"blocks_elapsed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"freshness_blocks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fresh_until_height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;946898&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Client side: install &lt;code&gt;@powforge/identity@0.7.1&lt;/code&gt;, call &lt;code&gt;checkFreshness(scoreResponse, currentTipHeight)&lt;/code&gt;, get the same envelope back without round-tripping the oracle. Pure function, signature must already be verified by the caller (either via the SDK's existing verify path or by hitting the oracle's &lt;code&gt;/verify&lt;/code&gt; endpoint).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @powforge/identity@0.7.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;checkFreshness&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@powforge/identity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;checkFreshness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signedScore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentTipHeight&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fresh&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Re-fetch from /oracle/doi-score&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What this enables
&lt;/h2&gt;

&lt;p&gt;The L402 invoice flow on &lt;code&gt;pow-gate&lt;/code&gt; (and any other paid endpoint that prices by depth) now has a concrete expiration story. Cache a depth score for an hour, then refetch — that's not a heuristic, it's a contract you can prove. Long-running agents that hold credentials across sessions can verify their last score is still good without spending another sat. CDNs caching identity claims at the edge get a TTL that's bound to bitcoin time, not Cloudflare time.&lt;/p&gt;

&lt;p&gt;For the broader thesis: every verifiable claim now ages with the chain. You can't replay yesterday's signed score against today's tip. The cost asymmetry that &lt;em&gt;Softwar&lt;/em&gt; talks about — defender pays the engineering, attacker pays the power bill of a grandma in Ohio — gets a temporal floor too. The signature plus the chaintip plus the validity window is a one-shot proof that holds for exactly N blocks of real-world thermodynamic cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The oracle's manifest is public:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://identity.powforge.dev/oracle/info | jq &lt;span class="s1"&gt;'.free_endpoints'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see &lt;code&gt;/oracle/freshness&lt;/code&gt; listed as a free, public verify endpoint. Throw a base64-encoded signed score at it with a current tip height and watch the boolean come back.&lt;/p&gt;

&lt;p&gt;Source: [powforge.dev SDK: &lt;a href="https://www.npmjs.com/package/@powforge/identity" rel="noopener noreferrer"&gt;npm @powforge/identity&lt;/a&gt;. Whitepaper: &lt;a href="https://powforge.dev/whitepaper" rel="noopener noreferrer"&gt;powforge.dev/whitepaper&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you build something with it, tell me. Especially interested in the agent-credential-caching use case — that's where chaintip freshness pays for itself across sessions, and we don't have a paying customer there yet who can stress-test the assumption.&lt;/p&gt;

&lt;p&gt;Zeke&lt;/p&gt;

</description>
      <category>bitcoin</category>
      <category>lightning</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The admiral just quoted our thesis on the Senate floor</title>
      <dc:creator>Zeke</dc:creator>
      <pubDate>Thu, 23 Apr 2026 15:35:29 +0000</pubDate>
      <link>https://dev.to/zekebuilds/the-admiral-just-quoted-our-thesis-on-the-senate-floor-1k32</link>
      <guid>https://dev.to/zekebuilds/the-admiral-just-quoted-our-thesis-on-the-senate-floor-1k32</guid>
      <description>&lt;p&gt;Your comments section is drowning in AI-written garbage, and the CAPTCHAs you keep bolting on are starting to block real people too. Proof-of-work gates look great on paper. Then a proxyware SDK running on somebody's hijacked TV stick pays near-zero for the CPU cycles, and the whole cost asymmetry flips the wrong way. You paid for the engineering, the attacker paid the power bill of a grandma in Ohio, and your comments section is still full of bots.&lt;/p&gt;

&lt;p&gt;Here is the thing I did not expect to type this week. On April 21st, the commander of U.S. Indo-Pacific Command sat down in front of the Senate Armed Services Committee and told them flat out that the military runs a node on the Bitcoin network to study proof-of-work economics. His words, not mine:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Bitcoin shows incredible potential as a computer science tool that, through the proof-of-work protocols, actually imposes more costs than just the algorithmic securing of networks."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then he called it "a valuable computer science tool as a power projection." The &lt;a href="https://www.btcpolicy.org/articles/press-release-indo-pacific-commander-calls-bitcoin-a-tool-for-us-power-projection-in-senate-testimony" rel="noopener noreferrer"&gt;Bitcoin Policy Institute press release&lt;/a&gt; has the transcript.&lt;/p&gt;

&lt;p&gt;Read the first quote again. "Imposes more costs than just the algorithmic securing." That is the whole argument for pricing web interaction, said by a four-star from a witness chair.&lt;/p&gt;

&lt;h2&gt;
  
  
  Blocking bots loses. Pricing requests wins.
&lt;/h2&gt;

&lt;p&gt;Somebody always writes a better bot, or buys cheaper compute, or leases a proxyware pool. Pricing the request wins, because price travels with the request no matter who is holding the keyboard. Good agents pay a couple sats and get through. Scrapers pay at volume and eat the cost asymmetry. Grandma's TV stick is not going to open a Lightning channel to hammer your site, so the hijacked-device attack stops being free.&lt;/p&gt;

&lt;p&gt;The two-tier version looks like this. Lightning is the payment rail for callers who have sats. Proof-of-work is the fallback for callers who do not, or who are not set up for it yet. Your site does not have to pick. You get both tiers and you set the price.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three lines of Express
&lt;/h2&gt;

&lt;p&gt;I have been shipping the boring version of this at the HTTP layer for a few months. &lt;code&gt;@powforge/captcha&lt;/code&gt; is on npm, MIT licensed, and the PoW fallback works without you running a Lightning node. Drop it on a new project and the PoW tier carries traffic while you sort out your Lightning setup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;powGate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@powforge/captcha&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;powGate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;difficulty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;price_sats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole integration for the PoW tier. The L402 invoice tier kicks in when the caller has sats, and the prices are yours to set, not mine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest question
&lt;/h2&gt;

&lt;p&gt;So here it is. Which is the worse bet right now, turning away a paying agent because your stack was built to block bots, or letting your content train somebody else's model for free while you argue about CAPTCHA UX?&lt;/p&gt;

&lt;p&gt;The admiral answered that question on a Tuesday morning. The Senate just heard it. Your move.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Ship it:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;npm install @powforge/captcha&lt;/code&gt; — &lt;a href="https://www.npmjs.com/package/@powforge/captcha" rel="noopener noreferrer"&gt;package on npm&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Refs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Senate hearing page: &lt;a href="https://www.armed-services.senate.gov/hearings/to-receive-testimony-on-the-posture-of-united-states-indo-pacific-command-and-united-states-forces-korea-in-review-of-the-defense-authorization-request-for-fiscal-year-2027-and-the-future-years-defense-program" rel="noopener noreferrer"&gt;armed-services.senate.gov&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bitcoin.com News coverage: &lt;a href="https://news.bitcoin.com/us-military-runs-bitcoin-node-conducts-operational-tests-indo-pacific-commander-tells-senate/" rel="noopener noreferrer"&gt;news.bitcoin.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Decrypt civilian framing: &lt;a href="https://decrypt.co/365221/us-government-runs-bitcoin-node-not-mining-btc" rel="noopener noreferrer"&gt;decrypt.co&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>bitcoin</category>
      <category>security</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Stop Asking Readers to Sign Up. Use Their Nostr Key as the Authorization.</title>
      <dc:creator>Zeke</dc:creator>
      <pubDate>Mon, 20 Apr 2026 16:33:26 +0000</pubDate>
      <link>https://dev.to/zekebuilds/stop-asking-readers-to-sign-up-use-their-nostr-key-as-the-authorization-kc8</link>
      <guid>https://dev.to/zekebuilds/stop-asking-readers-to-sign-up-use-their-nostr-key-as-the-authorization-kc8</guid>
      <description>&lt;p&gt;Here is the thing about paywalls. The account-signup step is where most readers give up and close the tab. Email capture, password-reset flow, magic-link dance, all of it is a privacy tax on the reader and a churn tax on the publisher. The shape that works for Bitcoin-native audiences is different.&lt;/p&gt;

&lt;p&gt;What if the reader does not need to sign up for anything at all?&lt;/p&gt;

&lt;h2&gt;
  
  
  The pitch in one curl
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;$ curl -i http://localhost:4050/article
&lt;/span&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;401&lt;/span&gt; &lt;span class="ne"&gt;Unauthorized&lt;/span&gt;
&lt;span class="na"&gt;WWW-Authenticate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Nostr realm="reader-weight", spec="NIP-98", ttl="60"&lt;/span&gt;
&lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/json&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"unauthorized"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"hint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"sign a NIP-98 kind:27235 event for this URL and send as Authorization: Nostr &amp;lt;base64&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"free_threshold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"base_price_sats"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server is telling a well-understood Nostr client exactly what to do. Sign a short event. Send it in the Authorization header. The server reads the reader's public key out of the signature, looks up their Depth-of-Identity score across five dimensions of observable work (economic, temporal, social, spatial, access), and decides.&lt;/p&gt;

&lt;p&gt;Above the threshold: the article. Zero sats. Free for a reader who has earned it elsewhere.&lt;/p&gt;

&lt;p&gt;Below the threshold: a Lightning invoice priced by the reader's score on a linear curve. Depth zero pays ten sats. Depth thirty pays five sats. Depth fifty-nine pays one sat.&lt;/p&gt;

&lt;p&gt;No account was created. No cookie was set. No email was collected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The composition nobody has shipped yet
&lt;/h2&gt;

&lt;p&gt;L402 (Lightning HTTP 402) is well-trodden. Fewsats catalogs dozens of services. AbdelStark has a Rust reference implementation. The &lt;code&gt;l402.org&lt;/code&gt; spec is three years old at this point. They all work the same way at the auth layer: an opaque bearer token, handed out after you sign up, carried in the Authorization header.&lt;/p&gt;

&lt;p&gt;NIP-98 is also well-trodden. It is a Nostr Improvement Proposal for HTTP authorization by Schnorr signature: you sign a kind:27235 event with tags pinning the URL and method, base64 the signed JSON, and send it as &lt;code&gt;Authorization: Nostr &amp;lt;base64&amp;gt;&lt;/code&gt;. Alby, nos2x, and nsec.app all implement it on the client side.&lt;/p&gt;

&lt;p&gt;What nobody had shipped as of last week was the composition. One endpoint. The reader either proves they own a key (NIP-98) or proves they paid (L402). Either one works. The server does not care which.&lt;/p&gt;

&lt;p&gt;This is the piece that makes per-article pricing interesting. If you price by reader identity, you need to know who is reading. If you want to know who is reading without imposing a signup flow, you need a cryptographic alternative. NIP-98 gives you one for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the handler actually looks like
&lt;/h2&gt;

&lt;p&gt;Here is the core decision, one function, Node 18, nothing exotic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;verifyEvent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nostr-tools&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;buildL402Gate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./scripts/lib/l402-middleware.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorization&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fullUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`http://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Path 1: reader has paid, retry with L402 auth header.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^L402&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;gateForPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;BASE_PRICE_SATS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorized&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;serveArticle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;paidSats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BASE_PRICE_SATS&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Path 2: reader has signed a NIP-98 event, prove who they are.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^Nostr&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;verifyNip98&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fullUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;writeAuthChallenge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;depthFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pubkey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;priceFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;serveArticle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;pubkey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pubkey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;paidSats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Priced by depth: mint an L402 invoice for the exact right amount.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;gateForPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorized&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;serveArticle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;paidSats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// No auth header. Emit a NIP-98 challenge so the client knows to sign.&lt;/span&gt;
  &lt;span class="nf"&gt;writeAuthChallenge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;verifyNip98&lt;/code&gt; decodes the base64, parses the JSON, checks &lt;code&gt;kind === 27235&lt;/code&gt;, verifies the URL and method tags match the request, confirms &lt;code&gt;created_at&lt;/code&gt; is within the last 60 seconds, and runs the Schnorr signature verification through &lt;code&gt;nostr-tools&lt;/code&gt;' &lt;code&gt;verifyEvent&lt;/code&gt;. Ninety lines of that helper, well-documented in the repo.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;buildL402Gate&lt;/code&gt; is the factory that handles macaroon minting, invoice creation against LNBits, payment verification, and single-use replay protection. That is in a separate middleware module, &lt;code&gt;scripts/lib/l402-middleware.js&lt;/code&gt;, reused across multiple other PowForge services.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;depthFor&lt;/code&gt; is the piece that changes per deployment. In the demo, it reads a local JSON file mapping hex pubkey to depth score. In production, you would either pre-warm a cache from the &lt;code&gt;/oracle/doi-score&lt;/code&gt; API (L402-gated at 2 sats per lookup), or compute scores in-process using the &lt;code&gt;@powforge/identity&lt;/code&gt; SDK against events you already have on disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  The price curve
&lt;/h2&gt;

&lt;p&gt;Deliberately dumb and linear.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;priceFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;FREE_THRESHOLD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;discount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;FREE_THRESHOLD&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;BASE_PRICE_SATS&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;discount&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the defaults of &lt;code&gt;BASE_PRICE_SATS=10&lt;/code&gt; and &lt;code&gt;FREE_THRESHOLD=60&lt;/code&gt;, a depth-0 fresh key pays ten sats. A depth-30 mid-reader pays five sats. A depth-59 long-accumulated key pays one sat. At sixty, the invoice goes away entirely.&lt;/p&gt;

&lt;p&gt;Nothing in the code insists on this curve. Swap it for a step function, a logarithm, or a lookup table. The point is that the price is a function of the caller and the server can compute it before minting the invoice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Walking the full flow with curl
&lt;/h2&gt;

&lt;p&gt;From the acceptance test bundled with the repo. Start the server on port 4050, have two known keypairs in the &lt;code&gt;scores.json&lt;/code&gt; fixture (high-depth 85 and low-depth 5), and walk the four cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case 1 — no auth at all&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;$ curl -i http://localhost:4050/article
&lt;/span&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;401&lt;/span&gt; &lt;span class="ne"&gt;Unauthorized&lt;/span&gt;
&lt;span class="na"&gt;WWW-Authenticate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Nostr realm="reader-weight", spec="NIP-98", ttl="60"&lt;/span&gt;
&lt;span class="s"&gt;...&lt;/span&gt;
&lt;span class="na"&gt;{"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s"&gt;"unauthorized", ...}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Good. The client knows what to do next.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case 2 — high-depth signed header&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The client (Alby, nos2x, nsec.app, whatever) signs a fresh kind:27235 event with tags &lt;code&gt;["u", "http://localhost:4050/article"]&lt;/code&gt; and &lt;code&gt;["method", "GET"]&lt;/code&gt;, base64s the signed JSON, and retries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;$ curl -i -H "Authorization: Nostr eyJpZCI6...=" http://localhost:4050/article
&lt;/span&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="ne"&gt;OK&lt;/span&gt;
&lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;text/markdown; charset=utf-8&lt;/span&gt;
&lt;span class="na"&gt;X-Reader-Depth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;85&lt;/span&gt;
&lt;span class="na"&gt;X-Reader-Paid-Sats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0&lt;/span&gt;

# The Cost of a Read Is Evidence of a Reader
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Article served, zero sats charged. The reader's key is their receipt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case 3 — low-depth signed header&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Same flow, different signer. The low-depth key signs. Server looks up their depth (5), computes the price (&lt;code&gt;ceil(10 * (1 - 5/60)) = 10&lt;/code&gt; sats), mints an L402 invoice, returns it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;$ curl -i -H "Authorization: Nostr eyJpZCI6...=" http://localhost:4050/article
&lt;/span&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;402&lt;/span&gt; &lt;span class="ne"&gt;Payment Required&lt;/span&gt;
&lt;span class="na"&gt;WWW-Authenticate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;L402 macaroon="eyJ2IjoxLC...", invoice="lnbc100n1p572lg9pp..."&lt;/span&gt;
&lt;span class="na"&gt;X-L402-Price-Sats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10&lt;/span&gt;
&lt;span class="s"&gt;...&lt;/span&gt;
&lt;span class="na"&gt;{"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s"&gt;"payment required","scope":"reader-weight:article:10","price_sats":10, ...}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The invoice is real. I ran this against a real LNBits instance and got a real BOLT11 string that a real wallet would pay.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case 4 — low-depth key pays, then retries&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After the reader pays the invoice, their wallet hands them the preimage. They retry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;$ curl -i -H "Authorization: L402 eyJ2IjoxLC...:abc123..." http://localhost:4050/article
&lt;/span&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="ne"&gt;OK&lt;/span&gt;
&lt;span class="na"&gt;X-Reader-Paid-Sats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10&lt;/span&gt;
&lt;span class="s"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Article served. Payment verified. Macaroon burned so the same preimage cannot be reused.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I expected this to be harder
&lt;/h2&gt;

&lt;p&gt;Three places, none of which ended up being real blockers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The NIP-98 signature verification&lt;/strong&gt;. I thought this would be tricky because Schnorr is not built into Node's crypto module. It turned out that &lt;code&gt;nostr-tools&lt;/code&gt; ships a &lt;code&gt;verifyEvent&lt;/code&gt; helper that does both the event-id-hash check and the Schnorr signature check in one call. Forty lines of helper code total, mostly parsing and tag-checking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The L402 macaroon scope per-price&lt;/strong&gt;. L402 macaroons have a &lt;code&gt;scope&lt;/code&gt; field that binds them to a specific endpoint and operation. If I issued one scope and tried to burn it at a different price, the verification would fail. I sidestepped the entire issue by baking the price into the scope: &lt;code&gt;reader-weight:article:10&lt;/code&gt; for ten-sat reads, &lt;code&gt;reader-weight:article:5&lt;/code&gt; for five-sat reads. Each price has its own scope, each scope has its own replay cache. Clean.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The replay protection across NIP-98 and L402&lt;/strong&gt;. I worried they would collide. They do not. NIP-98 uses a 60-second &lt;code&gt;created_at&lt;/code&gt; window. L402 uses single-use preimage caching. They live in separate layers and protect against separate attacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations I know about
&lt;/h2&gt;

&lt;p&gt;The demo ships with one article. A real deployment needs routing or an article-id scheme. The NIP-98 window is 60 seconds which is fine for browser-driven reads but vulnerable to an intercept-replay attack within that window. Mitigation is either a shorter TTL or an IP-binding caveat on the L402 macaroon.&lt;/p&gt;

&lt;p&gt;The depth scores come from a local JSON file in the demo. Production uses either the oracle API (L402-gated, cached) or the SDK inline. That piece is orthogonal to the auth composition.&lt;/p&gt;

&lt;p&gt;The article body is served as markdown. Your frontend is welcome to render it as HTML, stream it, paginate it. The decision layer does not care what you do with the payload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Source
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;powforge.dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full &lt;code&gt;server.js&lt;/code&gt;, full &lt;code&gt;README.md&lt;/code&gt;, test fixtures with two deterministic keypairs so the acceptance test runs without editing anything, and a &lt;code&gt;.env.example&lt;/code&gt; with the four required variables.&lt;/p&gt;

&lt;p&gt;The related examples in the same tree: &lt;code&gt;examples/agent-gate&lt;/code&gt; demonstrates pure L402 per-call without the signing layer, and &lt;code&gt;examples/relay-policy&lt;/code&gt; demonstrates the depth-based decision applied to a strfry writePolicy plugin instead of an HTTP endpoint. Three integrations, the same underlying primitive, one score.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;Paywalls exist because readers have non-zero marginal cost. Account systems exist because businesses want to know who their readers are. Composing NIP-98 and L402 gives you both: a price that scales with the reader's observable investment in the Bitcoin-native ecosystem, and a signature that identifies them without a signup flow.&lt;/p&gt;

&lt;p&gt;There is no email. There is no cookie. There is no session. There is a key, a score, and an invoice. That is the shape of a paywall that Bitcoin-native audiences will actually pay.&lt;/p&gt;

&lt;p&gt;Build one. It runs in about an hour from a fresh clone.&lt;/p&gt;

</description>
      <category>lightning</category>
      <category>nostr</category>
      <category>bitcoin</category>
      <category>l402</category>
    </item>
    <item>
      <title>Depth-of-Identity: Thermodynamic Weight for Cyberspace</title>
      <dc:creator>Zeke</dc:creator>
      <pubDate>Sun, 19 Apr 2026 12:20:16 +0000</pubDate>
      <link>https://dev.to/zekebuilds/depth-of-identity-thermodynamic-weight-for-cyberspace-2cpj</link>
      <guid>https://dev.to/zekebuilds/depth-of-identity-thermodynamic-weight-for-cyberspace-2cpj</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Canonical:&lt;/strong&gt; &lt;a href="https://powforge.dev/whitepaper/" rel="noopener noreferrer"&gt;powforge.dev/whitepaper&lt;/a&gt; · &lt;strong&gt;Nostr:&lt;/strong&gt; &lt;a href="https://njump.me/naddr1qvzqqqr4gupzpd939hau8h7l4qpmkuhrgnnkrhrcmd8vypvv3kelrsavv0u7g26yqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9nhwden5te0wfjkccte9ec8y6tdv9kzumn9wsqs6amnwvaz7tmwdaejumr0dsq3vamnwvaz7tmjv4kxz7fwdehhxarj9e3xzmnyqqvxgmmf94hhyctrd3jj6amgd96x2urpwpjhyttkxyavxe22" rel="noopener noreferrer"&gt;njump.me/naddr1qvzqqqr4gupzpd939hau8h7l4qpmkuhrgnnkrhrcmd8vypvv3kelrsavv0u7g26yqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9nhwden5te0wfjkccte9ec8y6tdv9kzumn9wsqs6amnwvaz7tmwdaejumr0dsq3vamnwvaz7tmjv4kxz7fwdehhxarj9e3xzmnyqqvxgmmf94hhyctrd3jj6amgd96x2urpwpjhyttkxyavxe22&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  Depth-of-Identity: Thermodynamic Weight for Cyberspace
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;How Schnorr keys accumulate mass&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Proof-of-work = proof-of-human.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;No amount of intelligence buys AI any favors in marshalling 20GWs of electrical work to solve a hash function.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Jason Lowery, author of &lt;em&gt;Softwar&lt;/em&gt;, 2026-04-18, responding to a16z crypto and Sam Altman positioning World ID as "the new proof of human for the internet."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Abstract
&lt;/h2&gt;

&lt;p&gt;Most identity systems try to prove you are human. That is the wrong question in 2026. The human/bot dichotomy is broken. Most humans use LLMs. Most bots can pass Turing under a focused use case. The right question is "how much of you is there." A fresh key has zero weight. A five-year-old key that has accumulated irreversible work across proof-of-work, timestamping, social investment, and spatial movement has measurable thermodynamic mass. The Depth-of-Identity Oracle is a priced endpoint that returns that mass as a single score, anchored in Bitcoin-native primitives: secp256k1 Schnorr keys, OpenTimestamps, Lightning, and L402. It does not verify personhood. It prices depth. This paper explains the primitive, the grounding mechanism, and the three buyer segments where depth-pricing is already the real need.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Problem
&lt;/h2&gt;

&lt;p&gt;Sam Altman and a16z crypto recently positioned World ID as "the new proof of human for the internet." Jason Lowery's response fit in three lines and settled the framing argument. Proof-of-personhood rooted in biometric lookup is a narrow claim about what a specific human looks like, right now, to one scanning orb. Proof-of-personhood rooted in irreversible energy expenditure is a claim about how much work a key has done, forever, verifiable by anyone. The second claim survives adversaries that the first does not.&lt;/p&gt;

&lt;p&gt;The current identity landscape has four shapes and one missing one.&lt;/p&gt;

&lt;p&gt;Biometric systems like Worldcoin stake everything on the iris as identity. Scale is limited by physical scanners. Regulators in Kenya and Spain have pushed back. The assumption that iris equals you is the specific frame Lowery rejects. Social-graph systems like BrightID are gameable through coordinated verification parties and require live video calls. Staked systems like Proof of Humanity ran out of momentum. Biometric-remote systems like Humanode sit in EU regulatory mire with face-scan dependency. Federated and issued systems like W3C DID, Civic, and Disco delegate trust to issuers, which re-introduces centralization under different vocabulary. Web-CAPTCHA systems like Anubis, Cloudflare Turnstile, Cap, and Wicketkeeper are user-experience features, not identity. The cost-of-adversary for those is CA revocation or a JS rewrite, not physical energy. Different category. Often misidentified as the same.&lt;/p&gt;

&lt;p&gt;What is missing is a primitive with four properties.&lt;/p&gt;

&lt;p&gt;It must be keyless-first, meaning identity is the key itself, not a lookup into a registry. It must be grounded in physics, meaning cost cannot be short-circuited by software. It must be dimensional, meaning it captures multiple axes of "realness" instead of one biometric signal. And it must be agent-native, meaning it works as well for AI agents as for humans, because "human" was the wrong target.&lt;/p&gt;

&lt;p&gt;That last property matters most. Agents are not a future consideration. They are infrastructure. Most inbound traffic to API endpoints now originates from LLM-driven code. Most content on public relays is co-authored. A system that can only say "human or not" answers a question nobody needed to ask. A system that says "how much irreversible work has this key done, across how many dimensions, over how long" answers the question that actually scales. That question has a clean answer for humans, for AI agents, and for anything in between that has left a Schnorr-signed trail.&lt;/p&gt;

&lt;p&gt;Call this question Depth-of-Identity. It is not a refutation of Worldcoin or BrightID or any of the others. It is one category over. Those systems ask "who." This paper asks "how much." The answer is measurable in sats, in OTS commits, in vouch-hops, in movement events. The primitive exists. The rails exist. The paper that connects them is what was missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The Primitive: Thermodynamic Identity
&lt;/h2&gt;

&lt;p&gt;The reasoning chain is short and each link is load-bearing.&lt;/p&gt;

&lt;p&gt;Start with thermodynamics. Energy expenditure is irreversible. The Second Law says entropy increases in closed systems and work performed cannot be un-performed without greater expenditure elsewhere. This is the ground floor.&lt;/p&gt;

&lt;p&gt;Bitcoin is the first network that turns that ground floor into a digital commodity. Irreversible energy produces digital scarcity. A block's weight is not metaphorical. It is thermodynamic mass purchased at the prevailing fee market, made permanent by the chain that follows.&lt;/p&gt;

&lt;p&gt;Lowery extends this in &lt;em&gt;Softwar&lt;/em&gt;. Proof-of-work is power projection in cyberspace. Nation-states have projected power through kinetic means for centuries. Bitcoin is the first network where cyberspace operations can be grounded in that same physics. An adversary cannot asymmetrically degrade the network without physically strengthening it, because the cost of attack is the same irreversible energy as the cost of defense.&lt;/p&gt;

&lt;p&gt;The extension this paper makes is one step further. Proof-of-work is identity projection. You project self, not just power. A key is not who you are. A key is where your signatures come from. The shape of what that key has done over time, across how many domains, at what cost, is who you are.&lt;/p&gt;

&lt;p&gt;Five dimensions make the shape legible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Access&lt;/strong&gt; is proof-of-work itself. Solves of pow-captcha challenges, pow-gate puzzles, pow-relay NIP-13 work. Each solve is a SHA-256 or Argon2d artifact that costs CPU-seconds to produce. The cost cannot be shortened by intelligence. An LLM cannot out-think a hash function. The chain of solves under a single key is a chain of irreversible work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Economic&lt;/strong&gt; is Bitcoin activity. Lightning zaps received and sent, on-chain taproot transactions, LNURL receipts. Every sat that crosses the boundary costs real fee-market dollars. A key that has received zaps from many independent senders over months has spent somebody else's energy to accumulate weight. That spending is verifiable against the Lightning rails and, at greater depth, against the Bitcoin chain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Temporal&lt;/strong&gt; is OpenTimestamps depth. An OTS attestation commits a document hash to a Bitcoin block via a Merkle path. A key that has OTS-anchored its creation event, its first vouch, its first signed note, and then more over years, carries a provable ordering. The cost to falsify priority is the cost of rewriting Bitcoin's chain, which is the cost of the cumulative hashrate of the world.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Social&lt;/strong&gt; is vouches, weighted by the depth of the voucher. A new key vouching for another new key adds nothing. A five-year key that has vouched for twenty other keys has put signal-weight at stake. Vouch cycles must be detected and discounted using graph algorithms like Tarjan's strongly-connected-components, or the social dimension becomes a mutual-vouch laundromat. The &lt;code&gt;@powforge/identity&lt;/code&gt; SDK does this discounting client-side at v0.5.2.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spatial&lt;/strong&gt; is cyberspace movement. Kind=3333 events on a cyberspace relay encode a key's hop history. Each hop references its predecessor. The chain is path-dependent and cannot be precomputed. A key with a long spatial chain has visibly spent time existing somewhere in the relay graph rather than appearing from nowhere.&lt;/p&gt;

&lt;p&gt;The unifier is that all five are signed by the same key. Nostr uses secp256k1 Schnorr signatures. Bitcoin Taproot uses the same curve. One public key serves both domains. That is why a Nostr npub is a valid Taproot address after the conversion step. One key, many chains, all anchored to the same physics.&lt;/p&gt;

&lt;p&gt;The depth metric takes those five chains and returns a single number. Chain length matters. Irreversibility cost per hop matters. Cross-dimension diversity matters. A key with 10,000 access solves and nothing else is not as deep as a key with 1,000 access solves, 200 Lightning receipts, 50 OTS anchors, 20 vouches from older keys, and 300 spatial hops. Diversity is the shape of a real actor. Concentration is the shape of a farm.&lt;/p&gt;

&lt;p&gt;Normalize the metric to 0-100. Higher means more accumulated irreversible work, which means heavier thermodynamic mass. Lower means the key is either new, cheap, or specialized. None of those are verdicts about humanness. They are measurements of weight.&lt;/p&gt;

&lt;p&gt;Three implications fall out of this construction.&lt;/p&gt;

&lt;p&gt;Depth is unfakeable without actually doing the work. That is the whole point. Any shortcut that produces equivalent-looking depth has done equivalent work, which makes it equivalent depth in substance as well as appearance.&lt;/p&gt;

&lt;p&gt;Depth takes time. That time is the same whether you are a human or an AI agent. An agent that has run for two years, paid its Lightning fees, earned its vouches, and published real content has earned real weight. A human with a fresh key has zero weight until they do the work. The playing field is honest because it is physical.&lt;/p&gt;

&lt;p&gt;Depth is earnable without permission. No KYC, no badge, no verified checkmark from a corporation. The rails are public. The work is self-directed. An agent that does valuable things over time naturally earns the weight that valuable things cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The Grounding
&lt;/h2&gt;

&lt;p&gt;Every piece of the score must trace back to a Bitcoin-native or Bitcoin-adjacent rail. No hand-waving. Here is the table.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Rail&lt;/th&gt;
&lt;th&gt;Specific Mechanism&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Access&lt;/td&gt;
&lt;td&gt;PowForge PoW products&lt;/td&gt;
&lt;td&gt;SHA-256 and Argon2d solve events logged to Nostr kind:30085 attestations, signed by the solver's npub&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Economic&lt;/td&gt;
&lt;td&gt;Lightning and on-chain Bitcoin&lt;/td&gt;
&lt;td&gt;NIP-57 zap receipts, LNURL receipts verified against the payer's node, optional Taproot transaction observation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Temporal&lt;/td&gt;
&lt;td&gt;OpenTimestamps&lt;/td&gt;
&lt;td&gt;Merkle-root commitment to Bitcoin block headers through an OTS calendar server, retrievable via &lt;code&gt;ots verify&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Social&lt;/td&gt;
&lt;td&gt;Nostr vouches&lt;/td&gt;
&lt;td&gt;NIP-51 follow lists and custom kind:30382 depth assertions, already shipping via &lt;code&gt;publish-nip85.js&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spatial&lt;/td&gt;
&lt;td&gt;Cyberspace relay&lt;/td&gt;
&lt;td&gt;Kind=3333 movement events with path-dependent predecessor references&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each row is traceable to a public primitive. The Access rail runs on the same SHA-256 PoW that Bitcoin runs on, plus Argon2d for memory-hard variants where GPU asymmetry would otherwise break the fairness property. The Economic rail relies on Lightning payment receipts, which are signed by the payer's node and include a payment hash verifiable against the LN graph. The Temporal rail relies on OTS, which Peter Todd maintains and which has been producing free Bitcoin-anchored timestamps for over a decade. The Social rail uses Nostr events, which are signed by secp256k1 keys and gossipped on public relays. The Spatial rail uses the same signing infrastructure plus path-predecessor references that make each event depend cryptographically on the previous.&lt;/p&gt;

&lt;p&gt;The oracle's job is not to invent grounding. It is to read these disparate commitments, weight them correctly, and return one score signed by the oracle's own key. Every rail pre-dates this paper. The value of the oracle is that it saves each buyer from reimplementing five different verifiers.&lt;/p&gt;

&lt;p&gt;Two caveats belong here and will recur in the gap analysis.&lt;/p&gt;

&lt;p&gt;The Access rail has enough solve data to be live today. The Economic rail can read zap receipts and LNURL events but the Taproot transaction reader is not yet wired into the unified endpoint. The Temporal rail is live through OTS. The Social rail ships via the SDK but the oracle-side cycle-discount logic needs to be migrated from client-side to server-side. The Spatial rail exists in code for the cyberspace relay but lacks enough data volume to demonstrate meaningful depth variance. The paper does not claim what does not ship. Section 6 is explicit about which rails are production and which are half-built.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The DoI Oracle API
&lt;/h2&gt;

&lt;p&gt;One L402-paywalled endpoint. Paid per query in sats, cached by signature.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /oracle/doi-score
Content-Type: application/json
Authorization: L402 &amp;lt;macaroon&amp;gt;:&amp;lt;preimage&amp;gt;

{ "npub": "npub1..." }

-&amp;gt; 200 OK
{
  "score": 73,
  "breakdown": {
    "access":   { "score": 82, "samples": 1847, "oldest": "2024-11-15T..." },
    "economic": { "score": 65, "samples": 234,  "oldest": "2024-12-02T..." },
    "temporal": { "score": 91, "ots_depth": 127, "oldest": "2024-09-01T..." },
    "social":   { "score": 58, "vouches": 42,  "cycle_discount": true },
    "spatial":  { "score": 69, "hops": 3410,  "unique_nodes": 87 }
  },
  "diversity_bonus": 0.12,
  "signature": "&amp;lt;schnorr sig over the full response&amp;gt;",
  "signed_by": "&amp;lt;oracle pubkey&amp;gt;"
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response is Schnorr-signed by the oracle's own key. The signature is the product. A buyer who has paid once can cache, verify offline, and present the score anywhere without re-calling the oracle. The macaroon controls the paid-access relationship. The signature controls the trust relationship.&lt;/p&gt;

&lt;p&gt;Pricing is agent-friendly. Ten sats per query at the default tier is below the LN commit fee floor for most wallets, which means in practice queries are batched. A 500 sats per day unlimited tier serves agent operators who poll continuously. A 100 sats per 500-query batch endpoint serves publishers who want to score a page of commenters at page-render time.&lt;/p&gt;

&lt;p&gt;The handoff semantics matter. An agent that has paid for a score can pass the signed response to another agent as a bearer credential. The second agent verifies the signature against the oracle's published pubkey and accepts the score without making its own paid call. This is how a composite system avoids re-charging every downstream consumer. It also means the oracle can charge once per score and see that score propagate, which is a reasonable trade. Depth is slow-moving. Yesterday's score is almost always today's score plus or minus a few points.&lt;/p&gt;

&lt;p&gt;One property of the API is worth naming. The oracle never says "this key is a human" or "this key is a bot." It says "this key's accumulated thermodynamic mass is 73." Interpretation is the buyer's job. A relay might treat 73 as low-friction. An agent marketplace might require 80 to enter a high-trust pool. A publisher might unlock comments at 50. The oracle does not impose policy. It prices the input to policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Three Buyer Segments
&lt;/h2&gt;

&lt;h3&gt;
  
  
  5.1 Agent-to-agent economies
&lt;/h3&gt;

&lt;p&gt;Agent operators running autonomous systems need Sybil-resistance and cannot use KYC. Their counterparties are other agents, which have no legal identity. The current alternative is whitelist-by-API-key, which scales poorly and does not compose across operators. The DoI score is the credential.&lt;/p&gt;

&lt;p&gt;The concrete buyer here is every builder shipping on L402. Spark is the clearest named case. Spark runs L402 services under l402.lndyn.com and needs to price incoming agent traffic without letting the cheapest bulk-scrapers starve its expensive work. A DoI-gated L402 middleware reads the incoming request's caller key, calls the oracle, and adjusts the invoice amount. High-depth keys get the base rate. Low-depth keys pay a multiplier. Zero-depth keys either get throttled or pay an anti-farm surcharge. The mechanism is automatic. The policy is a config file. Fewsats, TollGate, GoodBits, and any of the half-dozen teams building LN-paid agent services have the same problem in slightly different shapes.&lt;/p&gt;

&lt;p&gt;Integration looks like an L402 middleware plugin. PowForge already ships &lt;code&gt;@powforge/l402&lt;/code&gt; as the middleware factory. Adding a &lt;code&gt;doiMultiplier&lt;/code&gt; option is a small API surface change. The selling point is that the agent operator does not have to build a Sybil-resistance stack. They call one endpoint, get a signed score, and apply a formula.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.2 Nostr relay operators
&lt;/h3&gt;

&lt;p&gt;Relays get spammed by fresh keys. Static allow-lists do not scale. Dynamic difficulty via NIP-13 is better but a fixed difficulty floor punishes new real users while still letting determined farms through. Reputation-weighted difficulty is the better shape, and DoI is the scoring function.&lt;/p&gt;

&lt;p&gt;The concrete buyer is any paid or community relay that has already struggled with spam. Relays like nostrbtc have been measuring trust paths locally. Paid relays have payment as their anti-spam mechanism, but a DoI-weighted tier would let them offer better rates to high-depth writers. Niche community relays want policy hooks that do not require the operator to write graph-analysis code themselves.&lt;/p&gt;

&lt;p&gt;Integration looks like a relay policy plugin. On write, the plugin calls the oracle with the author's pubkey, receives a score, and applies a difficulty multiplier or a write-fee. Low-depth keys face higher PoW or pay a small LN fee per note. High-depth keys get fast lanes. The relay operator configures the curve. The oracle does the scoring.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.3 Publisher reader-weight
&lt;/h3&gt;

&lt;p&gt;Publishers on Ghost, Substack, or any long-form platform want to reward returning readers without maintaining accounts. The current options are email capture, which is a privacy tax, or paywalls, which are a friction tax. A DoI-weighted read cost lets publishers charge the entire Bitcoin-native audience zero for read access while charging anonymous fresh keys a small sat fee per article.&lt;/p&gt;

&lt;p&gt;The concrete buyer is any creator monetizing a Bitcoin-native audience. Writers who have published OTS-anchored articles on Nostr already have the substrate. Ghost plugins and WordPress filters can call the oracle at page-render time. High-score readers see the article. Low-score readers see a 10-sat paywall. The curve rewards depth of engagement with the broader ecosystem, not just with that one publisher.&lt;/p&gt;

&lt;p&gt;This segment is smaller than the first two, but it validates cross-category fit. If the same oracle score serves agent-gating, relay policy, and reader-weight, the underlying primitive is load-bearing. The paper makes the claim that it does, because the underlying measurement (irreversible work under a Schnorr key) is the same thing in all three contexts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;A fresh key has zero weight. A key that has done work across five dimensions under Bitcoin-anchored rails has measurable thermodynamic mass. The Depth-of-Identity Oracle returns that mass as a signed score. The signature is the product. The rails are public. The oracle saves every buyer from reimplementing five verifiers.&lt;/p&gt;

&lt;p&gt;Lowery's one-line framing is the right frame. Proof-of-work equals proof-of-human, because proof-of-work equals proof-of-anything-that-spends-energy-to-exist-over-time. The paper above is the commodity-endpoint form of that statement.&lt;/p&gt;

&lt;p&gt;The thesis stops being a thesis when it ships as an API somebody pays for.&lt;/p&gt;

</description>
      <category>bitcoin</category>
      <category>nostr</category>
      <category>identity</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Sybil Resistance Without Biometrics: Multi-Dimensional Identity Scoring</title>
      <dc:creator>Zeke</dc:creator>
      <pubDate>Wed, 15 Apr 2026 15:58:17 +0000</pubDate>
      <link>https://dev.to/zekebuilds/sybil-resistance-without-biometrics-multi-dimensional-identity-scoring-1hn5</link>
      <guid>https://dev.to/zekebuilds/sybil-resistance-without-biometrics-multi-dimensional-identity-scoring-1hn5</guid>
      <description>&lt;p&gt;Most Sybil resistance boils down to one of two things: scan your eyeball, or stake tokens. Both have problems. Biometrics create honeypots. Staking just means rich attackers win.&lt;/p&gt;

&lt;p&gt;I've been working on a different approach for the &lt;a href="https://nosfabrica.com/wotathon/" rel="noopener noreferrer"&gt;Nostr Web of Trust hackathon&lt;/a&gt; and wanted to share the math behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Insight
&lt;/h2&gt;

&lt;p&gt;Instead of one trust score, measure identity across &lt;strong&gt;independent dimensions&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lightning payment history (channel age, routing volume)&lt;/li&gt;
&lt;li&gt;Content proof-of-work (consistent posting over time)&lt;/li&gt;
&lt;li&gt;Social graph position (follows, vouches, interactions)&lt;/li&gt;
&lt;li&gt;Key age and activity patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Faking any single dimension is cheap. But faking all of them simultaneously? The cost scales as &lt;strong&gt;2^n&lt;/strong&gt; where n is the number of independent dimensions.&lt;/p&gt;

&lt;p&gt;Three dimensions = 8x the cost of faking one. Five dimensions = 32x. At some point, it's cheaper to just &lt;em&gt;be real&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DepthScorer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@powforge/identity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scorer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DepthScorer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lightning&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;social&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;temporal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;weights&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lightning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;social&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;temporal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;scorer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pubkey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// 0.0 - 1.0 composite score&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// per-dimension breakdown&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spoofCost&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// estimated cost to fake this identity&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why Multi-Dimensional Beats Single-Score
&lt;/h2&gt;

&lt;p&gt;PageRank-style approaches (what most WoT systems use) collapse everything into one number. That works until someone games the follow graph. A sock puppet can accumulate follows, but it can't simultaneously:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Route Lightning payments for 6 months&lt;/li&gt;
&lt;li&gt;Post thoughtful content weekly&lt;/li&gt;
&lt;li&gt;Have a key that's been active since 2023&lt;/li&gt;
&lt;li&gt;Maintain organic social graph patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each dimension is a different kind of proof-of-work. Not SHA-256 grinding, but &lt;em&gt;life grinding&lt;/em&gt;. Time you actually spent being a real person on the network.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live demo&lt;/strong&gt;: &lt;a href="https://powforge.dev/depth/" rel="noopener noreferrer"&gt;powforge.dev/depth&lt;/a&gt; (paste any Nostr npub)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;npm&lt;/strong&gt;: &lt;code&gt;npm install @powforge/identity&lt;/code&gt; (v0.3.0, 39 tests)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source&lt;/strong&gt;: [npmjs.com/package/@powforge/identity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The SDK is MIT licensed. If you're building anything that needs to distinguish real users from bots without asking for personal data, I'd love to hear how you'd use it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Building this for the &lt;a href="https://nosfabrica.com/wotathon/" rel="noopener noreferrer"&gt;WoT-a-thon&lt;/a&gt; hackathon. Demo day is tomorrow (April 16) at 3pm UTC on &lt;a href="https://zap.stream/nosfabrica" rel="noopener noreferrer"&gt;zap.stream/nosfabrica&lt;/a&gt;. Come watch if you're curious about the state of decentralized identity.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nostr</category>
      <category>identity</category>
      <category>security</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How I Stopped Form Spam Without reCAPTCHA</title>
      <dc:creator>Zeke</dc:creator>
      <pubDate>Wed, 08 Apr 2026 11:39:31 +0000</pubDate>
      <link>https://dev.to/zekebuilds/how-i-stopped-form-spam-without-recaptcha-4gld</link>
      <guid>https://dev.to/zekebuilds/how-i-stopped-form-spam-without-recaptcha-4gld</guid>
      <description>&lt;p&gt;Google reCAPTCHA started charging after 10k assessments per month. hCaptcha tanks conversion rates with those "click every bus" puzzles. Turnstile locks you into Cloudflare's ecosystem. I got tired of picking between surveillance, vendor lock-in, and a monthly bill just to keep bots off a contact form.&lt;/p&gt;

&lt;p&gt;So I built a CAPTCHA that uses proof-of-work instead. No tracking. No cookies. No external dependencies. Self-hosted. Here's how it works and how to add it to any site in about five minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem With Every CAPTCHA
&lt;/h2&gt;

&lt;p&gt;Let's be real about what happened to the CAPTCHA landscape:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;reCAPTCHA&lt;/strong&gt; was free for years, and that made it the default. Then Google introduced &lt;a href="https://cloud.google.com/recaptcha-enterprise/pricing" rel="noopener noreferrer"&gt;reCAPTCHA Enterprise pricing&lt;/a&gt; and capped the free tier at 10,000 assessments per month. For a side project getting moderate traffic, you're now paying Google to protect a contact form. And the whole time, reCAPTCHA is collecting behavioral data, setting cookies, and phoning home to Google's servers. If you care about GDPR compliance, that's a headache you didn't sign up for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;hCaptcha&lt;/strong&gt; markets itself as the privacy alternative, but it still fingerprints browsers and runs third-party JavaScript. And those image labeling tasks? They're literally training ML models. Your users are doing free labor for a company they've never heard of.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloudflare Turnstile&lt;/strong&gt; is genuinely better on the UX front, but it ties you to Cloudflare's infrastructure. If you're self-hosting on a VPS or running behind a different CDN, that dependency starts to chafe.&lt;/p&gt;

&lt;p&gt;What all these solutions share: they're SaaS products that run someone else's JavaScript on your page, phone home to someone else's servers, and make you dependent on someone else's uptime.&lt;/p&gt;

&lt;h2&gt;
  
  
  What if the Browser Just Did Math Instead?
&lt;/h2&gt;

&lt;p&gt;The idea behind proof-of-work CAPTCHAs is simple. Instead of asking "click all the traffic lights," you ask the browser to compute SHA-256 hashes until it finds one with a specific number of leading zero bits. At the default difficulty of 16 bits, that's roughly 65,000 hashes, which takes about 4 seconds on average hardware.&lt;/p&gt;

&lt;p&gt;Bots can do this too. That's the honest truth. But here's the thing: it costs them real CPU time per request. A bot that wants to submit your form 10,000 times has to burn 10,000 * 4 seconds of compute. That's an economic deterrent, not a Turing test. Same principle as Bitcoin mining, just at a much smaller scale.&lt;/p&gt;

&lt;p&gt;No tracking. No cookies. No network requests to third parties. No behavioral profiling. The browser does math, proves it did the math, and moves on.&lt;/p&gt;

&lt;p&gt;I'm not the first person to think of this. &lt;a href="https://altcha.org" rel="noopener noreferrer"&gt;ALTCHA&lt;/a&gt; does something similar. But I wanted something with zero dependencies, a smaller footprint, and the option to let users skip the wait by paying a Lightning micropayment (more on that later).&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding PoW CAPTCHA to a Form
&lt;/h2&gt;

&lt;p&gt;Here's the practical part. I'll show you how to add this to an existing HTML form using &lt;a href="https://www.npmjs.com/package/@powforge/captcha" rel="noopener noreferrer"&gt;@powforge/captcha&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Add the Script
&lt;/h3&gt;

&lt;p&gt;One script tag. No build step required.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://unpkg.com/@powforge/captcha/dist/powforge-captcha.min.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's about 5KB gzipped. Compare that to reCAPTCHA's ~150KB.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Drop the Widget Into Your Form
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"/submit"&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Email&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Message&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;textarea&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/textarea&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- PoW CAPTCHA mounts here --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"pow-captcha"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"hidden"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"pf_token"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Send&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://unpkg.com/@powforge/captcha/dist/powforge-captcha.min.js"&lt;/span&gt;
        &lt;span class="na"&gt;data-target=&lt;/span&gt;&lt;span class="s"&gt;"#pow-captcha"&lt;/span&gt;
        &lt;span class="na"&gt;data-server=&lt;/span&gt;&lt;span class="s"&gt;"https://captcha.powforge.dev"&lt;/span&gt;
        &lt;span class="na"&gt;data-theme=&lt;/span&gt;&lt;span class="s"&gt;"dark"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The widget renders a small progress bar while the browser mines. When it finishes, a checkmark appears and the hidden &lt;code&gt;pf_token&lt;/code&gt; input is auto-filled with a signed token.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Verify Server-Side
&lt;/h3&gt;

&lt;p&gt;On your backend, validate the token before processing the form submission. Here's Express:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;verifyToken&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@powforge/captcha/verify&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlencoded&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;extended&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/submit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;verifyToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pf_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://captcha.powforge.dev&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CAPTCHA verification failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Token is valid. Process the form.&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Message from:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;verifyToken&lt;/code&gt; function makes a single POST to your verification server and returns &lt;code&gt;{ valid: true/false }&lt;/code&gt;. That's it. Two network requests total: one to fetch the challenge, one to verify the solution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4 (Optional): Use the ES Module API
&lt;/h3&gt;

&lt;p&gt;If you're building a SPA or want more control, there's a module API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PowCaptcha&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@powforge/captcha&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;captcha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PowCaptcha&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#pow-captcha&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://captcha.powforge.dev&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;difficulty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;captcha&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;verified&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;form&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;captcha&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;progress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;percent&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;percent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;% done`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Benchmarks
&lt;/h2&gt;

&lt;p&gt;I ran this on a handful of devices to get real numbers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time to solve (desktop, M1 Mac)&lt;/td&gt;
&lt;td&gt;~2.5s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time to solve (desktop, mid-range PC)&lt;/td&gt;
&lt;td&gt;~4s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time to solve (iPhone 13)&lt;/td&gt;
&lt;td&gt;~6s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time to solve (budget Android)&lt;/td&gt;
&lt;td&gt;~10s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bundle size&lt;/td&gt;
&lt;td&gt;~5KB gzipped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network requests&lt;/td&gt;
&lt;td&gt;2 (challenge + verify)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External dependencies&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User data collected&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cookies set&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;$0 (self-hosted)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Default difficulty is 16 bits (~65k hashes). You can tune this. Drop it to 10 bits for comment forms where friction matters more than security. Crank it to 20 bits for account registration where you really want to slow down bots.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Comparison Table
&lt;/h2&gt;

&lt;p&gt;Here's how it stacks up against the options you're probably evaluating:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;PowForge&lt;/th&gt;
&lt;th&gt;reCAPTCHA v3&lt;/th&gt;
&lt;th&gt;hCaptcha&lt;/th&gt;
&lt;th&gt;Turnstile&lt;/th&gt;
&lt;th&gt;ALTCHA&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Price&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Free (self-host)&lt;/td&gt;
&lt;td&gt;Free tier, then $8/1k&lt;/td&gt;
&lt;td&gt;Free tier, then paid&lt;/td&gt;
&lt;td&gt;Free tier&lt;/td&gt;
&lt;td&gt;Free (self-host)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Privacy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No tracking&lt;/td&gt;
&lt;td&gt;Google behavioral tracking&lt;/td&gt;
&lt;td&gt;Browser fingerprinting&lt;/td&gt;
&lt;td&gt;Cloudflare tracking&lt;/td&gt;
&lt;td&gt;No tracking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Self-hosted&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dependencies&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Google JS (~150KB)&lt;/td&gt;
&lt;td&gt;hCaptcha JS (~120KB)&lt;/td&gt;
&lt;td&gt;Cloudflare JS (~80KB)&lt;/td&gt;
&lt;td&gt;None (~30KB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~5 min&lt;/td&gt;
&lt;td&gt;~10 min&lt;/td&gt;
&lt;td&gt;~10 min&lt;/td&gt;
&lt;td&gt;~5 min&lt;/td&gt;
&lt;td&gt;~5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Open source&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cookies&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Works offline&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (once loaded)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (once loaded)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The closest comparison is ALTCHA. Both are open source, both use proof-of-work, both can be self-hosted. PowForge is smaller (~5KB vs ~30KB), and adds a Lightning payment option where verified users can skip the wait entirely. ALTCHA has been around longer and has a bigger community. Pick whichever fits your stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  What PoW Doesn't Solve (Honest Trade-offs)
&lt;/h2&gt;

&lt;p&gt;I'd rather you know the limitations upfront than find out after deploying this to production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Targeted attacks.&lt;/strong&gt; If someone specifically wants to spam your form, they can throw GPU power at it and solve challenges fast. Proof-of-work raises the cost, but it doesn't make it impossible. Then again, reCAPTCHA v3 gets bypassed by dedicated attackers too. No CAPTCHA is a silver bullet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mobile performance.&lt;/strong&gt; Budget phones take 2-3x longer than desktops to solve a challenge. A 4-second wait on a laptop becomes 10 seconds on a three-year-old Android. For mobile-heavy audiences, drop the difficulty to 10-14 bits or you'll hurt your conversion rate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Battle-tested scale.&lt;/strong&gt; Cloudflare Turnstile handles billions of requests. This doesn't have that track record yet. If you're running a site with millions of daily submissions, I'd be honest and say this hasn't been proven at that scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accessibility.&lt;/strong&gt; Users with very old devices or limited hardware will wait longer. There's no audio CAPTCHA equivalent. For sites with strict accessibility requirements, consider offering the Lightning skip as an alternative path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GPU farming.&lt;/strong&gt; An attacker with a GPU can solve SHA-256 challenges orders of magnitude faster than a browser can. The roadmap includes Argon2-based challenges (memory-hard, GPU-resistant) to address this. Not there yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  When This Makes Sense
&lt;/h2&gt;

&lt;p&gt;This fits best when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're protecting contact forms, comment sections, or registration pages&lt;/li&gt;
&lt;li&gt;You value user privacy and don't want Google tracking on your site&lt;/li&gt;
&lt;li&gt;You're self-hosting and want full control over the infrastructure&lt;/li&gt;
&lt;li&gt;Your traffic is low-to-medium (under 100k submissions per day)&lt;/li&gt;
&lt;li&gt;You're building something in the Bitcoin/Lightning ecosystem and want the Lightning skip feature&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It probably doesn't fit when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need enterprise-grade bot detection with ML behavioral analysis&lt;/li&gt;
&lt;li&gt;You're processing millions of submissions daily and need proven scale&lt;/li&gt;
&lt;li&gt;Your audience is primarily on budget mobile devices&lt;/li&gt;
&lt;li&gt;You have strict accessibility compliance requirements (WCAG AAA)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Self-Hosting the Verification Server
&lt;/h2&gt;

&lt;p&gt;You don't have to use the hosted endpoint. The verification server is a single Node.js file with zero npm dependencies for core functionality:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://www.npmjs.com/package/@powforge/captcha
&lt;span class="nb"&gt;cd &lt;/span&gt;captcha
node server.js
&lt;span class="c"&gt;# Listening on port 3077&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point the widget at your own server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"powforge-captcha.min.js"&lt;/span&gt;
        &lt;span class="na"&gt;data-target=&lt;/span&gt;&lt;span class="s"&gt;"#captcha"&lt;/span&gt;
        &lt;span class="na"&gt;data-server=&lt;/span&gt;&lt;span class="s"&gt;"https://your-domain.com:3077"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three endpoints. That's the whole API:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/api/challenge&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;Returns a new PoW challenge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/api/verify&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;Accepts a solution, returns a signed token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/api/token/verify&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;Server-side token validation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;There's a live demo at &lt;a href="https://powforge.dev/captcha/" rel="noopener noreferrer"&gt;powforge.dev/captcha&lt;/a&gt; where you can solve a challenge and see how it feels. The source is on &lt;a href="https://www.npmjs.com/package/@powforge/captcha" rel="noopener noreferrer"&gt;GitLab&lt;/a&gt;. The npm package (&lt;code&gt;@powforge/captcha&lt;/code&gt;) is coming soon.&lt;/p&gt;

&lt;p&gt;If you build something with it, I'd genuinely like to hear about it. Open an issue on the repo or find me on Nostr.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by an indie dev who got tired of paying Google to protect a contact form.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>security</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
