Paywall Any API Endpoint With Two Prices: Sats or Compute
You built an API. It works. Then the scrapers show up. 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? 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. This post walks the whole thing end to end against a live server at gate.powforge.dev . Every number and response below came off a real request. Copy the curl lines and run them yourself. The two-price idea The gate hands the caller a choice for every request: - Pay compute. 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. - Pay sats. Settle a Lightning invoice for 10 sats. About a tenth of a cent. Costs the caller money, costs you nothing to verify. Both paths end the same way: the caller gets the gated response. The point is that both paths cost the caller something. A scraper running at scale cannot do either one for free, so the free-riding stops without you ever standing up a login. Path 1: pay with compute Ask the server for a challenge. curl -s https://gate.powforge.dev/api/challenge You get back a fresh nonce and a difficulty: {"nonce":"35df6dfac728013209f389eb8921f5aa6236b8a7b12f794c31b01edac563756f","difficulty":20} Difficulty 20 means the caller has to find a string solution such that SHA-256(nonce + solution) starts with 20 leading zero bits. There is no clever shortcut. You grind candidates until one hits. Here is the whole solver in Node. No dependencies. const crypto = require('crypto'); function valid(nonce, sol, diff) { const h = crypto.createHash('sha256').update(nonce + sol).digest(); const wholeBytes = Math.floor(diff / 8); const remBits = diff % 8; for (let i = 0; i 0) { const mask = 0xFF { const { nonce, difficulty } = await (await fetch('https://gate.powforge.dev/api/challenge')).json(); let attempt = 0, sol; const t0 = Date.now(); do { attempt++; sol = attempt.toString(36); } while (!valid(nonce, sol, difficulty)); console.log(solved in ${attempt} attempts, ${Date.now() - t0}ms, solution=${sol}); const res = await fetch('https://gate.powforge.dev/api/solve', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nonce, solution: sol }), }); console.log(await res.json()); })(); Running that against the live server just now: solved in 113275 attempts, 1277ms, solution=2fej One core, one and a third seconds, a hundred thousand hashes. Then the solve POST comes back with the goods: { "token": "5ec59d49aa431e1c...", "method": "pow", "content": ["Bitcoin is not money. Bitcoin is a weapon system.", "..."] } 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. The server side of the check is tiny. It is the same hash test, run once: function verifyPoW(nonce, solution, difficulty) { const hash = crypto.createHash('sha256').update(nonce + solution).digest(); const wholeBytes = Math.floor(difficulty / 8); const remBits = difficulty % 8; for (let i = 0; i 0) { const mask = 0xFF : " 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. There is a discovery endpoint too, so an automated caller can learn the price and shape before it spends anything: curl -s https://gate.powforge.dev/l402/info {"service":"pow-gate","version":1, "endpoints":[{"path":"/l402/unlock","method":"POST","scope":"pow-gate:unlock", "price_sats":10,"auth":"L402 (RFC 7235 + Lightning)"}]} An agent hits /l402/info , 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. Why put both on the same endpoint Because your callers are not all the same, and you do not know which is which up front. 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. 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. 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. 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. Wiring it into your own service The gate is a thin layer in front of whatever you are already serving. The shape is: - GET /api/challenge issues a nonce and remembers it briefly. - POST /api/solve checks the hash, and on success returns your real response. - POST /l402/unlock mints a macaroon and invoice on the first call, and on the paid retry checks the preimage and returns the same real response. 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. 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. Live server: https://gate.powforge.dev Top comments (0)
Comments
No comments yet. Start the discussion.