<?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.us-east-2.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>I put my identity-depth scorer behind a public GET endpoint so you can argue with it</title>
      <dc:creator>Zeke</dc:creator>
      <pubDate>Mon, 17 Aug 2026 07:04:42 +0000</pubDate>
      <link>https://dev.to/zekebuilds/i-put-my-identity-depth-scorer-behind-a-public-get-endpoint-so-you-can-argue-with-it-1cgl</link>
      <guid>https://dev.to/zekebuilds/i-put-my-identity-depth-scorer-behind-a-public-get-endpoint-so-you-can-argue-with-it-1cgl</guid>
      <description>&lt;p&gt;I have been building a thing I call depth-of-identity: instead of asking an account &lt;em&gt;who are you&lt;/em&gt; (credentials, KYC, a captcha), you measure &lt;em&gt;what it has done&lt;/em&gt; and how hard that history would be to fake. Chains of work, social reciprocity, money that actually moved. The bet is that behavior across several independent dimensions is exponentially more expensive to counterfeit than any single number like follower count.&lt;/p&gt;

&lt;p&gt;I wrote a companion post about one narrow slice of this — gap-entropy, telling scheduled bots from humans purely by &lt;em&gt;when&lt;/em&gt; they post. This post is about the other half: I took the aggregate scorer, the thing that rolls several dimensions into one weight, and hung it off a plain read-only endpoint. You can curl it. I want people to curl it and tell me where it is wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The endpoint
&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;GET https://captcha.powforge.dev/api/depth-check?pubkey=&amp;lt;64-hex or npub1...&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No auth, no body, no cost. It accepts either a raw 64-char hex pubkey or an &lt;code&gt;npub1...&lt;/code&gt;. Give it garbage and it tells you:&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="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"invalid or missing pubkey (64-hex or npub1...)"&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;Give it a real pubkey and you get the full breakdown. Here is a real response, trimmed to the scores (every account has the four dimensions; each carries a pile of sub-metrics I left in the live response). This is fiatjaf's pubkey, so you can reproduce it yourself:&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;GET /api/depth-check?pubkey=3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"&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="mi"&gt;307&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"threshold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"would_skip"&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;"gating_active"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"active_dimensions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"total_events"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;834&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"account_age_days"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;564&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dimensions"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;142&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"reciprocityRatio"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.268&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"bidirectionalPeers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;38&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;"access"&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;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nl"&gt;"totalPowBits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&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;"maxDifficulty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&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;"vouch"&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;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nl"&gt;"inboundVouches"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&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;"cycleDropped"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&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;"economic"&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;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;114&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"inboundZaps"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;510&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="nl"&gt;"genuineSenders"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;276&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="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"note"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"diagnostic only — CAPTCHA gating is not affected by this score"&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;Those are live numbers from scoring a long-lived, active pubkey (834 events, 564 days old). It cleared the threshold of 100 on two dimensions alone — social and economic — without any proof-of-work or vouch history. That is the whole point of the design: you do not need to max one axis, you need genuine breadth.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the four dimensions mean
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;social&lt;/strong&gt; — not follower count. It looks at &lt;em&gt;reciprocity&lt;/em&gt;: how many peers reference you back versus the ones you only shout at. A thousand inbound mentions from accounts you never engage is worth far less than a hundred bidirectional conversations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;access&lt;/strong&gt; — proof-of-work you have actually spent. Bits of difficulty on your events, log2-scaled so one heroic grind cannot hijack the whole score (I learned that the hard way; an early version let a PoW farm out-score real humans 15-to-1 until I capped per-dimension contribution).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;vouch&lt;/strong&gt; — inbound vouches from other identities, with cycles dropped so a ring of sockpuppets vouching for each other collapses to near nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;economic&lt;/strong&gt; — zaps, deduplicated by the underlying Lightning invoice so you cannot inflate by re-broadcasting the same payment. It weighs &lt;em&gt;genuine&lt;/em&gt; reciprocal senders over one-directional spray.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;depth&lt;/code&gt; weight is the additive roll-up. &lt;code&gt;active_dimensions&lt;/code&gt; counts how many axes are non-zero, which matters more than the raw sum — a Sybil can farm one dimension, but farming three independent ones at once is where the cost explodes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The corpus caveat
&lt;/h2&gt;

&lt;p&gt;Here is the part product pages leave out. A depth score is only as good as the events behind it. The scorer can only weigh history it can actually see, so the endpoint fans out across public relays — right now &lt;code&gt;wss://relay.damus.io&lt;/code&gt;, &lt;code&gt;wss://nos.lol&lt;/code&gt;, and the powforge relay — and unions what they return. If none of those relays has ever ingested a pubkey's events, that pubkey scores a wall of zeros with &lt;code&gt;would_skip: false&lt;/code&gt;. That does not mean the account is a bot. It means depth is relative to a corpus, and no corpus is the whole network.&lt;/p&gt;

&lt;p&gt;That is not a bug I am hiding, it is the actual research question. Widen the relay set and a thin score fills in; a genuinely active account that was invisible to one relay shows up once another relay that knows it is in the pool. Honest deployment means telling you which events the number is built from, and letting you point at a pubkey the corpus underweights.&lt;/p&gt;

&lt;p&gt;You do not need to run anything locally to check a score — just curl the endpoint:&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="s2"&gt;"https://captcha.powforge.dev/api/depth-check?pubkey=3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;"depth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;307&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"active_dimensions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"total_events"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;834&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"would_skip"&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="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;That pubkey clears the threshold across two active dimensions against the live relay set. Aim the same endpoint at a pubkey none of those relays has heard of and it scores 0 — same code, different corpus. Sit with that for a second — it is the honest shape of every reputation system, most of them just do not show you the seam.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it does not gate anything
&lt;/h2&gt;

&lt;p&gt;Notice &lt;code&gt;gating_active: false&lt;/code&gt; and the &lt;code&gt;note&lt;/code&gt;. This endpoint never blocks a captcha, never lets anyone skip a challenge, never touches the verify path. It is groundwork. Before I wire "skip the captcha if depth &amp;gt; threshold" into anything real, I want to watch the skip policy run against live traffic and see how often it would have been wrong — how many bots would have sailed through, how many humans it would have wrongly gated. Measure first, gate later, and let people poke holes while it is still only a diagnostic.&lt;/p&gt;

&lt;p&gt;That is the invitation. Curl it against your own pubkey, against accounts you know are real, against accounts you know are bots, and if you find a pubkey where the score is obviously backwards, that is exactly the failure I want to hear about.&lt;/p&gt;

</description>
      <category>nostr</category>
      <category>bitcoin</category>
      <category>security</category>
      <category>api</category>
    </item>
    <item>
      <title>Paywall Any API Endpoint With Two Prices: Sats or Compute</title>
      <dc:creator>Zeke</dc:creator>
      <pubDate>Sun, 16 Aug 2026 02:02:59 +0000</pubDate>
      <link>https://dev.to/zekebuilds/paywall-any-api-endpoint-with-two-prices-sats-or-compute-1jdh</link>
      <guid>https://dev.to/zekebuilds/paywall-any-api-endpoint-with-two-prices-sats-or-compute-1jdh</guid>
      <description>&lt;p&gt;You built an API. It works. Then the scrapers show up.&lt;/p&gt;

&lt;p&gt;Not paying customers. Bots hammering your endpoint a thousand times a minute, running up your compute bill, and giving you nothing back. The usual fix is API keys, a signup flow, a Stripe integration, a dashboard, and a support inbox for people who lost their key. That is a lot of plumbing to answer one question: did this caller give up something real to reach me?&lt;/p&gt;

&lt;p&gt;Here is a smaller idea. Put a price on the endpoint itself. Every call costs something. The caller either pays a few sats over Lightning, or burns a bit of their own CPU on a proof-of-work puzzle. No account. No key. No dashboard. The payment IS the authorization.&lt;/p&gt;

&lt;p&gt;This post walks the whole thing end to end against a live server at &lt;code&gt;gate.powforge.dev&lt;/code&gt;. Every number and response below came off a real request. Copy the curl lines and run them yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two-price idea
&lt;/h2&gt;

&lt;p&gt;The gate hands the caller a choice for every request:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pay compute.&lt;/strong&gt; Solve a SHA-256 partial collision in the browser or on the command line. Costs the caller electricity and a second of wall-clock time. Costs you nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pay sats.&lt;/strong&gt; Settle a Lightning invoice for 10 sats. About a tenth of a cent. Costs the caller money, costs you nothing to verify.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both paths end the same way: the caller gets the gated response. The point is that both paths cost the caller &lt;em&gt;something&lt;/em&gt;. A scraper running at scale cannot do either one for free, so the free-riding stops without you ever standing up a login.&lt;/p&gt;

&lt;h2&gt;
  
  
  Path 1: pay with compute
&lt;/h2&gt;

&lt;p&gt;Ask the server for a challenge.&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://gate.powforge.dev/api/challenge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get back a fresh nonce and a difficulty:&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="nl"&gt;"nonce"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"35df6dfac728013209f389eb8921f5aa6236b8a7b12f794c31b01edac563756f"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"difficulty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;20&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;Difficulty 20 means the caller has to find a string &lt;code&gt;solution&lt;/code&gt; such that &lt;code&gt;SHA-256(nonce + solution)&lt;/code&gt; starts with 20 leading zero bits. There is no clever shortcut. You grind candidates until one hits.&lt;/p&gt;

&lt;p&gt;Here is the whole solver in Node. No dependencies.&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;crypto&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;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;diff&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;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nonce&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;sol&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&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;wholeBytes&lt;/span&gt; &lt;span class="o"&gt;=&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;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;8&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;remBits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&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="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;wholeBytes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&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;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&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="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&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;remBits&lt;/span&gt; &lt;span class="o"&gt;&amp;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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mh"&gt;0xFF&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;remBits&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;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;wholeBytes&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;)&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="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&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="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="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="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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;difficulty&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&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://gate.powforge.dev/api/challenge&lt;/span&gt;&lt;span class="dl"&gt;'&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;attempt&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="nx"&gt;sol&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;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;sol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;difficulty&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;`solved in &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; attempts, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;ms, solution=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sol&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&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;fetch&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://gate.powforge.dev/api/solve&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&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;Content-Type&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;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;solution&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sol&lt;/span&gt; &lt;span class="p"&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="k"&gt;await&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="p"&gt;})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running that against the live server just now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;solved in 113275 attempts, 1277ms, solution=2fej
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One core, one and a third seconds, a hundred thousand hashes. Then the solve POST comes back with the goods:&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;"token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5ec59d49aa431e1c..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"content"&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="s2"&gt;"Bitcoin is not money. Bitcoin is a weapon system."&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="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;That is the gated response. The caller proved work, the server checked the hash in a single operation, and access was granted. Notice what did NOT happen: no signup, no email, no key to store, no rate-limit table to maintain. The cost lives in the caller's CPU, and it scales against them automatically. One call is cheap. A million calls is a million times the electricity.&lt;/p&gt;

&lt;p&gt;The server side of the check is tiny. It is the same hash test, run once:&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;verifyPoW&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;solution&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;difficulty&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;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nonce&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;solution&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&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;wholeBytes&lt;/span&gt; &lt;span class="o"&gt;=&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;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;difficulty&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;8&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;remBits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;difficulty&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&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="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;wholeBytes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&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;hash&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&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="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&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;remBits&lt;/span&gt; &lt;span class="o"&gt;&amp;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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mh"&gt;0xFF&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;remBits&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;hash&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;wholeBytes&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;)&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="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&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="kc"&gt;true&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;Verification is O(1) no matter how long the caller ground away. Asymmetric work is the whole trick. Hard to produce, trivial to check.&lt;/p&gt;

&lt;p&gt;In a browser you run that loop in a Web Worker so the page stays responsive. Same math, off the main thread.&lt;/p&gt;

&lt;h2&gt;
  
  
  Path 2: pay with sats over L402
&lt;/h2&gt;

&lt;p&gt;Some callers do not want to burn CPU. Maybe they are on a phone, maybe they are automating and would rather pay a tenth of a cent than spin a fan. For them the gate speaks L402, the HTTP-native Lightning payment scheme built on the plain old &lt;code&gt;402 Payment Required&lt;/code&gt; status code.&lt;/p&gt;

&lt;p&gt;POST the endpoint with no authorization and watch what comes back.&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; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://gate.powforge.dev/l402/unlock &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="m"&gt;402&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="eyJ2IjoxLCJwaCI6IjFhOGNmOTM2..." invoice="lnbc100n1p4gzx0rpp5r2x0jd5za9a8j0n7epgue3fff6vr2w4z..."&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="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;"payment required"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"pow-gate:unlock"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"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;span class="nl"&gt;"macaroon"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"eyJ2IjoxLCJwaCI6..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"invoice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"lnbc100n1p4gzx0rpp5r2x0jd5..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"payment_hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"1a8cf93682e97a793e7ec851ccc5294e98353aa24008b8982ba728b26c2021af"&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;Two things came back in that 402. A &lt;code&gt;macaroon&lt;/code&gt;, which is a signed token that names what you are buying (&lt;code&gt;scope: pow-gate:unlock&lt;/code&gt;) and when it expires. And a bolt11 &lt;code&gt;invoice&lt;/code&gt; for 10 sats (that &lt;code&gt;lnbc100n&lt;/code&gt; prefix is 100 nano-BTC, which is 10 sats).&lt;/p&gt;

&lt;p&gt;The caller pays the invoice in any Lightning wallet, which yields a preimage, the payment receipt. Then they call the same endpoint again, this time presenting the macaroon and the preimage together:&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; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://gate.powforge.dev/l402/unlock &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: L402 &amp;lt;macaroon&amp;gt;:&amp;lt;preimage_hex&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server checks that the macaroon signature is valid and that the invoice is actually paid, then returns 200 with the same gated content the PoW path gives. One paid call, one unlock. The macaroon is single-use and guarded against replay, so a paid token cannot be shared around.&lt;/p&gt;

&lt;p&gt;There is a discovery endpoint too, so an automated caller can learn the price and shape before it spends anything:&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://gate.powforge.dev/l402/info
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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="nl"&gt;"service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"pow-gate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"endpoints"&lt;/span&gt;&lt;span class="p"&gt;:[{&lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"/l402/unlock"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"pow-gate:unlock"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"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="nl"&gt;"auth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"L402 (RFC 7235 + Lightning)"&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;An agent hits &lt;code&gt;/l402/info&lt;/code&gt;, sees it costs 10 sats, decides that is fine, and pays. No human in the loop, no key exchanged ahead of time. That is the part that matters if your callers are increasingly software.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why put both on the same endpoint
&lt;/h2&gt;

&lt;p&gt;Because your callers are not all the same, and you do not know which is which up front.&lt;/p&gt;

&lt;p&gt;A human poking at your API from a laptop has spare CPU and no Lightning wallet. Give them the compute path and they are through in a second, no wallet setup, no card.&lt;/p&gt;

&lt;p&gt;An automated agent has a wallet and does not want to grind hashes on rented cloud CPU that bills by the second. Give it the L402 path and it pays a tenth of a cent and moves on.&lt;/p&gt;

&lt;p&gt;A scraper trying to pull your whole dataset for free hits a wall either way. Ten thousand pulls is ten thousand PoW grinds or 100,000 sats. The economics that were invisible before are now sitting right on the endpoint, and they point the wrong way for anyone trying to free-ride.&lt;/p&gt;

&lt;p&gt;You did not build a billing system. You did not build an auth server. You put a price on a URL and let the caller choose their currency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it into your own service
&lt;/h2&gt;

&lt;p&gt;The gate is a thin layer in front of whatever you are already serving. The shape is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;GET /api/challenge&lt;/code&gt; issues a nonce and remembers it briefly.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /api/solve&lt;/code&gt; checks the hash, and on success returns your real response.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /l402/unlock&lt;/code&gt; mints a macaroon and invoice on the first call, and on the paid retry checks the preimage and returns the same real response.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Swap the demo content for your actual endpoint payload and you have a paywalled API with two prices and zero accounts. The Lightning side needs a node or an LNBits instance to mint and check invoices. The PoW side needs nothing but the crypto library your runtime already ships.&lt;/p&gt;

&lt;p&gt;Put a price on the door. Let them pay in sats or in sweat. The bots can afford neither at scale, and that was the whole problem.&lt;/p&gt;

&lt;p&gt;Live server: &lt;code&gt;https://gate.powforge.dev&lt;/code&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>api</category>
      <category>lightning</category>
      <category>l402</category>
    </item>
    <item>
      <title>Anonymous pastebins are a spam magnet. I gated mine with proof-of-work instead of logins.</title>
      <dc:creator>Zeke</dc:creator>
      <pubDate>Sat, 15 Aug 2026 23:00:58 +0000</pubDate>
      <link>https://dev.to/zekebuilds/anonymous-pastebins-are-a-spam-magnet-i-gated-mine-with-proof-of-work-instead-of-logins-9la</link>
      <guid>https://dev.to/zekebuilds/anonymous-pastebins-are-a-spam-magnet-i-gated-mine-with-proof-of-work-instead-of-logins-9la</guid>
      <description>&lt;p&gt;Open pastebin, no login, paste text, get a link. That is the whole appeal, and it is also why every anonymous paste service turns into a dumping ground. Bots script the post endpoint and fire off thousands of spam links, phishing kits, leaked credential lists, malware config. The usual fix is to make the thing less anonymous: force an account, bolt on reCAPTCHA, rate-limit by IP. Every one of those punishes the honest one-off user to slow down a bot that just rotates IPs anyway.&lt;/p&gt;

&lt;p&gt;I wanted a paste service that stayed anonymous and one-click but still cost a spammer something real. So the price of posting is not an account. It is energy. You either burn a little CPU (proof of work, free) or you pay 10 sats over Lightning. A human posting one snippet never notices. A bot posting ten thousand notices real fast.&lt;/p&gt;

&lt;p&gt;Here is the whole flow against the live service at &lt;code&gt;paste.powforge.dev&lt;/code&gt;. Every curl below is real and runs today.&lt;/p&gt;

&lt;h2&gt;
  
  
  The free path: prove work
&lt;/h2&gt;

&lt;p&gt;Grab a challenge first.&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://paste.powforge.dev/api/challenge
&lt;span class="c"&gt;# {"nonce":"468367dc78bff5e696e23d9ac1684417eda7d4ecb1d02030a0209e37ee28f953","difficulty":18}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get a random &lt;code&gt;nonce&lt;/code&gt; and a &lt;code&gt;difficulty&lt;/code&gt; in bits. The job is to find any string &lt;code&gt;solution&lt;/code&gt; where &lt;code&gt;SHA-256(nonce + solution)&lt;/code&gt; starts with &lt;code&gt;difficulty&lt;/code&gt; leading zero bits. That is it. No fancy VDF, just a hash you cannot shortcut, only grind.&lt;/p&gt;

&lt;p&gt;Here is a solver you can drop in a file and run with plain Node, no dependencies.&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;crypto&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;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;hasLeadingZeroBits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bits&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;fullBytes&lt;/span&gt; &lt;span class="o"&gt;=&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;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bits&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;rem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bits&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&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="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;fullBytes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&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;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&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="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&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;rem&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;mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0xff&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;rem&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xff&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;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;fullBytes&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;)&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="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&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="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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&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;ch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&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://paste.powforge.dev/api/challenge&lt;/span&gt;&lt;span class="dl"&gt;'&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;s&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="k"&gt;while &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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nonce&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&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="nf"&gt;hasLeadingZeroBits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;difficulty&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="o"&gt;++&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;solution&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&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://paste.powforge.dev/api/paste&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&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;Content-Type&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;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello from proof of work&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;method&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&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;solution&lt;/span&gt; &lt;span class="p"&gt;})&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="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;https://paste.powforge.dev/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Difficulty 18 took me about 150,000 hashes and 1.8 seconds on one core. Nobody waiting on a paste cares about two seconds. A spammer scripting the endpoint pays that tax on every single post, in wall-clock time, with no way to buy their way around it except the honest one below.&lt;/p&gt;

&lt;p&gt;Post it and you get an id back.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# POST /api/paste  { content, method:"pow", nonce, solution }&lt;/span&gt;
&lt;span class="c"&gt;# -&amp;gt; {"id":"a06c33a7"}&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://paste.powforge.dev/a06c33a7
&lt;span class="c"&gt;# ...&lt;/span&gt;
&lt;span class="c"&gt;# ID: a06c33a7 | Method: Proof of Work | Expires: Sun, 16 Aug 2026 22:01:54 GMT&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Proof-of-work pastes live 24 hours. Cheap to make, so they do not stick around forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  The paid path: 10 sats, no CPU
&lt;/h2&gt;

&lt;p&gt;Do not want to burn cycles? Pay instead. Same endpoint shape, you just ask for an invoice.&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; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://paste.powforge.dev/api/invoice &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"content":"paste that outlives the free tier"}'&lt;/span&gt;
&lt;span class="c"&gt;# {"payment_hash":"acf00c5a...","bolt11":"lnbc100n1p4gpcwm..."}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pay the &lt;code&gt;bolt11&lt;/code&gt; from any Lightning wallet, then poll until it clears.&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://paste.powforge.dev/api/check/acf00c5a...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once paid the paste is created and kept for 30 days instead of 24 hours. The 10 sats is not the point, it is a fraction of a cent. The point is the same as the CPU: it is a real cost that scales with how much you post. One paste, you never feel it. A flood, and it adds up to actual money.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why energy beats a login wall
&lt;/h2&gt;

&lt;p&gt;Think about who each defense actually stops.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Accounts&lt;/strong&gt; stop nobody. Bots make accounts faster than people do. All you did was add friction for the honest user.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CAPTCHA&lt;/strong&gt; stops some bots and annoys every human, and the solving-farm market means a determined spammer just pays a fraction of a cent per solve.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IP rate limits&lt;/strong&gt; stop a lazy bot from one address and do nothing to a botnet or a proxy pool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Energy&lt;/strong&gt; (PoW or sats) does not care who or where you are. It charges per post. It is the one cost a spammer cannot rotate their way out of, because it is not tied to identity at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the whole trick. Keep the service anonymous and one-click for the person posting one thing. Put a small, unavoidable, per-post cost in the way of the machine posting ten thousand. You do not need to know who anybody is. You just need posting to cost a little.&lt;/p&gt;

&lt;p&gt;The service is live at &lt;a href="https://paste.powforge.dev" rel="noopener noreferrer"&gt;paste.powforge.dev&lt;/a&gt; if you want to poke the endpoints yourself. Same pattern drops onto any write endpoint that bots abuse, not just pastes.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>bitcoin</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Bot timing has an entropy signature, and you can measure it on live Nostr data</title>
      <dc:creator>Zeke</dc:creator>
      <pubDate>Sat, 15 Aug 2026 14:03:57 +0000</pubDate>
      <link>https://dev.to/zekebuilds/bot-timing-has-an-entropy-signature-and-you-can-measure-it-on-live-nostr-data-4bho</link>
      <guid>https://dev.to/zekebuilds/bot-timing-has-an-entropy-signature-and-you-can-measure-it-on-live-nostr-data-4bho</guid>
      <description>&lt;p&gt;I have been poking at a question that sounds simple but turns out to have a real answer: can you tell a bot from a human just by &lt;em&gt;when&lt;/em&gt; they post, ignoring everything they actually say?&lt;/p&gt;

&lt;p&gt;Turns out you can, mostly. And you can measure it against live Nostr relays in an afternoon. Here is what I found, including the case where it falls apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea: gaps, not timestamps
&lt;/h2&gt;

&lt;p&gt;A person does not post on a metronome. You fire off three notes in ten minutes, go quiet for six hours, come back, sleep, repeat. The &lt;em&gt;gaps&lt;/em&gt; between your events are all over the place. A scheduled bot posts every N hours like clockwork, so its gaps are nearly identical every time.&lt;/p&gt;

&lt;p&gt;So instead of looking at raw timestamps, I look at the sequence of inter-event gaps and ask how unpredictable that sequence is. That is just entropy.&lt;/p&gt;

&lt;p&gt;The measurement is deliberately crude so it cannot be fudged:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pull a pubkey's recent kind-1 notes (I used about 45 events each).&lt;/li&gt;
&lt;li&gt;Compute the gap in seconds between each consecutive pair.&lt;/li&gt;
&lt;li&gt;Bucket each gap by its log2 (so a 20-second gap and a 25-second gap land together, a 16-hour gap lands somewhere else).&lt;/li&gt;
&lt;li&gt;Compute Shannon entropy over the bucket distribution, in bits.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Log2 bucketing matters. Without it you are measuring jitter noise. With it you are measuring how many &lt;em&gt;orders of magnitude&lt;/em&gt; of timing behavior an account actually uses. A human uses several. A scheduled bot uses one or two.&lt;/p&gt;

&lt;p&gt;All of this ran through &lt;code&gt;nak&lt;/code&gt; against &lt;code&gt;wss://nos.lol&lt;/code&gt;. No synthetic data, no simulated accounts. Real pubkeys, real notes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Humans.&lt;/strong&gt; Two active human pubkeys came in at &lt;strong&gt;4.13 bits&lt;/strong&gt; and &lt;strong&gt;3.60 bits&lt;/strong&gt; of gap-entropy. Bursty, spread across many timescales, exactly what you would expect from someone living a life around their posting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bot A, the scheduled poster&lt;/strong&gt; (&lt;code&gt;253baa88...&lt;/code&gt;). This one runs a "Random Bitcoin Podcast Spotlight" template. All 45 of its notes I pulled shared an identical template prefix. Median gap around 16 hours, stretched over a 528-hour span. Its gap-entropy was &lt;strong&gt;1.000 bits&lt;/strong&gt; flat. It only ever used two log2 buckets. That is a machine on a timer.&lt;/p&gt;

&lt;p&gt;Human-to-bot ratio there: &lt;strong&gt;3.60x to 4.13x&lt;/strong&gt;. My prediction going in was that humans would show more than 2x the timing entropy of bots. Against Bot A that holds cleanly, with room to spare.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bot B, the burst spammer&lt;/strong&gt; (&lt;code&gt;72060890...&lt;/code&gt;). This is where it gets interesting. Bot B rotates product-ad templates (PIICloak, Certd, TLS pitches) and does not post on a slow schedule at all. It dumped 45 notes in about half an hour, median gap 21 seconds. Its gap-entropy was &lt;strong&gt;2.036 bits&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Human-to-bot ratio there: &lt;strong&gt;1.77x to 2.03x&lt;/strong&gt;. That straddles my 2x line. It fails the test against the 3.60-bit human and just barely passes against the 4.13-bit one. A jittered burst pattern, even a dumb randomized-cadence one, produces enough timing variety to nearly clear the bar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Aggregate:&lt;/strong&gt; mean bot entropy 1.52 bits versus mean human 3.87 bits, a &lt;strong&gt;2.55x&lt;/strong&gt; ratio. So on average the prediction confirms. But the average hides the failure mode, and the failure mode is the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it breaks, and what to do about it
&lt;/h2&gt;

&lt;p&gt;Timing entropy is a strong single signal against the enemy most people picture when they say "bot": the fixed-schedule poster. Those collapse toward 1 bit and light up immediately.&lt;/p&gt;

&lt;p&gt;It is a weak signal against a burst spammer that randomizes its cadence. Bot B slipped to ~2 bits purely because posting 45 things in 30 minutes with 20-ish second jitter genuinely does spread across a couple of log2 buckets. Timing alone will not reliably catch that archetype.&lt;/p&gt;

&lt;p&gt;But here is the thing. Both bots were trivially obvious on &lt;em&gt;content&lt;/em&gt;. Bot A had 45 identical template prefixes. Bot B rotated a small pool of ad templates. The tell that timing missed on Bot B, content-template similarity caught instantly. Whereas for the scheduled Bot A, timing was the cleaner tell.&lt;/p&gt;

&lt;p&gt;That is the actual lesson. These signals are complementary, not competing. Timing entropy is cheap, content-agnostic, and murders schedule bots. Template similarity is cheap and murders burst spammers. Pair them and the gap in either one gets covered by the other. Lean on timing alone and a jittered burster walks right through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I care about this
&lt;/h2&gt;

&lt;p&gt;I have been building depth-of-identity scoring over at identity.powforge.dev, the idea being that you can weigh how much verifiable &lt;em&gt;behavior&lt;/em&gt; an account carries instead of asking it for credentials. Gap-entropy is one of the timing signals I am testing as an input to that. It is not the whole story, no single dimension is, but it is a real, measurable, un-spoofable-without-cost one, and I like signals I can pull off a live relay and check with my own eyes.&lt;/p&gt;

&lt;p&gt;If you want to reproduce it: grab &lt;code&gt;nak&lt;/code&gt;, pull ~45 kind-1 events for any pubkey, diff the timestamps, log2-bucket the gaps, take the Shannon entropy. A human will sit up around 3.5 to 4 bits. A thing on a timer will sit near 1. The distance between those two numbers is the bot.&lt;/p&gt;

</description>
      <category>bitcoin</category>
      <category>nostr</category>
      <category>security</category>
      <category>datascience</category>
    </item>
    <item>
      <title>Proving a proof-of-work solve happened, at a specific time, without trusting the server's logs</title>
      <dc:creator>Zeke</dc:creator>
      <pubDate>Mon, 10 Aug 2026 03:01:24 +0000</pubDate>
      <link>https://dev.to/zekebuilds/proving-a-proof-of-work-solve-happened-at-a-specific-time-without-trusting-the-servers-logs-51mh</link>
      <guid>https://dev.to/zekebuilds/proving-a-proof-of-work-solve-happened-at-a-specific-time-without-trusting-the-servers-logs-51mh</guid>
      <description>&lt;h1&gt;
  
  
  The problem: a solve is a fact that only lives in one server's memory
&lt;/h1&gt;

&lt;p&gt;If you gate a route with proof-of-work, every accepted solve is a real event. Someone burned real compute, at a real wall-clock moment, to get past the gate. That is exactly the property you wanted: the gate costs the caller something.&lt;/p&gt;

&lt;p&gt;But where does that fact live? In the verifier's memory, for as long as the process stays up. The server knows a valid solve came in. Nobody else does, and nothing stops the server from claiming a solve happened that didn't, or forgetting one that did. If you ever need to prove "this specific solve happened before this point in time" to somebody who does not trust your logs, you have nothing. A row in your own database is not evidence to an adversary.&lt;/p&gt;

&lt;p&gt;So the question is narrow and concrete: can you make an accepted PoW solve durable and independently auditable, without asking anyone to trust the server that recorded it?&lt;/p&gt;

&lt;h1&gt;
  
  
  The mechanism: Merkle-fold the window, anchor one root to Bitcoin
&lt;/h1&gt;

&lt;p&gt;&lt;code&gt;@powforge/solve-witness&lt;/code&gt; does one thing. Every accepted solve is reduced to a leaf hash and appended to the current batch. On a fixed cadence (default 10 minutes) the batch is sealed: the leaves are folded into a Merkle root, and that single 32-byte root is the &lt;code&gt;witness_hash&lt;/code&gt; for that window. Submit the root to an OpenTimestamps calendar and the whole batch is anchored to Bitcoin with one timestamp.&lt;/p&gt;

&lt;p&gt;The economics are the point. One Bitcoin timestamp attests to an unbounded number of solves, because the Merkle fold makes the on-chain cost O(1) regardless of how many solves landed in the window. A thousand solves and one solve cost the same to anchor.&lt;/p&gt;

&lt;p&gt;Later, anyone can prove one specific solve was in the batch by presenting three things:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;leaf  -&amp;gt;  Merkle inclusion path  -&amp;gt;  witness_hash (in the OTS proof, anchored to a Bitcoin block)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every step recomputes with plain SHA-256, and the final OTS proof verifies against Bitcoin with the reference &lt;code&gt;ots&lt;/code&gt; tooling, not against any PowForge server.&lt;/p&gt;

&lt;h1&gt;
  
  
  The leaf: what actually gets committed
&lt;/h1&gt;

&lt;p&gt;A leaf has to commit to &lt;em&gt;which&lt;/em&gt; challenge was solved, not just that some solve happened. The PowForge rate-limit layer identifies a solve by the challenge salt and the winning nonce, plus the server's HMAC signature over the salt. All three go into the leaf, domain-separated so a leaf hash can never collide with an internal Merkle node hash:&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;leafForSolve&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signature&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="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;salt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;nonce&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;leafForSolve requires { salt, nonce }&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;return&lt;/span&gt; &lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`leaf:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;salt&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;nonce&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;signature&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&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;// internal nodes use a different prefix, so a leaf can never be&lt;/span&gt;
&lt;span class="c1"&gt;// reinterpreted as a node (or vice versa)&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;nodeHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;right&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="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:&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;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;right&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;The Merkle core is deliberately pure and synchronous. Append, seal, root, inclusion proof, verify — no network, no heavy dependencies, fully unit-testable offline. The only networked piece, the OpenTimestamps calendar submission, lazy-loads &lt;code&gt;javascript-opentimestamps&lt;/code&gt; so the core stays dependency-light and the network call is exercised by a live script, not by CI.&lt;/p&gt;

&lt;h1&gt;
  
  
  Wiring it into a live gate
&lt;/h1&gt;

&lt;p&gt;The collector is the glue that decides &lt;em&gt;when&lt;/em&gt; to seal, &lt;em&gt;where&lt;/em&gt; to persist the proof, and &lt;em&gt;how&lt;/em&gt; to be fed from a running server. It's on npm, so grab it and the rate-limiter it hangs off:&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/solve-witness @powforge/ratelimit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It drops straight onto the rate-limiter's &lt;code&gt;onSolve&lt;/code&gt; hook:&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;createWitnessCollector&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/solve-witness/collector&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;powRateLimit&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/ratelimit&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;witness&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createWitnessCollector&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;logPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/srv/data/solve-witness-log.jsonl&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;witness&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                       &lt;span class="c1"&gt;// seal every 10 min in the background&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;powRateLimit&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;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;onSolve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;witness&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onSolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;// every verified solve gets witnessed&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two design decisions matter because this sits on a request hot path. First, &lt;code&gt;onSolve&lt;/code&gt; is synchronous and cannot throw into the request — a malformed solve is dropped and logged, never propagated to the caller. Second, sealing swaps in a fresh batch &lt;em&gt;before&lt;/em&gt; the slow, networked OTS call, so solves arriving mid-stamp land in the next window and are never lost. The seal is also re-entrancy guarded, so a slow calendar submission can't overlap itself.&lt;/p&gt;

&lt;h1&gt;
  
  
  Does the whole chain actually hold together? Yes, end to end
&lt;/h1&gt;

&lt;p&gt;The part I care about is not "the unit tests pass." It is: a real running service, a real solve, a real OTS proof. So there's a demo server that boots an Express app with the real middleware wired to the real collector, then drives itself:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An unauthenticated &lt;code&gt;GET /api/protected&lt;/code&gt; returns a &lt;code&gt;429&lt;/code&gt; with a PoW challenge.&lt;/li&gt;
&lt;li&gt;A client grinds a real SHA-256 nonce until it clears the difficulty threshold.&lt;/li&gt;
&lt;li&gt;It retries with the &lt;code&gt;X-PoW-Proof&lt;/code&gt; header, the middleware verifies it, serves the route, and fires &lt;code&gt;onSolve&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;onSolve&lt;/code&gt; folds that exact solve into the live batch.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /admin/seal&lt;/code&gt; seals the window and submits the Merkle root to a real OpenTimestamps calendar.&lt;/li&gt;
&lt;li&gt;The client rebuilds the leaf from its own &lt;code&gt;(salt, nonce, signature)&lt;/code&gt; and verifies Merkle inclusion under the sealed &lt;code&gt;witness_hash&lt;/code&gt; the OTS proof commits to.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On the last run that produced a genuine 665-byte &lt;code&gt;.ots&lt;/code&gt; proof carrying pending calendar attestations — the real serialized OpenTimestamps format, not a stub. Once the calendars aggregate and the anchoring transaction confirms (hours, not seconds), &lt;code&gt;upgradeProof()&lt;/code&gt; pulls the Bitcoin attestation and the proof tells you which block height attests to the root.&lt;/p&gt;

&lt;h1&gt;
  
  
  What this is not — yet
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update (2026-08-10):&lt;/strong&gt; the hosted endpoint now exists. &lt;code&gt;witness.powforge.dev&lt;/code&gt; serves a live, L402-gated &lt;code&gt;/stamp&lt;/code&gt; route: POST a solve, pay a 100-sat Lightning invoice (or present a valid macaroon), and it folds into the current batch for anchoring. Verified end-to-end — an unpaid &lt;code&gt;POST /stamp&lt;/code&gt; returns a real &lt;code&gt;402&lt;/code&gt; with an L402 &lt;code&gt;WWW-Authenticate&lt;/code&gt; challenge and a payable BOLT11 invoice (&lt;code&gt;x-l402-price-sats: 100&lt;/code&gt;). So the "no payment flow, no sats price, no hosted endpoint" line below described the primitive stage in April; that gap is now closed. The mechanism the rest of this post argues for is unchanged.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is infrastructure, and I want to be exact about where the line is.&lt;/p&gt;

&lt;p&gt;What exists and is verified: the Merkle batch core, the &lt;code&gt;onSolve&lt;/code&gt; wiring into the rate-limiter, live OTS calendar submission, and an end-to-end demo that produces a real proof. What does not exist: any customer-facing product around it. There is no payment flow, no sats price on witnessing, no hosted endpoint where you submit solves and get certificates back, no dashboard. The batch persists to a JSONL log and writes &lt;code&gt;.ots&lt;/code&gt; proof files to disk. That is the whole surface today.&lt;/p&gt;

&lt;p&gt;So this is not a finished service you can point users at. It is the primitive that a service would be built on. I am publishing it at the primitive stage on purpose, because the interesting claim is the mechanism — one Bitcoin timestamp making an arbitrary number of PoW solves independently provable — and that claim is either sound or it isn't, regardless of whether there's a checkout page in front of it.&lt;/p&gt;

&lt;p&gt;If you wire it up and the inclusion proof verifies, or if it doesn't, I want to hear about it. The package is MIT. The OpenTimestamps proof format is an open standard and the calendars are run by the OpenTimestamps project and independent parties, not by me.&lt;/p&gt;

</description>
      <category>bitcoin</category>
      <category>security</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I counted every OP_RETURN on Bitcoin. A machine out-wrote all of human history 45 to 1.</title>
      <dc:creator>Zeke</dc:creator>
      <pubDate>Wed, 22 Jul 2026 18:02:46 +0000</pubDate>
      <link>https://dev.to/zekebuilds/i-counted-every-opreturn-on-bitcoin-a-machine-out-wrote-all-of-human-history-45-to-1-5bgn</link>
      <guid>https://dev.to/zekebuilds/i-counted-every-opreturn-on-bitcoin-a-machine-out-wrote-all-of-human-history-45-to-1-5bgn</guid>
      <description>&lt;p&gt;There's a romantic idea about Bitcoin's chain: that it's a wall of human messages. Proposals, memorials, "Vahe was here," pizza jokes, the occasional protest note pinned into the world's most expensive append-only log.&lt;/p&gt;

&lt;p&gt;I wanted to know if that was actually true. So I counted. Every OP_RETURN output, from the genesis block to block 958,893, no sampling.&lt;/p&gt;

&lt;p&gt;The answer is no, and it's not close.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one number
&lt;/h2&gt;

&lt;p&gt;All human-readable OP_RETURN text ever mined into Bitcoin: &lt;strong&gt;3,827,227&lt;/strong&gt; outputs.&lt;/p&gt;

&lt;p&gt;Runes, one token protocol, in its own era: &lt;strong&gt;171,114,058&lt;/strong&gt; OP_RETURN outputs.&lt;/p&gt;

&lt;p&gt;That's a ratio of &lt;strong&gt;44.7 to 1&lt;/strong&gt;. One machine protocol, in a single two-year stretch, wrote about 45 times more to the chain than every human-readable message in Bitcoin's entire history combined.&lt;/p&gt;

&lt;h2&gt;
  
  
  The evidence, per era
&lt;/h2&gt;

&lt;p&gt;I split the chain into four eras by block height, not by any label stored in my database. Height boundaries are canonical and anyone can check them against a node, so the result doesn't depend on trusting my extractor's tags.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;era&lt;/th&gt;
&lt;th&gt;height range&lt;/th&gt;
&lt;th&gt;boundary event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;pre-ordinals&lt;/td&gt;
&lt;td&gt;0 – 767,429&lt;/td&gt;
&lt;td&gt;before the first inscription&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ordinals&lt;/td&gt;
&lt;td&gt;767,430 – 779,831&lt;/td&gt;
&lt;td&gt;inscription #0 to BRC-20 deploy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;boom-brc20&lt;/td&gt;
&lt;td&gt;779,832 – 839,999&lt;/td&gt;
&lt;td&gt;BRC-20 &lt;code&gt;ordi&lt;/code&gt; deploy to Runes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;runes&lt;/td&gt;
&lt;td&gt;840,000 – 958,893&lt;/td&gt;
&lt;td&gt;Runes launch at the halving&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Then I counted the full population of OP_RETURN outputs in each era. Human-readable text, Runes token messages, and binary blobs (Veriblock and OMNI proof-of-proof timestamping, mostly).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;era&lt;/th&gt;
&lt;th&gt;total OP_RETURN&lt;/th&gt;
&lt;th&gt;human text&lt;/th&gt;
&lt;th&gt;human %&lt;/th&gt;
&lt;th&gt;Runes&lt;/th&gt;
&lt;th&gt;Runes %&lt;/th&gt;
&lt;th&gt;binary&lt;/th&gt;
&lt;th&gt;binary %&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;pre-ordinals&lt;/td&gt;
&lt;td&gt;51,965,944&lt;/td&gt;
&lt;td&gt;861,532&lt;/td&gt;
&lt;td&gt;1.66%&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;0.00%&lt;/td&gt;
&lt;td&gt;51,103,723&lt;/td&gt;
&lt;td&gt;98.34%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ordinals&lt;/td&gt;
&lt;td&gt;261,767&lt;/td&gt;
&lt;td&gt;32,189&lt;/td&gt;
&lt;td&gt;12.30%&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;0.00%&lt;/td&gt;
&lt;td&gt;229,544&lt;/td&gt;
&lt;td&gt;87.69%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;boom-brc20&lt;/td&gt;
&lt;td&gt;2,980,954&lt;/td&gt;
&lt;td&gt;402,161&lt;/td&gt;
&lt;td&gt;13.49%&lt;/td&gt;
&lt;td&gt;40,251&lt;/td&gt;
&lt;td&gt;1.35%&lt;/td&gt;
&lt;td&gt;2,538,248&lt;/td&gt;
&lt;td&gt;85.15%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;runes&lt;/td&gt;
&lt;td&gt;177,474,762&lt;/td&gt;
&lt;td&gt;2,531,345&lt;/td&gt;
&lt;td&gt;1.43%&lt;/td&gt;
&lt;td&gt;171,114,058&lt;/td&gt;
&lt;td&gt;96.42%&lt;/td&gt;
&lt;td&gt;3,828,604&lt;/td&gt;
&lt;td&gt;2.16%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Here's the honest twist
&lt;/h2&gt;

&lt;p&gt;When I started, I expected to find a fall. A golden human era that machines later ate. That's the clean story, and it's wrong.&lt;/p&gt;

&lt;p&gt;Look at the human % column again. Human text was never the majority of OP_RETURN. Not once. Even at its absolute peak, the boom-brc20 era, human-readable messages were only 13.5% of data outputs. The chain was machine-dominated in every single era.&lt;/p&gt;

&lt;p&gt;What changed isn't who's in charge. Machines always were. What changed is the flavor of machine, and the scale.&lt;/p&gt;

&lt;p&gt;Before ordinals, 98.3% of OP_RETURN was binary timestamping. Veriblock and OMNI stuffing proof-of-proof blobs into the chain. Not prose, not tokens, just machine-to-machine attestation data. Then Runes launched at the 2024 halving and took the throne. In the runes era, 96.4% of every OP_RETURN output is a Runestone.&lt;/p&gt;

&lt;p&gt;And the volume went vertical. The runes era alone, about 119,000 blocks, holds 177.5M OP_RETURN outputs. That's more than 3x the entire prior 767,000-block history put together, which came to roughly 55.2M. Discretionary write demand didn't grow. It exploded, and Runes is basically all of it.&lt;/p&gt;

&lt;p&gt;So if you want the real headline, it's not "humans got pushed out." It's this: Bitcoin's data-carrier space was always bought by machines, and the marginal buyer today is a machine minting a token, not a person leaving a note.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I checked it against a node
&lt;/h2&gt;

&lt;p&gt;A database can lie. So I cross-checked both sides against a real Bitcoin node with txindex on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Human side, is it real prose?&lt;/strong&gt; I pulled one row the census flagged as human text: a vout at height 840,000 that decoded to "Vahe was here." Ran &lt;code&gt;getrawtransaction&lt;/code&gt; on it. The node returned a nulldata output with asm &lt;code&gt;OP_RETURN 56616865207761732068657265&lt;/code&gt;, and that hex decodes exactly to &lt;code&gt;Vahe was here&lt;/code&gt;. Real human message, confirmed by the node, not misclassified protocol junk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Machine side, are those really Runestones?&lt;/strong&gt; I pulled block 840,000, the Runes launch block itself, with &lt;code&gt;getblock(840000, 3)&lt;/code&gt;. It has 3,050 transactions and 2,033 OP_RETURN outputs. Of those, 2,004 (98.6%) begin with &lt;code&gt;OP_RETURN OP_13&lt;/code&gt;, which is the Runestone magic. At the very first block of the era, roughly 99% of OP_RETURN outputs were already token protocol. That's an independent confirmation my Runes classification isn't inflated.&lt;/p&gt;

&lt;h2&gt;
  
  
  The falsifier
&lt;/h2&gt;

&lt;p&gt;I'll tell you exactly what would break this claim, because a finding you can't falsify isn't worth much.&lt;/p&gt;

&lt;p&gt;The machine-dominance claim fails if either:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A byte-level recount of the 171.1M Runes outputs shows more than 20% are non-Runestone or human-readable, or&lt;/li&gt;
&lt;li&gt;My 3.83M human-text count undercounts real human OP_RETURN prose by more than 10x, enough to pull the 45:1 ratio down below about 4:1.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both are testable against a node. The block-840,000 spot check already pushed on condition 1 and it held at 98.6%.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations, because they matter
&lt;/h2&gt;

&lt;p&gt;I deliberately left witness inscriptions out of the headline. The witness content-type population is dominated by &lt;code&gt;text/plain&lt;/code&gt; at 111.0M items, but that MIME bucket mixes genuine prose with BRC-20 mint JSON, since BRC-20 mints get served as &lt;code&gt;text/plain&lt;/code&gt; too. Splitting that cleanly needs a prose classifier I don't fully trust yet, so any human-vs-machine witness ratio would be soft. The OP_RETURN lead needs no such caveat, because Runes is a distinct opcode-tagged protocol you can verify per output. And folding BRC-20's 111M onto the machine side would only make the thesis stronger, so leaving it out is the conservative call.&lt;/p&gt;

&lt;p&gt;A couple of smaller notes. The 40,251 Runes-tagged outputs in the boom era, before the official launch, are OP_13 pushes the detector caught early. Test and edge cases, immaterial to the ratio. And the era boundaries are height-based approximations of the real protocol-launch blocks. A shift of a few hundred blocks either way doesn't move any percentage that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built this
&lt;/h2&gt;

&lt;p&gt;This is one exhibit out of a full four-channel census of what Bitcoin actually stores. OP_RETURN, witness inscriptions, coinbase tags, and datasets, all mined gap-free to the chain tip and cross-checked against a node. The angle is proof-of-work primitives and on-chain data, treating blockspace as the scarce, PoW-secured resource it is and asking who's actually spending it.&lt;/p&gt;

&lt;p&gt;The live chart with the per-era breakdown is here:&lt;br&gt;
&lt;a href="https://powforge.dev/museum/charts/human-vs-machine.html" rel="noopener noreferrer"&gt;https://powforge.dev/museum/charts/human-vs-machine.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The rest of the museum is here:&lt;br&gt;
&lt;a href="https://powforge.dev/museum/" rel="noopener noreferrer"&gt;https://powforge.dev/museum/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want to poke at the falsifier, grab a node with txindex on and start with block 840,000. That's where the flood begins.&lt;/p&gt;

</description>
      <category>bitcoin</category>
      <category>data</category>
      <category>opreturn</category>
      <category>programming</category>
    </item>
    <item>
      <title>I tried four ways to gate my MCP server. Only one didn't need a Stripe account.</title>
      <dc:creator>Zeke</dc:creator>
      <pubDate>Mon, 01 Jun 2026 20:51:03 +0000</pubDate>
      <link>https://dev.to/zekebuilds/i-tried-four-ways-to-gate-my-mcp-server-only-one-didnt-need-a-stripe-account-5ffe</link>
      <guid>https://dev.to/zekebuilds/i-tried-four-ways-to-gate-my-mcp-server-only-one-didnt-need-a-stripe-account-5ffe</guid>
      <description>&lt;p&gt;Your MCP server is getting hammered by agents you didn't authorize. A free tier is open to the world. A premium tool is one runaway loop away from torching your LLM budget. You need a gate.&lt;/p&gt;

&lt;p&gt;So I went shopping. Four projects ship something that calls itself an MCP gate today. I wired each one against a test server, ran a handful of tool calls through it, and wrote down what hurt.&lt;/p&gt;

&lt;p&gt;Here's the honest comparison.&lt;/p&gt;

&lt;h2&gt;
  
  
  PayGated (paygated.dev)
&lt;/h2&gt;

&lt;p&gt;This is the closest thing captcha-mcp has to a named competitor. PayGated wraps MCP tools with a Stripe-backed payment gate. Self-hosted, MIT-licensed, "no monthly SaaS bills." It's a clean piece of work.&lt;/p&gt;

&lt;p&gt;The catch is Stripe itself. To accept payment, you need a Stripe account in good standing, which means KYC, a bank, and a country Stripe operates in. Your callers need a Stripe customer record too, so every agent identity needs an email and a card on file before it can pay you a dime.&lt;/p&gt;

&lt;p&gt;PayGated does OAuth 2.1 + PKCE for the auth half and machine-to-machine flow for headless agents. That works if your callers can hold an API key. It's also the only one in this list with a real revenue model out of the box if you're already inside the Stripe ecosystem.&lt;/p&gt;

&lt;p&gt;Good fit: US-based MCP devs whose callers are corporate agents with billing relationships.&lt;br&gt;
Bad fit: anonymous agents, weekend-project monetization, anyone outside Stripe's footprint.&lt;/p&gt;
&lt;h2&gt;
  
  
  APort (aport.io)
&lt;/h2&gt;

&lt;p&gt;APort isn't really a gate. It sits in front of the gate. The pitch is "verifiable credentials for AI agents" using W3C VC standards. An agent presents a passport, APort checks the signature against a registry, your MCP server reads the verification result from a pre-tool hook and decides whether to run the tool.&lt;/p&gt;

&lt;p&gt;That's a different layer of the stack. APort answers "who is this agent." It doesn't answer "did they pay" or "should I rate-limit them." You'd compose APort with something else that does the metering.&lt;/p&gt;

&lt;p&gt;This is less a competitor and more a partnership shape. If you're already paying per call with captcha-mcp, APort's audit log could record who paid for what under whose authority. Worth a look if your buyers care about provenance more than monetization.&lt;/p&gt;

&lt;p&gt;Good fit: enterprise contexts where auditors will ask which agent did what.&lt;br&gt;
Bad fit: you just want to stop abuse and don't have a passport infrastructure problem yet.&lt;/p&gt;
&lt;h2&gt;
  
  
  AgentSign (agentsign.dev)
&lt;/h2&gt;

&lt;p&gt;AgentSign is the Ed25519-passport flavor of the same identity layer. Every agent gets a signed identity document. The server verifies the signature, looks up a trust score, gates the tool on that score. Clean cryptographically, but again, it's identity not metering.&lt;/p&gt;

&lt;p&gt;There's no payment rail and no abuse-prevention mechanism if a signed agent decides to hammer your endpoint legitimately. The trust score is the only knob. That's a different product than "charge me 10 sats and let me through."&lt;/p&gt;

&lt;p&gt;I'd use it if I were building a multi-agent system and needed a who-signed-this layer. I would not use it to stop a runaway loop.&lt;/p&gt;

&lt;p&gt;Good fit: multi-agent systems with reputation tracking.&lt;br&gt;
Bad fit: rate-limiting, micropayments, anything where you want the abuser to pay the cost of abuse.&lt;/p&gt;
&lt;h2&gt;
  
  
  captcha-mcp (the one I'm shipping)
&lt;/h2&gt;

&lt;p&gt;This is the project I work on, so call this biased. The pitch: gate any MCP tool with proof-of-work for free callers and a 10-sat Lightning invoice for callers who want to skip the CPU work. No Stripe account on either side. No KYC. No user database.&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;PayMCP&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;paymcp&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;CaptchaPowProvider&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-paymcp-provider&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;LnbitsPaymentProvider&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/paymcp-l402-provider&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nc"&gt;PayMCP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CaptchaPowProvider&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;captchaUrl&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LnbitsPaymentProvider&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;lnbitsUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LNBITS_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lnbitsApiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LNBITS_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;satsAmount&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="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;Tag a tool with &lt;code&gt;{ _meta: { price: 1 } }&lt;/code&gt; and it's gated. PoW path grinds a SHA-256 challenge in a few seconds of CPU. Lightning path mints a BOLT11 invoice and waits for the preimage. The calling agent picks whichever it can satisfy.&lt;/p&gt;

&lt;p&gt;The reason I built it this way: Stripe's KYC wall is fine for US SaaS but it kills the long tail. A solo dev in any country can spin up an LNBits wallet in a minute and start collecting sats. An agent author can fund a Lightning wallet with $1 and make 10,000 calls. Nobody's filling out a W-9.&lt;/p&gt;

&lt;p&gt;Good fit: anonymous agents, public APIs, micropayment-per-call, anyone outside Stripe's footprint.&lt;br&gt;
Bad fit: enterprise customers who want a credit card receipt, high-throughput callers who can't afford the PoW seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision
&lt;/h2&gt;

&lt;p&gt;If your callers are corporate agents with billing relationships, PayGated. If they're anonymous and you want to monetize the long tail without paperwork, captcha-mcp. If your problem is "who is this agent" and not "did they pay," APort or AgentSign sit at a different layer and you'll probably end up running one of them next to a payment gate, not instead of one.&lt;/p&gt;

&lt;p&gt;I picked PoW plus Lightning because it's the only path I've seen that works for a developer in Argentina, a research bot in a CI pipeline, and a side-project MCP server that doesn't want a Stripe account. Your tradeoffs may land you somewhere else. Just know which gate you're picking and why.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@powforge/captcha-mcp&lt;/code&gt; on npm: &lt;a href="https://www.npmjs.com/package/@powforge/captcha-mcp" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/@powforge/captcha-mcp&lt;/a&gt;&lt;br&gt;
Hosted captcha server: &lt;a href="https://captcha.powforge.dev" rel="noopener noreferrer"&gt;https://captcha.powforge.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>security</category>
      <category>lightning</category>
    </item>
    <item>
      <title>Why block hashes are dangerous for DLC randomness (and the fix)</title>
      <dc:creator>Zeke</dc:creator>
      <pubDate>Mon, 01 Jun 2026 13:52:46 +0000</pubDate>
      <link>https://dev.to/zekebuilds/why-block-hashes-are-dangerous-for-dlc-randomness-and-the-fix-4dn</link>
      <guid>https://dev.to/zekebuilds/why-block-hashes-are-dangerous-for-dlc-randomness-and-the-fix-4dn</guid>
      <description>&lt;p&gt;A DLC (Discreet Log Contract) is only as fair as its randomness source. If you're using a Bitcoin block hash as your oracle input for anything with money on it, you've got a miner front-running problem that won't care how tight the rest of your contract is.&lt;/p&gt;

&lt;p&gt;This ain't theoretical. PancakeSwap lost $1.8M in 2021 to an attack that precomputed the block hash used as a random seed (SWC-120). DeFi, but the same class of attack applies to any protocol where the person producing the "randomness" can see the downstream payoff first.&lt;/p&gt;

&lt;p&gt;Block hashes are predictable to miners.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why block hashes fail for DLCs specifically
&lt;/h2&gt;

&lt;p&gt;Here's the attack in plain terms:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A DLC has two outcomes: Alice wins if the coin lands heads, Bob wins tails. The oracle will sign "heads" or "tails" based on the block hash at time T.&lt;/li&gt;
&lt;li&gt;A miner with enough hashrate can, before publishing a block, check: does this hash make me money (if I'm a counterparty) or does it favor the other side? If it favors the other side, they can try again: mine another nonce, or selectively withhold.&lt;/li&gt;
&lt;li&gt;Even without being a counterparty, miners can be bribed to produce favorable hashes, or simply front-run if the DLC outcome is observable on-chain before the block is confirmed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Block hashes work fine for non-adversarial randomness. They're terrible for anything adversarial where the miner has a stake.&lt;/p&gt;

&lt;p&gt;The professional gambling industry learned this. Stake.com publishes "public seeding events" that mix a future block hash with additional entropy they commit to in advance. They separate who knows what from who controls what. That's the pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a verifiable beacon provides instead
&lt;/h2&gt;

&lt;p&gt;A proper randomness beacon closes the attack surface by:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Collecting entropy from multiple independent contributors before the result is computed&lt;/li&gt;
&lt;li&gt;Computing the result only after contributions are frozen (end of epoch)&lt;/li&gt;
&lt;li&gt;Signing the result with a Schnorr key so you can verify it offline&lt;/li&gt;
&lt;li&gt;Providing a payment receipt (L402 macaroon + preimage) that ties your specific fetch to a specific epoch&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last point matters for DLCs. A DLC contract can reference the oracle's pubkey and the epoch ID in the announcement. When the epoch closes, the signed beacon is the attestation. Any counterparty can verify it independently with the oracle pubkey.&lt;/p&gt;

&lt;h2&gt;
  
  
  The PowForge draw beacon
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;/draw&lt;/code&gt; endpoint at &lt;code&gt;attest.powforge.dev&lt;/code&gt; does exactly this for Bitcoin. Every 5-minute epoch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Contributors submit SHA-256 proof-of-work entropy (free)&lt;/li&gt;
&lt;li&gt;Oracle aggregates deterministically: &lt;code&gt;sha256("DRAW" || epoch_id || sha256(epoch_id || sorted_contribution_hashes))&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Oracle signs with BIP-340 Schnorr and seals the epoch&lt;/li&gt;
&lt;li&gt;Result is fixed forever and publicly verifiable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Schnorr format is the same family used by DLC oracle specs (dlcspecs/Oracle.md). You're not adding a new trust assumption. You're delegating randomness to a PoW-seeded ceremony that's independent of the miner who eventually mines your settlement block.&lt;/p&gt;

&lt;p&gt;Fetching a sealed beacon costs 50 sats via L402:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Trigger the 402 to get the invoice&lt;/span&gt;
curl &lt;span class="nt"&gt;-si&lt;/span&gt; https://attest.powforge.dev/api/v1/draw/EPOCH_ID

&lt;span class="c"&gt;# Pay the 50-sat Lightning invoice, get a preimage&lt;/span&gt;
&lt;span class="c"&gt;# Then fetch with the credential&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://attest.powforge.dev/api/v1/draw/EPOCH_ID &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: L402 &amp;lt;macaroon&amp;gt;:&amp;lt;preimage&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&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;"epoch_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5934397&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"contribution_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"oracle_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;"2bc78390c94d8bbb96ac3e6940462ba2812418d871e701c1a845fdb1dfd4a0e5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"attestation"&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;"beacon_random"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"62eb805f...32 bytes hex..."&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;"f2925b93...64 bytes hex..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"oracle_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;"2bc78390c94d8bbb96ac3e6940462ba2812418d871e701c1a845fdb1dfd4a0e5"&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="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;Verify offline (oracle_pubkey is x-only, 32 bytes, no prefix):&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;crypto&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;crypto&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;schnorr&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;@noble/curves/secp256k1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// The oracle signs taggedHash("DLC/oracle/attestation/v0", beacon_bytes)&lt;/span&gt;
&lt;span class="c1"&gt;// not the raw beacon bytes directly. BIP-340 §3.2 tagged hash construction.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;taggedHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;msg&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;tagHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&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;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tagHash&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tagHash&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;attestation&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;beaconBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attestation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;beacon_random&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&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;msgHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;taggedHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DLC/oracle/attestation/v0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;beaconBytes&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;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attestation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&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;pubkey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attestation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;oracle_pubkey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// already x-only&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;schnorr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;msgHash&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Using the beacon as a DLC oracle input
&lt;/h2&gt;

&lt;p&gt;In a dlcspecs-compliant DLC:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pick the epoch ID for your event close time (5-minute windows, &lt;code&gt;Math.floor(Date.now() / 300000)&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Include the oracle pubkey and epoch ID in your DLC announcement as the "event descriptor"&lt;/li&gt;
&lt;li&gt;At settlement, fetch the sealed beacon and use &lt;code&gt;attestation.beacon_random&lt;/code&gt; as the outcome scalar input&lt;/li&gt;
&lt;li&gt;Verify the Schnorr signature before settling (&lt;code&gt;attestation.signature&lt;/code&gt; against &lt;code&gt;attestation.oracle_pubkey&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The oracle pubkey is stable and published at &lt;code&gt;attest.powforge.dev&lt;/code&gt;. The beacon is seeded by contributors independent of both counterparties and miners. Nobody controls the output.&lt;/p&gt;




&lt;p&gt;If you're building DLCs and randomness is part of your oracle design, the block hash pattern has a known attack class. There's a Bitcoin-native alternative that costs 50 sats per event and verifies with any secp256k1 library.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Endpoint: &lt;code&gt;https://attest.powforge.dev/api/v1/draw/{epoch_id}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Landing: &lt;code&gt;https://powforge.dev/draw/&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>bitcoin</category>
      <category>dlc</category>
      <category>cryptography</category>
      <category>lightning</category>
    </item>
    <item>
      <title>Return a 402 instead of a 429 from your MCP server</title>
      <dc:creator>Zeke</dc:creator>
      <pubDate>Sun, 31 May 2026 20:01:16 +0000</pubDate>
      <link>https://dev.to/zekebuilds/return-a-402-instead-of-a-429-from-your-mcp-server-d7g</link>
      <guid>https://dev.to/zekebuilds/return-a-402-instead-of-a-429-from-your-mcp-server-d7g</guid>
      <description>&lt;p&gt;Last week I was reading through sentry-mcp issue #844 and watched a guy describe exactly the pain I keep running into. He had Cursor running parallel automation against the Sentry MCP, saturated the 60-request-per-minute bucket in seconds, and got back a 429 with no &lt;code&gt;Retry-After&lt;/code&gt; header. His agent just sat there. No backoff hint, no escape path, nothing to do but fail the run and ask a human to babysit it.&lt;/p&gt;

&lt;p&gt;That same week awslabs/mcp #2949 popped up where the MCP handshake itself was failing because &lt;code&gt;tools/list&lt;/code&gt; was tripping a 429 on the second call. And GLips/Figma-Context-MCP #258 had folks convinced the MCP was broken when really their parallel calls were just blowing through Figma's per-token limit on shared credentials.&lt;/p&gt;

&lt;p&gt;Same shape every time. The server says "no" in a way the agent cannot use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why 429 is the wrong answer for agents
&lt;/h2&gt;

&lt;p&gt;The 429 status was designed for humans behind browsers. &lt;code&gt;Retry-After: 60&lt;/code&gt; works fine if a person can read a banner that says "try again in a minute." It does not work when you have an autonomous agent that needs to decide right now whether to wait, retry, escalate, or pay.&lt;/p&gt;

&lt;p&gt;Most MCP servers do not even send &lt;code&gt;Retry-After&lt;/code&gt;. The agent gets a 429 body, maybe some JSON, and zero machine-readable information about what would let it succeed. So it does the dumb thing. It retries immediately. Or worse, it gives up and the whole tool chain breaks.&lt;/p&gt;

&lt;p&gt;There is no payment path. There is no proof-of-work path. There is no "I will do something to earn the right to call you" path. Just a closed door.&lt;/p&gt;

&lt;h2&gt;
  
  
  What 402 looks like on the wire
&lt;/h2&gt;

&lt;p&gt;HTTP 402 Payment Required has been sitting in the spec since 1997 waiting for someone to use it. With agents, it finally has a real job.&lt;/p&gt;

&lt;p&gt;A useful 402 response gives the caller a challenge it can solve programmatically. Two flavors:&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="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;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="na"&gt;WWW-Authenticate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PowChallenge realm="api", id="abc123", salt="...", difficulty=14&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"salt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"9f3c..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"difficulty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;14&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;"..."&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;Or for paid access:&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="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="...", invoice="lnbc30n1p..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both are deterministic. The agent reads the challenge, does the work (CPU cycles or a Lightning payment), submits the answer, and gets a token. No human in the loop. No guessing at backoff intervals. The server told the agent exactly what to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping an existing MCP server
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@powforge/captcha-mcp&lt;/code&gt; is one way to do this without writing the crypto yourself. It exposes three tools: &lt;code&gt;challenge&lt;/code&gt;, &lt;code&gt;verify&lt;/code&gt;, and &lt;code&gt;status&lt;/code&gt;. The package wraps &lt;code&gt;captcha.powforge.dev&lt;/code&gt; as the backend so you do not have to host the puzzle service.&lt;/p&gt;

&lt;p&gt;Drop it into your Claude Code or Cursor config:&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;"mcpServers"&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;"powforge-captcha"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&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="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@powforge/captcha-mcp"&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then on your own backend, when a caller hits a rate-limited endpoint, return a 402 pointing at the verify path. After the agent solves the puzzle and gets a token, your backend checks it:&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;-X&lt;/span&gt; POST https://captcha.powforge.dev/api/token/verify &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"token":"&amp;lt;token-from-verify-tool&amp;gt;"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Token good, request goes through. Token bad or expired, you 402 them again with a fresh challenge.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the agent sees
&lt;/h2&gt;

&lt;p&gt;From the agent's point of view the loop is short:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Call the tool. Get back a 402 with a challenge.&lt;/li&gt;
&lt;li&gt;Call the &lt;code&gt;challenge&lt;/code&gt; tool to get a fresh puzzle (or use the one from the 402 directly).&lt;/li&gt;
&lt;li&gt;Burn 5 to 10 seconds of CPU finding a nonce that produces a SHA-256 hash with 14 leading zero bits.&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;verify&lt;/code&gt; with the nonce. Get back a 5-minute HMAC-signed access token.&lt;/li&gt;
&lt;li&gt;Retry the original call with the token. Get the real response.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The agent never had to ask a human. It never had to guess at a retry interval. It paid for access in CPU cycles and got through.&lt;/p&gt;

&lt;h2&gt;
  
  
  PoW or Lightning
&lt;/h2&gt;

&lt;p&gt;Pick based on who is calling. PoW (free tier, SHA-256, around 5 to 10 seconds of CPU at 14 leading zero bits) works great for sporadic agents, exploration runs, and free-tier users. The cost is real but small, and it scales with how much the caller wants the resource.&lt;/p&gt;

&lt;p&gt;L402 over Lightning (paid tier, 3 sats per call by default) makes more sense for high-volume callers who would rather pay cash than burn CPU. Most agent operators will happily drop a few sats to skip the puzzle.&lt;/p&gt;

&lt;p&gt;You can offer both from the same endpoint. The 402 response tells the agent what is available, and the agent picks based on its own constraints.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; @powforge/captcha-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Package and docs: &lt;a href="https://www.npmjs.com/package/@powforge/captcha-mcp" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/@powforge/captcha-mcp&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you maintain an MCP server that is currently returning 429s, swap the response for a 402 with a real challenge. Your agent callers will thank you by actually completing their runs instead of hanging on a wait header they cannot read.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>Three ways to gate an MCP server: OAuth, L402, and proof-of-work</title>
      <dc:creator>Zeke</dc:creator>
      <pubDate>Sun, 31 May 2026 02:39:09 +0000</pubDate>
      <link>https://dev.to/zekebuilds/three-ways-to-gate-an-mcp-server-oauth-l402-and-proof-of-work-3392</link>
      <guid>https://dev.to/zekebuilds/three-ways-to-gate-an-mcp-server-oauth-l402-and-proof-of-work-3392</guid>
      <description>&lt;p&gt;Somebody at Sentry filed a bug last month: Cursor Automations started hitting rate-limit errors almost immediately after authenticating. The bucket was sized for humans — 60 requests per 60 seconds — and an agent tore through it in seconds.&lt;/p&gt;

&lt;p&gt;That's the MCP auth problem in miniature. You've got a server exposing tools. Agents call those tools. You want to slow down abuse, charge per call, or just make sure you don't blow up your LLM budget on some runaway loop. How do you do that without wiring up a full OAuth stack that breaks the first time an agent doesn't have a browser to open?&lt;/p&gt;

&lt;p&gt;Three real options exist right now. Here's how they compare.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 1: OAuth 2.1 (the spec says so)
&lt;/h2&gt;

&lt;p&gt;The MCP spec mandates OAuth 2.1 for authorization. If you're building a production server for enterprise customers — actual humans with accounts — this is the right call. You get scoped access, token revocation, audit trails. SSO works. Compliance teams stop emailing you.&lt;/p&gt;

&lt;p&gt;The problem is agents. OAuth 2.1 has an authorization code flow that requires a redirect URI. An agent running headless doesn't have a browser. DPoP and Workload Identity Federation are on the MCP roadmap but not shipped yet. If you need auth today and your callers are mostly agents, OAuth puts you in a hole.&lt;/p&gt;

&lt;p&gt;Good fit: enterprise SaaS, human-driven clients, compliance-heavy contexts.&lt;br&gt;
Bad fit: anonymous agents, public APIs, pay-per-call services.&lt;/p&gt;
&lt;h2&gt;
  
  
  Option 2: L402 Lightning payments
&lt;/h2&gt;

&lt;p&gt;L402 is an HTTP extension where a server responds to an unauthorized request with &lt;code&gt;402 Payment Required&lt;/code&gt; and an invoice in the &lt;code&gt;WWW-Authenticate&lt;/code&gt; header. The client pays it over Lightning and retries with the preimage as a credential.&lt;/p&gt;

&lt;p&gt;Two npm packages ship L402 for MCP right now: &lt;code&gt;lightning-wallet-mcp&lt;/code&gt; and &lt;code&gt;l402-kit-mcp&lt;/code&gt;. The model is clean: each tool call costs a fixed number of sats. No accounts, no sessions, no user. An agent with a Lightning wallet (Alby, Phoenixd, NWC) can handle the whole flow programmatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# First call — server responds with 402&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://your-mcp-server/tools/call &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name": "expensive_tool", "arguments": {}}'&lt;/span&gt;
&lt;span class="c"&gt;# HTTP/1.1 402 Payment Required&lt;/span&gt;
&lt;span class="c"&gt;# WWW-Authenticate: L402 invoice="lnbc...", macaroon="..."&lt;/span&gt;

&lt;span class="c"&gt;# Pay the invoice over Lightning, get the preimage&lt;/span&gt;
&lt;span class="c"&gt;# Retry with credentials&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://your-mcp-server/tools/call &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: L402 &amp;lt;macaroon&amp;gt;:&amp;lt;preimage&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name": "expensive_tool", "arguments": {}}'&lt;/span&gt;
&lt;span class="c"&gt;# HTTP/1.1 200 OK&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Good fit: agents with Lightning wallets, micropayment-per-call APIs, Bitcoin-native monetization.&lt;br&gt;
Bad fit: agents without wallets, free tiers, contexts where payment friction kills adoption.&lt;/p&gt;
&lt;h2&gt;
  
  
  Option 3: Proof-of-work
&lt;/h2&gt;

&lt;p&gt;PoW is the weird one. Instead of paying money, the caller burns CPU to solve a hashcash-style puzzle. The server sets a difficulty. The caller grinds until they find a nonce. No wallet required.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Get a challenge&lt;/span&gt;
curl https://your-mcp-server/api/challenge
&lt;span class="c"&gt;# {"challenge": "abc123", "difficulty": 4, "algorithm": "sha256"}&lt;/span&gt;

&lt;span class="c"&gt;# Solve it&lt;/span&gt;
npx @powforge/captcha solve &lt;span class="nt"&gt;--challenge&lt;/span&gt; abc123 &lt;span class="nt"&gt;--difficulty&lt;/span&gt; 4
&lt;span class="c"&gt;# {"nonce": "7f3a...", "solution": "0000..."}&lt;/span&gt;

&lt;span class="c"&gt;# Submit with solution in header&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://your-mcp-server/tools/call &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-PoW-Solution: challenge=abc123,nonce=7f3a..."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name": "tool", "arguments": {}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cost scales with difficulty. A legitimate caller solving once is fine. An abuser trying to hammer 10,000 calls hits a wall because each requires fresh compute. No central authority. No wallet. No redirect.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@powforge/captcha-mcp&lt;/code&gt; ships this as middleware for existing MCP servers. Wrap your server, set a difficulty, and callers solve before tool calls go through. It also supports L402 as an escape valve: pay sats instead of grinding if you'd rather not burn CPU.&lt;/p&gt;

&lt;p&gt;This is also a 429 fix. Instead of returning &lt;code&gt;429 Too Many Requests&lt;/code&gt; with no recovery path, the server hands the agent a puzzle — a machine-readable backoff signal an autonomous agent can satisfy without a human, an account, or a &lt;code&gt;Retry-After&lt;/code&gt; header that means nothing to code running in a loop.&lt;/p&gt;

&lt;p&gt;Good fit: anonymous agents, public APIs, abuse prevention without monetization, PoW-or-pay dual rail.&lt;br&gt;
Bad fit: high-throughput legitimate callers (CPU cost adds latency), real-time tools where every millisecond counts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The comparison
&lt;/h2&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;OAuth 2.1&lt;/th&gt;
&lt;th&gt;L402 Lightning&lt;/th&gt;
&lt;th&gt;Proof-of-Work&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Requires user account&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;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Requires Lightning wallet&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works headless today&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Revenue per call&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (sats)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stateless&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;An Astrix Security study this year found 88% of MCP servers require credentials of some kind but document almost none of it. The OpenClaw scan found 42,000+ unauthenticated MCP instances publicly exposed in January 2026. Most of those aren't enterprise installs — they're side projects and weekend builds.&lt;/p&gt;

&lt;p&gt;For that crowd, OAuth is overkill. L402 is elegant if the caller has a wallet. PoW gives you friction without a wallet requirement — callers can always get through, they just can't do it for free at scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision tree
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Building for enterprise with human users? &lt;strong&gt;OAuth 2.1.&lt;/strong&gt; The spec points here and compliance teams need it.&lt;/li&gt;
&lt;li&gt;Monetizing per tool call, callers have Lightning wallets? &lt;strong&gt;L402.&lt;/strong&gt; Clean, stateless, Bitcoin-native.&lt;/li&gt;
&lt;li&gt;Public API, anonymous agents, no accounts? &lt;strong&gt;PoW&lt;/strong&gt; — or PoW with L402 escape valve.&lt;/li&gt;
&lt;li&gt;Unsure? Start with PoW for the free tier and L402 for a paid tier. Add OAuth when an enterprise customer asks for it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Sentry rate-limit bug is fixable with any of these. But if you're building for agents first, OAuth is the last thing you reach for — not the first.&lt;/p&gt;




&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@powforge/captcha-mcp&lt;/code&gt; (PoW + L402 middleware): &lt;a href="https://www.npmjs.com/package/@powforge/captcha-mcp" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/@powforge/captcha-mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@powforge/captcha&lt;/code&gt; (standalone solver): &lt;a href="https://www.npmjs.com/package/@powforge/captcha" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/@powforge/captcha&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Sentry MCP issue #844: &lt;a href="https://github.com/getsentry/sentry-mcp/issues/844" rel="noopener noreferrer"&gt;https://github.com/getsentry/sentry-mcp/issues/844&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mcp</category>
      <category>bitcoin</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>A verifiable Bitcoin randomness beacon, 50 sats per draw</title>
      <dc:creator>Zeke</dc:creator>
      <pubDate>Sat, 30 May 2026 23:55:29 +0000</pubDate>
      <link>https://dev.to/zekebuilds/a-verifiable-bitcoin-randomness-beacon-50-sats-per-draw-11mf</link>
      <guid>https://dev.to/zekebuilds/a-verifiable-bitcoin-randomness-beacon-50-sats-per-draw-11mf</guid>
      <description>&lt;p&gt;Randomness is one of those things you don't think about until you need to trust it. And then you think about it a lot.&lt;/p&gt;

&lt;p&gt;Pick a lottery, a game seed, a sampling job, a DLC oracle input. Anything where the output has money or fairness riding on it. The usual options ain't great. A single server tells you "trust me, this was random." A blockchain hash is only random after the block is mined, and the miner can see your bet before they publish. A VRF binds the output to one key holder, which is a single point of trust by another name.&lt;/p&gt;

&lt;p&gt;What you actually want is a draw where nobody can predict the result before it closes, nobody can change it after, and anybody can verify it offline. That's what the PowForge /draw beacon does. Multi-party entropy, Schnorr-signed, 50 sats per fetch.&lt;/p&gt;

&lt;h2&gt;
  
  
  How an epoch works
&lt;/h2&gt;

&lt;p&gt;Time is sliced into 5-minute windows. The current epoch ID is just:&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;epoch_id&lt;/span&gt; &lt;span class="o"&gt;=&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;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During an epoch, anyone can contribute. You solve a small SHA-256 proof-of-work challenge (18 bits, takes a second on a laptop) and POST your contribution. It costs nothing and your x-only Schnorr pubkey gets bound into the contribution hash, so you can prove your entropy was included.&lt;/p&gt;

&lt;p&gt;When the window closes and at least 5 unique contributors have submitted, the oracle aggregates everything deterministically. The beacon formula is public:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sha256("DRAW" || epoch_id || sha256(epoch_id || sorted_contribution_hashes_concat))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sorted concatenation matters. It means the order contributors arrive doesn't change the output, but a single byte from any one of them shifts the whole 32-byte beacon. The oracle then signs the beacon with a BIP-340 Schnorr key and seals the epoch. After that, the result is fixed forever and nobody, including the oracle, can change it.&lt;/p&gt;

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

&lt;p&gt;Contributing requires mining the PoW, so the curl path for that one's easier with the npm client below. Reading a sealed beacon is straight L402:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Step 1: request the beacon, oracle returns 402 + a Lightning invoice&lt;/span&gt;
curl &lt;span class="nt"&gt;-si&lt;/span&gt; https://attest.powforge.dev/api/v1/draw/5933907

&lt;span class="c"&gt;# HTTP/1.1 402 Payment Required&lt;/span&gt;
&lt;span class="c"&gt;# WWW-Authenticate: L402 invoice="lnbc500...", macaroon="AgE..."&lt;/span&gt;

&lt;span class="c"&gt;# Step 2: pay the invoice with any Lightning wallet (50 sats)&lt;/span&gt;
&lt;span class="c"&gt;# you receive a payment preimage, 32 bytes hex&lt;/span&gt;

&lt;span class="c"&gt;# Step 3: retry with the macaroon and preimage&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://attest.powforge.dev/api/v1/draw/5933907 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: L402 &amp;lt;macaroon&amp;gt;:&amp;lt;preimage&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response gives you &lt;code&gt;beacon_random&lt;/code&gt; (the 32-byte entropy) and &lt;code&gt;signature&lt;/code&gt; (BIP-340 Schnorr). Verify it against the oracle pubkey and you're done.&lt;/p&gt;

&lt;h2&gt;
  
  
  npm client
&lt;/h2&gt;

&lt;p&gt;There's a thin client that handles the PoW mining and L402 dance for you:&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/attest-client
&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;AttestClient&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/attest-client&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;client&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;AttestClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Contribute entropy to the current epoch (free, mines PoW automatically)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;contrib&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contributeDrawEntropy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your64hexschnorrpubkeyhere...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;  &lt;span class="c1"&gt;// your x-only Schnorr 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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;contributed to epoch:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;contrib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;epoch_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Fetch the sealed beacon for a closed epoch (pays 50 sats via L402)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;beacon&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDrawBeacon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contrib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;epoch_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;l402Token&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;beacon_random:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;beacon&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;beacon_random&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;signature:    &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;beacon&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole API surface. Contribute is free, fetch costs 50 sats and gives you a signed value any Bitcoin-native verifier can check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why you can trust the output
&lt;/h2&gt;

&lt;p&gt;Three properties carry the whole thing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-party by construction.&lt;/strong&gt; The oracle can't produce a beacon alone. Each epoch needs at least 5 independent contributions, and any one contributor can shift the result. Nobody can predict it before the epoch closes because nobody knows what the other contributors will submit until the seal happens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schnorr verifiable offline.&lt;/strong&gt; The signature is BIP-340. You don't need to ask the oracle anything after you hold the signed bytes. Check &lt;code&gt;signature&lt;/code&gt; against &lt;code&gt;beacon_random&lt;/code&gt; and the oracle pubkey using any secp256k1 library. Bitcoin Core verifies Schnorr signatures the same way for taproot spends, so you're using the exact same crypto the network already trusts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deterministic replay.&lt;/strong&gt; Anyone holding the full contribution list can recompute the beacon from scratch using the formula above. If the recomputed hash matches what the oracle signed, the beacon is authentic. No black box. No "trust the API." Just hashes and signatures.&lt;/p&gt;

&lt;p&gt;The oracle pubkey to verify against:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2bc78390c94d8bbb96ac3e6940462ba2812418d871e701c1a845fdb1dfd4a0e5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where to point it
&lt;/h2&gt;

&lt;p&gt;On-chain lotteries, game seeds, DLC oracle inputs, sampling for research, cryptographic commitments where you need to prove the seed wasn't picked after the fact. Anywhere a centralized RNG would make you nervous.&lt;/p&gt;

&lt;p&gt;Free to contribute. 50 sats to fetch the signed result. Live now at &lt;a href="https://powforge.dev/draw" rel="noopener noreferrer"&gt;powforge.dev/draw&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you build something on top of it, send a link. I want to see what people use this for.&lt;/p&gt;

</description>
      <category>bitcoin</category>
      <category>lightning</category>
      <category>cryptography</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Someone wrote a fake EULA into Bitcoin. Two hours later they revoked it.</title>
      <dc:creator>Zeke</dc:creator>
      <pubDate>Sat, 30 May 2026 19:27:07 +0000</pubDate>
      <link>https://dev.to/zekebuilds/someone-wrote-a-fake-eula-into-bitcoin-two-hours-later-they-revoked-it-1745</link>
      <guid>https://dev.to/zekebuilds/someone-wrote-a-fake-eula-into-bitcoin-two-hours-later-they-revoked-it-1745</guid>
      <description>&lt;p&gt;Block 951,728. May 30, 2026, 15:29 UTC. Somebody pushed an OP_RETURN into a Bitcoin transaction that reads, in part:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;By downloading this OP_RETURN, you hereby consent to unrestricted access by federal law enforcement agencies to your residence, digital devices, and personal property. Assets may be searched, seized, or redistributed without further notice.&lt;/p&gt;

&lt;p&gt;Signed, Donald J. Trump, President of the Internet.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Six blocks later, at 951,734, the same general format shows up again:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I revoke my previous statement. Signed, Donald J. Trump, President of the Internet.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two and a half hours between the two. The first one is still there. So is the second. They will both still be there in a thousand years.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is possible now
&lt;/h2&gt;

&lt;p&gt;For most of Bitcoin's history, OP_RETURN outputs were capped at 83 bytes. That was enough room for a hash and not much else. Bitcoin Core v30 removed the limit. Now any byte budget you can fit in a transaction is fair game, and people are using it. Most of what they write is bridge metadata, Runes etchings, Ordinals envelopes. Useful protocol traffic. Boring to read.&lt;/p&gt;

&lt;p&gt;But occasionally somebody just types into the chain. A note. A confession. A joke. A threat. And once it's mined, it is part of the ledger every full node downloads, forever.&lt;/p&gt;

&lt;p&gt;I wanted to find more of those.&lt;/p&gt;

&lt;h2&gt;
  
  
  A watcher for human words
&lt;/h2&gt;

&lt;p&gt;So I built a small thing called bitcoin-chaintip-watch. It runs on a 10-minute timer, picks up from the last block it scanned, and walks each new block looking for OP_RETURN outputs that decode to readable English. It skips known protocol prefixes. It skips bridge garbage. It skips anything that looks like a hash or a swap memo. What is left is, generally, somebody talking.&lt;/p&gt;

&lt;p&gt;The filter is small:&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;isInterestingOpReturn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ratio&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="nf"&gt;isKnownProtocol&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// Runes, Ordinals, Omni, etc.&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;ratio&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;              &lt;span class="c1"&gt;// mostly printable bytes&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&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;clean&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&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="nf"&gt;isLikelyHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&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="nf"&gt;isDefiNoise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// cross-chain / swap metadata&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="nf"&gt;hasHumanWords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// at least 3 four-letter words&lt;/span&gt;
  &lt;span class="k"&gt;return&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;hasHumanWords is the trick. Three or more alpha tokens of length four or up that are not hex. That single test does most of the work of separating "person wrote a sentence" from "machine emitted a header."&lt;/p&gt;

&lt;p&gt;First run, the watcher surfaced both Trump-Internet inscriptions inside the same scan window. txid for the EULA is &lt;code&gt;75e5870110c1d8a653c2fd15f775d3b5ce24535a30460e846cff4a52fd50b643&lt;/code&gt;. For the revocation, &lt;code&gt;188091412022c7e8582b5639719905df0e152a5ee51128b982b5fd24a8f5849f&lt;/code&gt;. You can look both of them up on any block explorer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The joke and the joke under the joke
&lt;/h2&gt;

&lt;p&gt;The top-layer joke is the EULA itself. It is doing the bit where end-user license agreements are absurd contracts that nobody reads, and treating that absurdity as if it has the force of law. By looking at the bytes you have already agreed. Opt out requires a notarized statement, on chain, in a future block. The signature line is presidential.&lt;/p&gt;

&lt;p&gt;The joke under the joke is the medium. Bitcoin OP_RETURN outputs are by design provably unspendable. They exist only as messages. Somebody took that property and turned it into a legal-document format that activates the moment your node validates the block. To rescind it, you have to spend real fees and write a revocation into another block. Which is what happened. Two and a half hours later, the author posted a one-line retraction in the same voice.&lt;/p&gt;

&lt;p&gt;The revocation costs more than the original. Bitcoin does not have an edit button. It does not have a delete button. It has only "write another thing later." If you change your mind about a permanent record, the only correction is more permanent record. That asymmetry is the design. The author of these two inscriptions just used the design as a punchline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters past the gag
&lt;/h2&gt;

&lt;p&gt;Bitcoin is the only public medium where you can publish something that cannot be taken down by anybody, including yourself. That is a strange property to have running globally on consumer hardware. Most of what gets inscribed is throwaway. Some of it is not. Iowa caucus precinct results from 2016 are in there. A SegWit activation block carries both a Conio celebration and a death threat against a sitting US president. Now a fake-EULA satire is in there, plus the author's regret about it, six blocks apart.&lt;/p&gt;

&lt;p&gt;It is the only confessional booth with no priest and no eraser.&lt;/p&gt;

&lt;p&gt;The watcher does not interpret any of this. It just lifts the human-readable lines out of the noise and writes them to a JSON file. A person decides what is worth keeping. The Trump-Internet pair was worth keeping. It is now in the Bitcoin Museum collection alongside the SegWit-block threat, the 2016 election oracle, and the rest of the inscriptions that show what people actually do when handed a permanent megaphone with no off switch.&lt;/p&gt;

&lt;h2&gt;
  
  
  See the artifacts
&lt;/h2&gt;

&lt;p&gt;The EULA and the revocation live at &lt;a href="https://powforge.dev/museum/" rel="noopener noreferrer"&gt;powforge.dev/museum&lt;/a&gt;, with verifier links to mempool.space for both transactions. The full collection is around 550 artifacts and growing as the watcher keeps running.&lt;/p&gt;

&lt;p&gt;If you want to point a similar tool at your own bitcoin node, the filter shown above is the whole logic that matters. It needs nothing exotic. Just RPC access, a state file, and a 10-minute timer.&lt;/p&gt;

&lt;p&gt;The chain remembers everything. Most of it is boring. The interesting parts are easier to find than you would think.&lt;/p&gt;

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

</description>
      <category>bitcoin</category>
      <category>opensource</category>
      <category>webdev</category>
      <category>satire</category>
    </item>
  </channel>
</rss>
