Proof of Personhood

Ask whether an address belongs to a unique, verified human. Free, no key, no account, no rate card — and you can check the answer without trusting us.

Anything handed out per-person is drained by whoever can forge the most identities. Airdrops, votes, reviews, queues, free tiers, support. The usual fixes — phone numbers, CAPTCHAs, government ID — either exclude the people who most need including, or fail against anyone willing to spend money.

ANKH maintains a register of people who have each been verified exactly once, checked independently by every node rather than by one company's database. This endpoint exposes that register, and nothing else about the person.

Ask

GET https://api.ankh.cash/api/v1/personhood/ankh_<address>
{
  "success": true,
  "data": {
    "address": "ankh_a5e4c5a1…",
    "isHuman": true,
    "since": "2026-02",
    "issuedAt": 1789270000000,
    "expiresAt": 1789270300000,
    "attestation": {
      "claim": "{\"address\":\"ankh_a5e4…\",\"isHuman\":true,…}",
      "nodeAddress": "ankh_ee6f8d81…",
      "publicKey": "040b3a692af13f…",
      "signature": { "r": "…", "s": "…" },
      "algorithm": "secp256k1/sha256"
    }
  }
}

What you get, and what you deliberately do not

Verify the answer yourself

The response is signed by the node that answered. You do not have to believe this server — check the signature, then check that the signer is a node the chain actually recognises.

import { createHash } from 'crypto';
import pkg from 'elliptic';
const ec = new pkg.ec('secp256k1');

const res  = await fetch(`https://api.ankh.cash/api/v1/personhood/${address}`);
const { data } = await res.json();
const att = data.attestation;

// 1. the signature covers the exact claim
const hash = createHash('sha256').update(att.claim).digest('hex');
const key  = ec.keyFromPublic(att.publicKey, 'hex');
if (!key.verify(hash, att.signature)) throw new Error('bad signature');

// 2. the claim says what the response says
const claim = JSON.parse(att.claim);
if (claim.address !== address) throw new Error('claim is for a different address');
if (Date.now() > claim.expiresAt) throw new Error('attestation expired');

// 3. the signer is a node the chain recognises
const nodes = await (await fetch('https://api.ankh.cash/api/v1/nodes')).json();
const known = nodes.data.some(n => n.publicKey === att.publicKey && n.isActive);
if (!known) throw new Error('signed by an unregistered node');

console.log(claim.isHuman ? 'unique human' : 'not verified');

Step three is what makes this different from a normal API. A server can lie about anything it alone asserts — but it cannot forge a signature from a node it does not control, and the node registry is on-chain and public.

Sign in with ANKH

Asking whether an address is a person is one thing; knowing the visitor in front of you controls that address is another. Anyone can type someone else's address. So the visitor signs a challenge, and you get a result you can check.

<script src="https://ankh.cash/ankh-connect.js"></script>
<script>
  document.querySelector('#join').onclick = async () => {
    const r = await AnkhConnect.signIn({ reason: 'One account per person' });
    if (r.isHuman) grantAccess(r.address);
  };
</script>

That opens ankh.cash, where the visitor unlocks their wallet and approves. Their key never leaves that window; your page receives a signed challenge and asks the node to verify it. You get back { address, isHuman, since, controlsAddress } — and nothing else, because the chain holds nothing else.

Call it from a click handler — browsers block popups opened any other way. And treat isHuman: false as a real answer: it means a genuine wallet that has not been verified, not a failure.

Under the hood it is two endpoints, if you would rather not use the script: POST /personhood/challenge returns an HMAC-tagged challenge, and POST /personhood/verify takes back { challenge, tag, signature }. Challenges are stateless and expire in five minutes, so nothing is stored between the two calls and any node can verify a challenge another issued.

Checking many at once

POST https://api.ankh.cash/api/v1/personhood/batch
Content-Type: application/json

{ "addresses": ["ankh_aaa…", "ankh_bbb…"] }   // up to 100

The batch form returns booleans without attestations, for filtering a list. Capped at 100 per request so it cannot be used to walk the register.

What this is good for

What it is not

Honest limits

Verification currently runs across a small number of nodes. A register is only as strong as the number of independent parties maintaining it, and ours is early — a larger, more widely distributed set of operators is what makes this hard to subvert. Hold it up is the thing that improves it.

Face templates are replicated to every node so each can check independently. That is what makes the answer trustworthy, and it also means more parties hold biometric material than a single-operator design would. Proving uniqueness without revealing the template — a zero-knowledge construction — is the right end state and is not built yet.

Source: github.com/ankhcash/ankh-chain · MIT · The Ledger