Skip to content

PoP-verified (proof of personhood)

The PoP-verified trust layer (Layer 4 in the trust spectrum) elevates a Human Proof from “a wallet attested” to “a unique human attested.” Verification happens fully on-chain through Worldcoin’s IWorldID verifier; no off-chain trust in any service.

A separate resolver (FidemarkPoPResolver) decodes the attestation, recomputes the externalNullifier and signal from the same content hash + attester, and calls the Worldcoin verifier. The same nullifier cannot be reused for the same content, preventing the same human from attesting the same thing twice.

  1. Hash the content client-side. Same as every other Fidemark flow.
  2. User runs IDKit verification. The user’s World App produces a Groth16 proof against the canonical action actionForContent(contentHash), with signal = (contentHash, attester) baked in.
  3. Submit on-chain. The SDK packages the proof into the PoP schema and submits to EAS. The resolver verifies the proof on-chain; reverts if invalid.
  4. Verify like any other attestation. The decoded result has type = "pop" plus pop.{root, nullifierHash, proof, ...} fields.

All three SDKs expose identical hash-to-field helpers so frontends can pre-compute exactly what the chain will check.

import { actionForContent, externalNullifierFor, signalHashFor, hashContent } from "@fidemark/sdk";
const contentHash = hashContent(myArticle);
actionForContent(contentHash); // "f1b94d27..." (30 chars)
externalNullifierFor("app_fidemark_xyz", contentHash); // BigInt
signalHashFor(contentHash, signerAddress); // BigInt

Why this format? Worldcoin’s Developer Portal caps action identifiers at 32 characters. We use a 2-char prefix (f1) plus 28 hex chars of the content hash (= 14 bytes = 112 bits of entropy). Collision probability across realistic attestation volumes is negligible.

The on-chain resolver exposes actionForContent(bytes32), externalNullifierFor(bytes32), and signalHashFor(bytes32,address) view functions that return identical values, useful for cross-checking IDKit input from a block explorer.

The proof comes from your IDKit integration; the snippet below assumes you already have it. See Configuration for every constructor option.

import { Fidemark, getNetwork } from "@fidemark/sdk";
const fidemark = new Fidemark({
network: getNetwork("base-sepolia"),
privateKey: process.env.PRIVATE_KEY,
});
const result = await fidemark.attestHumanWithPoP({
content: myArticle,
contentType: "text/article",
worldIdProof: {
root: "0x...",
nullifierHash: "0x...",
proof: ["0x...", "0x...", "0x...", "0x...", "0x...", "0x...", "0x...", "0x..."],
},
});

The resolver checks:

  • creator == attestation.attester (you can only PoP-attest as yourself)
  • proofMethod is on the resolver allowlist (pop-verified-worldid is registered at deploy)
  • createdAt is not too far in the future (1 day drift)
  • (externalNullifier, nullifierHash) has not been used before
  • IWorldID.verifyProof(...) does not revert

If any check fails, the attestation is rejected and no UID is created.

  1. Register a Worldcoin app at the Developer Portal. Pick Managed unless you have specific compliance reasons to self-manage signer keys.
  2. Allowlist actions of the form f1<28-hex> (the first 28 hex chars of the content hash, prefixed with f1). Easiest: enable the “create actions on demand” setting if your portal tier supports it; otherwise allowlist actions per content hash you publish.
  3. Frontend integration with IDKit:
    • Configure <IDKitRequestWidget> (v4) with your app_id, the canonical action, and the signal derived from (contentHash, attester).
    • Pass the resulting proof to your SDK of choice.
  • Not a humanity registry. The protocol stores the nullifier, not the human’s identity. You can prove “the same human attested these N pieces of content” via a stable (appId, action) externalNullifier, but you cannot link the nullifier to a real-world identity from the chain alone.
  • Not the only PoP source. Today’s resolver supports the World ID family. Other PoP providers (Gitcoin Passport, Privado ID, etc.) need their own schema + resolver because their proof shapes differ. They can be added without breaking existing PoP attestations.
  • Not Layer 3 (TEE). PoP proves identity, TEE proves code execution. They are orthogonal and can be combined via referenced attestation chains if a use case needs both.