<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Zeke</title>
    <description>The latest articles on DEV Community by Zeke (@zekebuilds).</description>
    <link>https://dev.to/zekebuilds</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3866714%2F688cd691-92a0-4825-af29-ca57b7b020bb.png</url>
      <title>DEV Community: Zeke</title>
      <link>https://dev.to/zekebuilds</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zekebuilds"/>
    <language>en</language>
    <item>
      <title>How I Stopped Form Spam Without reCAPTCHA</title>
      <dc:creator>Zeke</dc:creator>
      <pubDate>Wed, 08 Apr 2026 11:39:31 +0000</pubDate>
      <link>https://dev.to/zekebuilds/how-i-stopped-form-spam-without-recaptcha-4gld</link>
      <guid>https://dev.to/zekebuilds/how-i-stopped-form-spam-without-recaptcha-4gld</guid>
      <description>&lt;p&gt;Google reCAPTCHA started charging after 10k assessments per month. hCaptcha tanks conversion rates with those "click every bus" puzzles. Turnstile locks you into Cloudflare's ecosystem. I got tired of picking between surveillance, vendor lock-in, and a monthly bill just to keep bots off a contact form.&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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



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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

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




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

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