World ID Verification
Legacy provider
Section titled “Legacy provider”World ID is one of four verification methods on
ohshii_governance, and it is now a legacy one. It carries Strong strength and every existing World ID verification remains valid indefinitely — nothing on this page is being taken away from anyone who already holds it.What changes is intake: the provider no longer accepts new proofs (
accepts_new_proofs = false). The flow below documents how existing World ID verifications were made. A wallet that wants to become verified uses Google (Standard) or Telegram (Strong).The veto-override round’s fresh attestation (§5.5) is a Telegram attestation. A World ID attestation is accepted only while this provider accepts new proofs, so with intake closed Telegram is the anchor.
ohshii_governance can gate ONS voting behind a World ID proof-of-personhood
check. This document is the single reference for end users (“what happens to
my data?”), developers (“how is this wired?”) and DAO voters (“how is it
configured or paused?”). It covers the verification flow, the three
configuration states, the privacy model, the cryptography behind the
on-chain RP signatures, the governed v3/v4 request-preset toggle, the
veto-override re-attestation round, the rate-limit / cycle-budget posture and
the governance handles exposed for operations.
Protocol version. OhShii integrates World ID 4.0 via
@worldcoin/idkit@4.x. World is migrating credential holders from World ID 3.0 to 4.0, so the canister accepts both wire shapes: a new World App user produces a 4.0 (passport) proof and a pre-migration holder produces a legacy 3.0 proof, and thePOST /api/v4/verify/{rp_id}endpoint accepts the legacy 3.0 shape natively (distinguished by theprotocol_versionfield on the request body — there is noallow_legacy_proofsfield on that body). Which shape the in-browser widget requests is a governed toggle (see §9); what the canister accepts is always both. See the live 3.0 → 4.0 migration guide.
Scope. The onboarding gate described here applies to ONS proposals (the OhShii ecosystem governance canister); ordinary SONS proposals — one DAO per LGE campaign — do not go through this onboarding gate today. The separate veto-override re-attestation round (see §5.5) also serves SONS-family DAOs, but always with the human attesting a fresh proof directly on ONS — ONS trusts no SONS assertion.
Integration model & current state (at a glance)
Section titled “Integration model & current state (at a glance)”A developer- and auditor-facing summary of where OhShii stands with World ID and why. The detailed mechanics are in the numbered sections below; this is the orientation a reader can skim.
Current state.
| Property | Value |
|---|---|
| Protocol | World ID 4.0 (the current protocol), in PREVIEW — World’s early-adopter phase before General Availability (GA). |
| RP signature | Produced on-chain by the canister via IC threshold ECDSA: the canister signs each proof request itself, and the signing key exists only as shares distributed across the subnet’s nodes — no off-canister server, no exported secret. See §5. |
| Management mode | Managed — World currently holds the key it validates the canister’s signature against. Verification works fully under Managed. |
| Status | Legacy — closed to new proofs since 2026-09-05. Existing World ID verifications stay valid and keep Strong strength; the configuration stays on-chain and is listed in §1. New wallets verify with Telegram (Strong) or Google (Standard). |
The decentralization-relevant fact: the relying-party signature is a canister capability, identical in Managed and Self-Managed mode. The canister produces the same signature either way; the only difference is what World validates it against.
The v3→v4 migration is done — and is independent of Managed/Self-Managed.
- Moving from the legacy v3/3.0 proof format to v4 was required: World App users after the 4.0 cutover produce 4.0-only proofs. This migration is complete.
- World ID 4.0 does not require Self-Managed mode. Managed and Self-Managed are both valid under 4.0 — the protocol version (v3 vs v4) and the key-custody mode (Managed vs Self-Managed) are orthogonal. (This is the most common misconception; going to 4.0 does not force Self-Managed.)
- No existing verified user has to re-verify. While the provider accepts new
proofs, legacy 3.0 proofs are accepted too — the in-browser widget’s legacy flag lets
a pre-migration holder produce a 3.0 proof, and
/api/v4/verifyaccepts the 3.0 shape natively. This preservation holds regardless of the request toggle below. With intake closed (see the banner), no new proof of either format is registered, and every existing World ID verification keeps counting.
The governed v3/v4 request toggle (what the DAO controls).
- A governed setting (
request_preseton the World ID config) controls which proof format the widget requests. During the transition phase it is also reachable by the deploying principals, which are removed at the DAO handover; a normal-lane DAO proposal is the route today and the only route afterwards. The default requests v4-capable while still accepting legacy v3. See §7 and §9. - Scope, precisely: the toggle controls only what the widget requests, never what the canister accepts. Legacy acceptance stays on regardless, so the toggle can never lock out an existing verified user.
Managed vs Self-Managed — a deferred, optional decentralization upgrade (not a functional requirement).
- Managed = World holds the validation key. Self-Managed = the canister’s own on-chain-registered key is the validation target (World holds none of OhShii’s keys).
- Why switch: Self-Managed is the more decentralized posture — OhShii controls its signing identity on-chain, with no trust in a World-held key. It is a coherence/decentralization improvement, not a functional necessity: verification works identically either way.
- When: deferred to World ID GA. Switching during PREVIEW risks paying the on-chain registration fee twice if World forces a key rotation at GA; deferring buys one clean registration once the protocol is stable. Zero canister code change either way (the canister already signs correctly). See §5 “Self-Managed is deferred to GA”.
- Who: it is an operational action (a Portal switch + an on-chain RpRegistry registration), not a canister action — so, unlike the request toggle, it is not a DAO on-chain proposal. This is the one point where OhShii’s on-chain decentralization meets an external service (World) with its own controls: authority over the World account is external to the DAO’s governance.
What happens at GA.
GA = World ID 4.0 leaving preview and becoming the stable, final version. At GA: (a) World may require a signer-key rotation — the design tolerates it, because the canister’s key derives from a stable derivation tag, so a rotation is a re-read + re-register, never a key loss; (b) if OhShii wants the full decentralization posture, GA is the moment to switch to Self-Managed, once and cleanly. §11 lists what that switch involves.
Honest residuals (consistent with Capture Resistance).
- Managed mode is a small, disclosed trust in a World-held validation key — removable at GA via Self-Managed. A trade-off, not a flaw.
- A superseded legacy attestation endpoint,
verify_fresh_override_proof_for, remains callable alongside the caller-driven path (see Capture Resistance).
1. Configuration states
Section titled “1. Configuration states”World ID’s share of the sybil gate has three distinct states, driven by two fields of
WorldIdConfig on canister. The gate as a whole is the union of the four methods’
configurations: it applies when any of them is enabled.
| State | rp_id | enabled | Effect on ONS voting | What the user sees on /verify |
|---|---|---|---|---|
| Not configured | empty | either | World ID cannot verify anyone; the verification flow cannot start. With enabled = false it adds no gate — voting is open to any authenticated principal unless another method’s gate is enabled. With enabled = true it still makes the gate mandatory, exactly as in Required. | Grey banner “Verification is not available yet.” CTA disabled. |
| Optional | set | false | World ID adds no gate — voting is open unless another method’s gate is enabled. Verification is possible while the provider accepts new proofs; the LGE participation tiers above Guest read strong verification (is_strongly_verified), independently of this flag. | Amber banner “Verification is optional right now.” CTA reads “Verify anyway (optional)”. |
| Required | set | true | vote() rejects unless the caller is verified with any method (is_verified), and on a Critical proposal unless the method is a strong one (Telegram, World ID or DecideID). Both the canister and the frontend enforce this. | Cyan banner “Verification required to vote on ONS proposals.” |
Transitions between states are governed:
- Not configured → Optional / Required: ONS proposal
set_world_id_config. - Optional ↔ Required: same proposal, flipping
enabled. - Emergency pause: ONS proposal
set_world_id_daily_cap(0)halts new verifications without touchingenabled; existing verifications remain valid.
Across code upgrades: the configuration the DAO set is
kept as it is. An upgrade whose code cannot read the stored configuration is
refused, and the canister keeps running its previous code with the stored value
untouched; while the value cannot be read, the configuration reads as disabled and
closed to new proofs, and set_world_id_config refuses to write. The canonical
default configuration is applied at most once per canister, by the first upgrade
with this code and only if no configuration exists, so a configuration the DAO
cleared back to “Not configured” stays cleared. The marker that records this is protected the same way: an upgrade whose code cannot read it is refused.
The flag enabled only controls the vote gate. Users can still verify
voluntarily while enabled = false (as long as rp_id is set) so the
Human / Fish / Shark / Whale LGE tiers, which also read verification state,
continue to work independently from the vote gate.
accepts_new_proofs — the retirement flag
Section titled “accepts_new_proofs — the retirement flag”WorldIdConfig carries a second, independent flag, and it is the one that
retires the provider:
| Flag | Question it answers |
|---|---|
enabled | Does this provider make the verification gate mandatory? |
accepts_new_proofs | Will this provider still register a new proof? |
They are orthogonal. The three states in the table above are all states of
enabled; accepts_new_proofs cuts across them:
accepts_new_proofs = true(the state until 2026-09-05, and the default a configuration that predates the flag resolves to) — everything on this page is live.accepts_new_proofs = false— the retired state. Every entry point that would register a new proof refuses with a message saying the provider no longer accepts them. Everyone already verified stays verified, keeps voting, keeps their LGE tier, and keeps Strong strength. Retirement removes intake, never standing.
The flag is flipped by set_world_id_accepts_new_proofs, executed by
ohshii_governance through an approved proposal (and, during the transition
phase, also by the deploying principals). The tier depends on the
direction: setting it to true (reopening intake) runs the critical route and is
guardian-vetoable; setting it to false (closing intake) runs the standard route. The
positional configuration setter does not touch it: changing any other field preserves
whatever the retirement flag was set to.
A retiring wallet’s own retry is answered before the refusal, so someone who is already verified and calls again gets their own clear answer rather than a refusal for something they already hold.
Retired to new proofs on 2026-09-05 — the configuration stays on-chain
Section titled “Retired to new proofs on 2026-09-05 — the configuration stays on-chain”On 2026-09-05 the provider was closed to new proofs (set_world_id_accepts_new_proofs(false)),
and its gate flag is switched off once Telegram and Google are enabled
(set_world_id_config with enabled = false) — in that order, so the gate never drops
in between. Neither step erases anything: WorldIdConfig is a stored record, the
positional setter writes only the five fields it names and reads the other two back,
and the verified set is a separate store that no configuration change touches. The
whole record stays readable by anyone:
icp canister call xnsdw-yaaaa-aaaal-qsqua-cai get_world_id_config '()' -n ic --query --identity anonymous -o candidValues as read from mainnet on 2026-09-05, before the switch. None of them is a secret: the relying-party id and the action string travel in every proof request the browser makes, and the key name only selects which IC threshold-ECDSA key signs.
| Field | Value |
|---|---|
rp_id | rp_93dd068fa8d74df7 |
action | ohshii-governance-vote-v1 |
required_identifier | document,secure_document,passport |
rp_key_name | key_1 |
request_preset | unset — resolves to V4Capable |
enabled | true until the switch-off step, then false |
accepts_new_proofs | unset (accepting) until 2026-09-05, then false |
Re-activating the provider takes two approved ONS proposals (during the transition phase, the deploying principals can also call both setters directly), and neither needs a value that is not in this table or in the query above:
set_world_id_accepts_new_proofs(true)— aCriticalGovernanceOperationproposal reopens intake. Reopening is critical: every open provider is another way for one person to enter the electorate again on a second wallet, and no per-provider uniqueness check can see across providers. Closing intake again (false) is Standard, so the defensive direction stays fast.set_world_id_config("rp_93dd068fa8d74df7", "ohshii-governance-vote-v1", "document,secure_document,passport", true, "key_1")— aCriticalGovernanceOperationproposal makes the gate mandatory again. The setter is positional, so all five values are passed again withenabledas the one that changes;request_presetandaccepts_new_proofsare read back, not reset.
Keep the action string when re-activating: changing it changes which nullifiers
count as already spent (see §7).
Four-method gate: combined semantics
Section titled “Four-method gate: combined semantics”World ID is one of four independent verification methods on
ohshii_governance: World ID and DecideID, both
Strong and both legacy; Google, Standard and
live; and Telegram, Strong and
open to new proofs. Each has its own DAO-controlled configuration, its own verified set and its own pair
of flags.
The gate is composed in one place and it now has a strength axis:
- A critical action requires Strong whenever any Strong provider is enabled.
- Any other gated action requires Standard, which every method satisfies.
- If no provider is enabled, nothing is mandatory.
- If only Google is enabled, a critical action needs only Google — there is no Strong provider available to demand, and demanding one would make the critical tier unreachable.
The full table is in Governance → Identity verification.
Persistence on disable: a verification, once granted by any method, is never
cleared by a configuration change. Disabling World ID does not remove anyone from
its verified set, and the same holds for every other provider. The predicate that
answers “is this caller verified?” reads the verified sets directly, never the
enabled flags. A user verified via World ID keeps passing the gate after World
ID is set back to “Not Configured”, and keeps the LGE tier that comes with Strong
strength regardless of which flag flipped.
Persistence on retirement is the same guarantee on the second axis: closing World ID to new proofs takes nothing away from anyone who already holds one.
What happens when EVERY method is disabled: the gate does not fire and ONS
voting becomes open to any authenticated wallet — no verification required, no
per-method check. Anonymous principals are still rejected by the
reject_anonymous guard. All other voting rules (rate limit, voting power,
proposal status) still apply. A DAO that wants to halt voting without lifting the
Sybil filter must leave at least one method enabled = true, and use the
per-provider intake controls to block new verifications instead.
Any provider .enabled | ONS vote() for an unverified caller |
|---|---|
| none | Allowed — gate is off |
| at least one | Rejected unless the caller is in the verified set of any of the four providers (and, on a critical proposal, of a Strong one when a Strong provider is enabled) |
2. End-user flow (the “Required” case)
Section titled “2. End-user flow (the “Required” case)”- User lands on
/verify. Page fetchesget_world_id_config()anonymously and renders the state banner, plus per-credential info for each method inrequired_identifier. - User presses Start World ID verification.
- Frontend calls
request_world_id_rp_signature(action)on canister. The canister signs anrp_contextvia IC threshold ECDSA and returns{ rp_id, nonce_hex, created_at, expires_at, signature_hex }. - Frontend opens the
@worldcoin/idkit@4.xwidget. What it requests follows the governedrequest_preset(see §9): the defaultV4Capableasks for a v4passportcredential (NFC passport/eID — via the low-levelCredentialRequest('passport', …)constraint builder, since the installed IDKit version does not yet ship a namedpassportpreset helper), while the rollbackV3Legacyasks for the legacydocumentLegacypreset. The widget deliberately does not requestproof_of_human(Orb) — Orb devices are not universally accessible, andrequired_identifieris a document-based policy. The widget keeps theallow_legacy_proofsflag on in both cases so 3.0 credential holders still complete. The caller’s principal text is passed assignaland therp_contextfrom step 3 is forwarded verbatim. - User completes verification in their World App with the credential
World holds for them (e.g. an NFC-chipped document/passport, or a 4.0
passportcredential). - IDKit returns a proof to the page. For a 4.0 credential it is a
passport(or, if a DAO proposal ever re-enables it,proof_of_human) proof whoseproofis a 5-element array (plusissuer_schema_idandexpires_at_min, nomerkle_root); for a legacy 3.0 credential it is the older{ identifier, merkle_root, nullifier, proof, signal_hash }shape. The page readsresult.protocol_version("4.0"or"3.0") to tell them apart. - Frontend calls
register_world_id_verificationon canister, passing the common positional fields (merkle_root,nullifier,proof,signal_hash,identifier) plus a trailingopt WorldIdV4Proof:[{…}]carries the 5-element proof bundle for a 4.0 proof, or[]for a legacy 3.0 proof. The presence of that optional argument selects the verify path. The canister:- Validates input size / format.
- Recomputes the expected
signal_hashfromkeccak256(caller.to_text())and rejects any mismatch (binds the proof to the caller). - Rejects if the nullifier was already spent.
- Acquires a per-caller RAII lock to prevent the double-POST race.
- Consumes one slot of the canister-wide daily cap.
- POSTs the proof to
https://developer.world.org/api/v4/verify/{rp_id}in the matching 3.0 or 4.0 wire shape, via a single non-replicated HTTPS outcall. There is noallow_legacy_proofsfield on the verify body — the v4 endpoint accepts the 3.0 shape natively viaprotocol_version;allow_legacy_proofsis a client-side IDKit widget flag only. - On HTTP 200 +
success: true+ matchingidentifier, marks the caller as verified and the nullifier as spent.
- Frontend refreshes
am_i_verified(), closes the modal. User can now vote on ONS proposals.
When the VotePanel detects an ONS proposal and the gate is enabled, it
short-circuits handleVote into the verification modal if the caller is
not yet verified, instead of sending the (guaranteed-to-fail) vote call.
3. Privacy model
Section titled “3. Privacy model”The goal of this integration: verify a unique human without ever learning who that human is.
What the canister stores
Section titled “What the canister stores”| Persistent state | Shape | Contents |
|---|---|---|
VERIFIED_USERS | StableBTreeMap<Vec<u8>, u8>, MemoryId(53) | { principal_bytes → 1 }. The universal “is verified” boolean. |
USED_NULLIFIERS | StableBTreeMap<Vec<u8>, u8>, MemoryId(54) | { nullifier_bytes → 1 }. Blocks reuse so the same World ID cannot verify a second wallet. |
WORLD_ID_CONFIG | StableCell<WorldIdConfig>, MemoryId(55) | { rp_id, action, required_identifier, enabled, rp_key_name }. |
WORLD_ID_RP_PUBKEY_CACHE | StableCell<…>, MemoryId(56) | Cached SEC1-compressed public key derived from threshold ECDSA. |
WORLD_ID_DAILY_COUNTER | StableCell<{day_start_ns, count}>, MemoryId(57) | Rolling 24 h counter. Survives upgrades so bots cannot reset it by forcing a redeploy. |
WORLD_ID_DAILY_CAP | StableCell<u64>, MemoryId(58) | Ceiling on successful verifications per 24 h. Default 500. |
No timestamp is stored next to a principal’s verified = true flag. No
credential type is stored. No linkage between principal and nullifier is
stored in any single record: the two maps are indexed on disjoint keys.
The rate-limit counter (WORLD_ID_OP_RATE_LIMITS) and the in-flight
register lock (WORLD_ID_REGISTER_LOCKS) live in heap memory on purpose —
resetting both on upgrade is desirable (stale lock release, rate-limit
fairness) and the canister-wide daily counter in stable memory continues
to bound the economic exposure.
What World Foundation / Tools for Humanity hold
Section titled “What World Foundation / Tools for Humanity hold”Paraphrased from the official sources listed at the bottom of this doc and
on the /verify page:
- Document scans and biometric data never leave the user’s phone. They are held in the World App’s encrypted “Personal Custody Package”.
- World Foundation and Tools for Humanity state they do not retain images or data related to government IDs.
- The
/verifyendpoint receives the ZK proof (which is by construction zero-knowledge), plus thenullifier,merkle_root,signal_hash,action, andidentifier. It does not receive the user’s name, date of birth, photo, document number, or country.
What the OhShii canister sees
Section titled “What the OhShii canister sees”merkle_root(public commitment to the credential tree at a point in time; present on the legacy 3.0 path only — the 4.0 shape (passportorproof_of_human) has nomerkle_root)nullifier(one-way hash of(app_id, action, human))proof(ZK proof bytes — a single hex blob on 3.0, a 5-element Groth16 array plusissuer_schema_idandexpires_at_minon 4.0)signal_hash(the canister recomputes and compares it againsthash_to_field(caller.to_text()); if they differ, the proof is rejected as a replay)
None of these fields is personally identifying on its own, on either protocol
version. The canister retains verified=true and the nullifier only; it never stores
merkle_root, proof, signal_hash, action, or the credential type
persistently.
Two kinds of logs exist on IC canisters and both are redacted to avoid undoing the privacy properties above:
- Replica logs (
ic_cdk::println!) — the registration log line records the action name, the credential identifier, the configured allow-list, therp_id, and the proof length. It deliberately does not recordcaller,nullifier,merkle_rootorsignal_hash, so anyone reading the replica log cannot reconstruct “who verified when”. - Event log in stable memory (
EVENT_LOG) — successful user verifications write an event with emptydetails, so the public log records that “a verification happened” without exposing who or how. Escape-hatch events (theadmin_*recovery methods) keep their arguments indetailsso those privileged operations are auditable.
Anyone calling get_recent_events can tell that “verifications are
happening at roughly this rate”; they cannot tell which principal
verified at any given time.
4. Sybil resistance: the two bindings
Section titled “4. Sybil resistance: the two bindings”Two checks turn the is_verified flag into a real sybil gate.
- Signal binding (in the canister). The
signalparameter IDKit hashes intosignal_hashis the caller’s principal text. The canister recomputes this hash with the samehash_to_fieldfunction World’s IDKit uses (keccak256(utf8) >> 8, 32-byte padded) and rejects any proof whose signal_hash does not match. A proof captured from principal A cannot be replayed on behalf of principal B because B’s expected signal_hash differs. - Nullifier uniqueness (on canister + at World). World’s verify
endpoint refuses to re-verify a nullifier it already saw. Independently,
the canister tracks
USED_NULLIFIERS— so even if World’s server behaviour ever loosened, one World ID would still map to one IC principal here.
The combined effect: a valid verification requires both (a) a real credential owned by a real human in the World App, and (b) that the credential has never verified another principal on this canister before.
Downstream consumers of is_verified
Section titled “Downstream consumers of is_verified”is_verified is the OR over all four methods (Telegram, World ID, DecideID and
Google) and is consumed beyond ONS
vote gating. In particular, the Sponsor/referral system is verification-gated:
a sponsor↔user referral connection is only persisted on-canister, and a sponsor
only earns, once the invited user is verified. Until then the referrer code stays
pending client-side (no canister storage) and the referral reward stays with
the OhShii treasury. The check is made at purchase time and is not retroactive
to that buyer’s earlier unverified purchases — verifying mid-campaign does not
re-attribute rewards already routed to the treasury. See
SPONSOR_SYSTEM.md → Referral registration & verification gating.
5. RP signing via IC threshold ECDSA
Section titled “5. RP signing via IC threshold ECDSA”@worldcoin/idkit@4.x requires every proof request to carry an rp_context
signed with the relying party’s key. The signature algorithm is fixed:
| Property | Value |
|---|---|
| Curve | secp256k1 (same as Bitcoin and Ethereum) |
| Scheme | Recoverable ECDSA — 65 bytes: r || s || v |
| Message encoding | EIP-191 ("\x19Ethereum Signed Message:\n" + len + payload) |
| Hash | keccak256 |
Ed25519, BLS, Schnorr, HMAC-SHA256 and other algorithms would all have
required a different approach (a different chain-key primitive, or
custody of a shared secret). Because the scheme is standard Ethereum
personal_sign, IC’s native sign_with_ecdsa primitive matches exactly
and the canister acts as a legitimate relying party without ever touching
secret material.
IC threshold ECDSA in brief
Section titled “IC threshold ECDSA in brief”- The master private key is split across the nodes of a subnet in a
threshold-of-
nscheme. No single node holds it; no canister holds it. - The canister derives its per-purpose key with the
derivation_pathb"ohshii-governance/world-id/rp-signing/v1". - To sign, the canister calls
sign_with_ecdsa(message_hash, derivation_path, key_id). The subnet nodes run the threshold protocol and return a signature bit-for-bit identical to what an off-chain signer with the raw private key would produce. - The private key is not backed up, not exported and not cached anywhere. A malicious upgrade cannot extract it because the canister has never seen it.
The corresponding public key / Ethereum address is fetched once via
ecdsa_public_key (a free query) and cached in
WORLD_ID_RP_PUBKEY_CACHE in stable memory. During bring-up it is read
back with the get_world_id_rp_public_key method and the Ethereum address
is pasted into the signing-key field of the World Developer Portal.
EIP-191 in the canister
Section titled “EIP-191 in the canister”payload = version(1) || nonce(32) || created_at_u64_be(8) || expires_at_u64_be(8) || hashed_action(32)?envelope = "\x19Ethereum Signed Message:\n" + ascii(len(payload)) + payloadmsg_hash = keccak256(envelope)signature = ecdsa_sign(msg_hash) // 65 bytes recoverableThree subtle rules the implementation enforces:
hash_to_field(shared for bothsignal_hashandaction_hash):keccak256(input)shifted right 8 bits, left-padded to 32 bytes. This matches@worldcoin/idkit-core/hashing#hashToFieldexactly; any other derivation produces signatures World cannot reconstruct.- Low-S normalisation (EIP-2): if
sign_with_ecdsaever returns a high-S signature, the canister normalises tos' = n - sbefore emitting. EIP-2-strict verifiers — which include World’s — reject high-S outputs even if mathematically valid. vrecovery.sign_with_ecdsaonly returns(r, s). The canister tries both recovery candidates (0 and 1), recovers to the public key withk256::VerifyingKey::recover_from_prehash, and keeps the one that matches the cached public key; exactly one will match.
| Key | Cycles / signature | USD (approx.) |
|---|---|---|
key_1 (production) | ~26 B | ~$0.003 |
test_key_1 (dev + mainnet testing) | ~10 B | ~$0.001 |
Setting rp_key_name to a different value invalidates the pubkey cache;
the new Ethereum address must be re-registered in the Portal before
signatures start working again.
Why the canister signs unconditionally (even in Managed mode)
Section titled “Why the canister signs unconditionally (even in Managed mode)”The canister invokes sign_with_ecdsa on every
request_world_id_rp_signature call, regardless of whether the World
Developer Portal is in Managed or Self-Managed mode. The
production app currently runs in Managed mode; the transition to
Self-Managed is deliberately deferred to World ID GA (see the
deferral note below). There is no config flag to skip the signing
when the Portal is Managed. The reasoning:
- The Portal in Managed mode neither validates nor rejects the canister’s
signature cryptographically. The canister-derived public key is
not registered anywhere — Managed mode means the Portal holds its
own key. Empirically (verified end-to-end on mainnet), flows with
the canister-signed
rp_contextsucceed in Managed mode; this is consistent with the widget either discarding the canister’s signature after a structural check or replacing it with a Portal-provided one before sending to the bridge. The two behaviours cannot be told apart from the outside because the relevant logic is inside a WASM binary. - Cost is bounded. Each
sign_with_ecdsacall onkey_1is26 B cycles ($0.003). The daily cap onregister_world_id_verificationis 500 by default; signing requests are rate-limited to 5/hour/principal and short-circuit for already- verified principals. Worst-case spend from this wasted signing in Managed mode is ~$0.17/day — small enough that the simplicity argument wins. - The code path is identical for Self-Managed. Moving to
Self-Managed is a Portal-side operation (registration on World Chain
via the
RpRegistrycontract). The canister produces exactly the same signature in both modes; what changes is whether World’s bridge validates it against a registered public key. Keeping the signing always-on means the transition to Self-Managed requires zero canister code changes. - Forward compatibility. The threshold-ECDSA signing stack
(EIP-191 wrap,
hash_to_field, low-S normalisation,vrecovery) is the same infrastructure needed for any future feature that signs Ethereum-style messages (e.g. treasury flows on EVM chains). Adding a “skip in Managed” toggle would introduce a conditional that could diverge over time and would need to be removed before Self-Managed.
Self-Managed is deferred to GA (deliberate)
Section titled “Self-Managed is deferred to GA (deliberate)”Today the integration runs in Managed mode. Switching to Self-Managed means registering the canister’s threshold-ECDSA Ethereum address with World as the relying-party signing key, so World validates the signatures the canister already produces (the integration code’s path for this is World’s on-chain RP registry). That registration is deliberately deferred to World ID GA rather than done now, to avoid a double registration: if World rotates or re-keys the relying-party registry at GA — a real possibility while the 4.0 protocol is still stabilising — an address registered today would have to be torn down and re-registered, opening a window where the registered key and the canister’s live key disagree and signatures fail closed.
The canister code path is identical either way — it always signs — so making the switch at GA is a registry/Portal-side operation with zero canister code changes. Until then, Managed mode is the supported posture and all seven security properties in §10 hold.
Forward use
Section titled “Forward use”The same infrastructure signs any EIP-191 message under any
derivation_path. Future features that need Ethereum-style signatures
(e.g. a treasury flow interacting with an EVM DEX) can reuse it without
introducing new secrets.
5.5 Veto-override re-attestation
Section titled “5.5 Veto-override re-attestation”Telegram is the anchor. The override round accepts a Telegram attestation. The World ID path described below is accepted only while this provider’s
accepts_new_proofsis true; with intake closed, Telegram is the sole anchor and the World ID path documents the legacy shape. See Telegram Verification §6 for the other shape and what differs on it — round-to-round unlinkability, and no outbound call in the attestation path.
When a Guardian vetoes a proposal, an overridable veto opens a second
override voting round. Voting in that round requires a fresh personhood
attestation (Telegram, or World ID while it accepts new proofs) — completed onboarding (the standing is_verified flag) is
not enough. This is by design: a fresh proof-of-personhood per override
round is the whole point of the round.
Key properties (all enforced on canister):
- Fresh proof per round. Each override round has its own World action,
ohshii-ovr:{dao}:{proposal_id}. World binds the proof to that action, and the canister records the proof’s nullifier in a per-override map keyed by(dao, proposal_id, nullifier). A human’s nullifier therefore differs across overrides, and a proof for one override cannot satisfy another. The override path never reads or writes the onboarding sets (VERIFIED_USERS/USED_NULLIFIERS), except a read-onlyis_any_method_verifiedadmission check that rejects callers who have not completed onboarding before any cycle-spending step. - One human, one ballot. The per-override nullifier map enforces that the same World ID re-attests at most once per override round; a second principal presenting the same nullifier for the same override is rejected.
- Caller-driven — ONS trusts no SONS assertion. The override round also
serves SONS-family DAOs, but a human always attests their own personhood
directly on ONS. The endpoint
attest_fresh_override_personhood_for_sonstakes no borrowable “voting principal” argument:msg_caller()IS the subject, and the suppliedsons_dao_idis inert key material — it only builds the World action string, the nullifier key and a DAO-dimensioned marker. It never feeds an eligibility, voting-power, authorization, budget or rate decision. Per-DAO eligibility is checked locally in SONS, never on ONS, so a caller naming an arbitrarysons_dao_idcannot bend an authorization outcome. A SONS DAO later learns the attestation through the read-onlydid_attest_overridequery (a pull, never a trigger). - Same threshold-ECDSA signer, same v3/v4 acceptance.
request_override_rp_signaturemints the RP signature for the per-override action using the same threshold-ECDSA pipeline as onboarding (override voters are already verified, so there is nocfg.actionconstraint here). The override/verifyoutcall accepts both 3.0 and 4.0 proofs exactly like the onboarding path — the same trailingopt WorldIdV4Proofselects the wire shape. - Bounded availability residual (honest caveat). The override flow is
rate-limited (a per-caller, per-proposal cap) and bounded by a canister-wide
outcall budget and the 2 T cycle floor. The worst case under pressure is a
delayed override window, not capture: there is no drain-to-freeze. A
guardian-removal proposal is vetoable-but-overridable, so a contested
removal (one the guardian vetoes) completes through this same override and
shares the bounded delay — but the guardian’s only power is the overridable
veto (it cannot approve, spend, or alter a result), and the removal of a
guardian that does not veto is decided in round 1, off this path; so the
worst case stays a delayed contested override, not capture. The legacy
verify_fresh_override_proof_forendpoint remains callable alongside the caller-driven path. These residuals are bounded, not fully closed.
Why the override needs a per-account anchor (and DecideID cannot supply one)
Section titled “Why the override needs a per-account anchor (and DecideID cannot supply one)”The argument below is about the shape of the anchor, not about World ID as a brand, and it is why the move went to Telegram rather than to Google: a Telegram account is a per-account anchor that survives a change of wallet, and a Google-only verification is Standard and never reaches an override ballot at all. Read “World ID” below as “the provider currently supplying the per-account anchor”.
Normal voter eligibility accepts any of the four methods — Telegram,
World ID and DecideID (Strong) or Google (Standard) — and a Critical proposal
accepts only a strong one (see §1 and
docs/DECIDEID_VERIFICATION.md); the override’s
fresh re-attestation accepts World ID and Telegram, never DecideID. The reason is the threat the override
exists to annul: acquired or farmed verified identities. An attacker need not
verify many humans — they can control a stockpile of already-verified principals
(the verified set is on-chain) and aim that bloc at a Critical proposal. A static
“is this principal verified?” check counts the whole stockpile; only forcing a
fresh, live proof of personhood collapses it onto the real humans behind it.
- World ID’s
nullifieris a per-human anchor. It is deterministic in(app_id, action, the human's World account)and stable across principals — the same human re-attesting from a different wallet yields the same nullifier for a given action. With a fresh per-(DAO, proposal)action the override de-duplicates on that nullifier, giving one human → one override ballot however many verified principals the human holds, and an attacker cannot forge a fresh proof for a human they do not control. - DecideID is principal-bound. Its verification sets a per-principal flag with per-presentation anti-replay; there is no cross-principal human anchor. A human controlling N bought principals has N independent DecideID verifications, so re-verifying with DecideID would just re-confirm each principal — it would not collapse the bloc to one human, i.e. it would empty the weapon. DecideID is a fine eligibility method but cannot power the override.
This raises the cost of a bought-identity take-over and makes it annullable by the community; it does not eliminate a coalition of enough distinct, real humans each casting a fresh proof — it prices that up. Full threat → defense narrative: Governance & Capture Resistance → “Why World ID — and why not DecideID — for the override”.
6. Rate limits and cycle budget
Section titled “6. Rate limits and cycle budget”The /verify outcall costs ~100 B cycles and each threshold-ECDSA signing
costs ~26 B. Bots can mint unlimited IC principals, so two layered
defences keep the worst case bounded:
Per-principal (heap, non-persistent)
Section titled “Per-principal (heap, non-persistent)”- 5 attempts / hour shared between
request_world_id_rp_signatureandregister_world_id_verification. - 30 s cooldown between consecutive attempts from the same principal.
- Already-verified principals are short-circuited out of
request_world_id_rp_signature, so no cycles are spent on them.
Canister-wide (stable, persistent)
Section titled “Canister-wide (stable, persistent)”consume_world_id_daily_cap_slotguards the/verifyoutcall with a rolling-24 h cap (StableCell<u64>, default500). Slot consumption happens only after the cheap quick-fail checks (nullifier already spent, invalid inputs), so replay attempts do not eat the budget.- The cap is configurable via
set_world_id_daily_cap. Passing0pauses the feature instantly:register_world_id_verificationrejects with a clear error, no outcall attempted. - At ~$0.05 per verification, the default budget bounds daily cycle spend from this feature at ~$25.
Race protection
Section titled “Race protection”WorldIdRegisterLockGuard (RAII lock, Drop-released) prevents two
concurrent register_world_id_verification calls from the same principal
from both passing the is_world_id_verified check and both paying the
outcall. The lock releases even if the callback traps — required, because
IC rolls back post-await state on trap but pre-await state persists, and
a manually released lock would stick forever on a trap.
Cycle floor pre-check (2 T)
Section titled “Cycle floor pre-check (2 T)”Both request_world_id_rp_signature and register_world_id_verification
abort immediately if
ic_cdk::api::canister_cycle_balance() < MIN_CYCLES_FOR_WORLD_ID
(memory::MIN_CYCLES_FOR_WORLD_ID = 2_000_000_000_000). Rationale:
- Each end-to-end flow spends roughly
26 B(threshold ECDSA) +100 B(HTTPS outcall) =126 Bcycles. A 2 T floor leaves ~16× headroom plus reserve for the freezing-threshold margin (~30 days of idle burn). - The pre-check fires before the rate-limit bump, the daily-cap slot consumption, and the per-caller register lock — a cycle-starved call wastes none of those resources.
- On failure, the canister:
- returns
Err("InsufficientCycles: …")to the caller; - emits the standard
ic_cdk::println!line (same style as theupload.rspre-payment cycle check it mirrors); - writes a
world_id_cycles_insufficiententry toEVENT_LOGin stable memory (detailsfield carries<context> current=<n> min=<n>) so the failure is visible to anyone throughget_recent_events.
- returns
This floor is separate from the canister’s general MIN_REQUIRED_CYCLES
(1 T). A world_id_cycles_insufficient SystemLifecycle event is the public
signal that the canister needs a cycles top-up.
7. Governance control
Section titled “7. Governance control”All World ID configuration lives on canister and is changed by ONS proposals — and, during the transition phase, also by the deploying principals. The Create Proposal form exposes two entries under “Governance Configuration”:
| Method | Args | Purpose |
|---|---|---|
set_world_id_config | rp_id, action, required_identifier, enabled, rp_key_name | Full config update (preserves request_preset, which has its own setter). required_identifier is comma-separated: a single proposal can allow multiple credential types (e.g. "document,secure_document"). Bumping action burns every existing nullifier and is a soft “reset everyone” lever; use the version suffix deliberately (-vote-v1, -vote-v2). |
set_world_id_request_preset | preset: variant { V4Capable; V3Legacy } | Toggles ONLY what the in-browser widget requests: V4Capable (the safe default) requests a v4-capable passport credential (NFC passport/eID); V3Legacy is an emergency narrowing rollback to the legacy documentLegacy request. Never changes what the canister accepts — the v4 /verify endpoint accepts legacy 3.0 proofs natively, so v3 holders keep verifying in every preset state. Set by a standard ExecuteAdminMethod proposal, and during the transition phase also by the deploying principals (guarded by is_admin_or_self_guard). The Create Proposal form has an entry for it. |
set_world_id_daily_cap | cap: nat64 | Canister-wide ceiling on verifications per 24 h. 0 pauses without touching anything else. |
These methods are guarded by is_admin_or_self_guard, so they are callable by
governance itself via an ExecuteAdminMethod proposal (the canister executes the
call on itself via call_raw(canister_self(), ...)), and, during the transition
phase, by the deploying principals — which are removed at the DAO handover, after
which the proposal is the only route.
Read-only queries for observability:
| Method | Returns |
|---|---|
get_world_id_config | The current WorldIdConfig. |
get_world_id_daily_cap | The u64 ceiling. |
get_world_id_daily_counter | (day_start_ns, count_in_current_window). |
is_verified(principal) | Universal boolean flag. O(log n) scan on stable memory, usable by any caller. |
am_i_verified | Shortcut for is_verified(msg_caller()). |
The get_world_id_rp_public_key method is gated by is_admin_or_self_guard (it makes
an inter-canister call to the management canister). It returns the Ethereum address
that World’s relying-party registry needs after an rp_key_name change or a switch
to Self-Managed mode.
8. Escape hatches
Section titled “8. Escape hatches”Guarded by is_admin_or_self_guard (executed by ohshii_governance through an
approved proposal and, during the transition phase, also callable by the deploying
principals) and written to EVENT_LOG with full details for auditability:
| Method | Effect |
|---|---|
admin_reset_world_id_rate_limit(opt principal) | Clears the heap rate-limit counter for one principal, or for everyone if null. Heap map is already upgrade-cleared; this is the no-upgrade equivalent. |
admin_clear_world_id_verified(principal) | Removes the universal verified flag for a principal. Does not free the nullifier. |
admin_clear_world_id_nullifier(text) | Frees a spent nullifier so it can verify again. Pairs with the one above. |
Because these are privileged recovery calls, the EVENT_LOG details field records
the target principal or nullifier verbatim — that is the audit trail.
9. Frontend integration
Section titled “9. Frontend integration”IDKit version and the governed request preset
Section titled “IDKit version and the governed request preset”- Package:
@worldcoin/idkit@^4.1.1(only stable 4.x line; 1.x/2.x are deprecated by World). This installed version does not yet ship named preset helpers for the v4 credential family (passport/proofOfHumanetc, as documented ondocs.world.org) — only the legacy presets (documentLegacy/orbLegacy/…) and the low-levelCredentialRequest(type, opts)constraint builder are exported. The v4 request below therefore goes throughCredentialRequest, not a named preset. - The widget requires
rp_contexton every proof request — for both v4passportconstraints and legacy presets. The canister provides it; the frontend does not. - Request preset (governed). What the widget requests is driven by
WorldIdConfig.request_preset, decoded bydecodeWorldIdRequestModeinconfig/worldid.js:V4Capable(the safe default — also the value used when the config is absent or unreadable) → the widget sends a v4passport(NFC passport/eID) constraints request viaCredentialRequest('passport', { signal }). Combined with theallow_legacy_proofs={true}widget flag, this single request satisfies BOTH new 4.0-only World App users AND existing 3.0 document-credential holders — it locks no one out. Orb/proof_of_humanis deliberately not requested (Orb devices are not universally accessible;required_identifieris a document-based policy).V3Legacy→ the widget falls back to the legacydocumentLegacy({ signal })request. This is a deliberate emergency narrowing / rollback state, NOT a steady state while 4.0-only users exist: it would lock out new World App users who can only produce a 4.0 proof.
- The preset only changes what the widget REQUESTS, never what the canister
ACCEPTS. The v4
/verifyendpoint accepts legacy 3.0 proofs natively (the canister sends the matching 3.0 wire shape), so v3 holders keep verifying in every preset state.request_presetis read only by the frontend and by its dedicated setter — it is never consulted on the canister’s verify/accept path.allow_legacy_proofsis likewise a client-side IDKit widget flag, not a field of the verify request body. - The accepted credential allow-list is the separate
WorldIdConfig.required_identifier(production:document,secure_document,passportas of 2026-07-27). Switch it viaset_world_id_configif governance moves toorb/proof_of_human,face, etc. The credential the widget requests must appear inrequired_identifier(and in the canister’sWORLD_KNOWN_IDENTIFIERSallow-list), otherwiseregister_world_id_verificationrefuses the proof before the outcall.
Content-Security-Policy
Section titled “Content-Security-Policy”@worldcoin/idkit instantiates a WebAssembly module and opens a WebSocket
session against the World bridge. The asset canister’s
.ic-assets.json5 CSP includes:
script-src 'self' 'wasm-unsafe-eval' https://challenges.cloudflare.com—'wasm-unsafe-eval'only allowsWebAssembly.compile()/WebAssembly.instantiate(). It does not allow JavaScripteval(), so the XSS surface is unchanged. Without it the widget cannot bootstrap and fails with aCompileErrorbefore ever reaching the bridge.connect-src— addshttps://bridge.worldcoin.org,https://bridge.world.org,https://developer.worldcoin.org,https://developer.world.org, and thewss://variants.frame-src— addshttps://worldcoin.org,https://world.org,https://id.worldcoin.org.
Three-state UX on /verify
Section titled “Three-state UX on /verify”The page fetches get_world_id_config() anonymously on mount (no auth
required) and renders different banners + CTA labels per state:
| State | Banner | CTA |
|---|---|---|
not_configured | Grey, “Verification is not available yet.” | Disabled, “Not available” |
optional | Amber, “Verification is optional right now.” Mentions LGE tier benefits. | Enabled, “Verify anyway (optional)“ |
required | Cyan, “Verification required to vote on ONS proposals.” | Enabled, “Start World ID verification” |
The same signal flows into the vote flow: VotePanel.handleVote only
short-circuits into the verification modal when the proposal is on ONS
and gateEnabled and the caller is not yet verified.
10. Security properties
Section titled “10. Security properties”These hold regardless of whether enabled is true or false:
- No private key custody. The canister never sees the signing key. A malicious upgrade cannot exfiltrate it. Dumping canister memory yields no secret material relevant to RP signing.
- No PII. The canister receives no personally identifying
information, not even through the
/verifyresponse body; response headers are stripped by the transform function for consensus determinism, and no PII fields exist in the body. - No principal ↔ nullifier linkage in persistent state.
VERIFIED_USERSkeys on principal;USED_NULLIFIERSkeys on nullifier; no third map joins them, no log line writes both together. - Proof replay is closed. The
signal_hashbinding check on canister ensures a proof for principal A cannot be accepted on behalf of principal B, even if/verifyitself accepted the proof cryptographically. - Bot cycle drain is bounded. Per-principal rate limit × daily cap caps the worst-case daily spend at ~$25 / day regardless of how many principals the attacker controls.
- Governance can pause instantly.
set_world_id_daily_cap(0)halts new verifications immediately.set_world_id_config(..., enabled=false, ...)removes the vote requirement without touching anything else. - Replica logs do not enumerate verified users. No combination of replica log + stable event log reveals “principal X verified at time T”.
Nothing above depends on Managed vs Self-Managed mode in the World Developer Portal. Managed mode means World holds/validates the signing key; Self-Managed lets the canister’s own threshold-ECDSA public key be the registered relying-party key. The app runs in Managed mode today; the move to Self-Managed is the planned end-state but is deferred to World ID GA (see §5). It removes the residual dependency on Portal key custody, but it does not affect any of the 7 properties above.
Why Self-Managed strengthens (without adding) the properties
Section titled “Why Self-Managed strengthens (without adding) the properties”In Managed mode the World Developer Portal generates and holds the RP signing key server-side. A compromise of Portal infrastructure (insider, breach, key leak) would let an attacker craft RP signatures for the OhShii app in its name. The proof itself would still need to come from a real World App on a real device — so the worst case is “attacker can trigger verification flows pretending to be the OhShii app”, not “attacker can mint verifications” — but the trust footprint includes Worldcoin’s key custody.
In Self-Managed mode the only key that can sign valid RP requests is the canister’s threshold-ECDSA key, held distributively across an IC subnet. No Worldcoin server, no OhShii principal, and no single IC node ever sees the full key. A compromise of either Worldcoin or the OhShii deploying identities cannot forge RP signatures for this app. Property 1 (“no private key custody”) becomes literally true on both ends of the connection — not just on the OhShii canister.
The on-canister code path is identical in both modes: the canister
signs every request_world_id_rp_signature via sign_with_ecdsa. In
Managed mode the bridge ignores that signature and uses the World-side
key; in Self-Managed mode the bridge validates that signature against
the canister’s registered Ethereum address. Switching modes is a
registry/Portal-side operation, not a canister upgrade, and is
deferred to GA (see §5); §11 lists
what the integration depends on.
11. Integration reference (legacy)
Section titled “11. Integration reference (legacy)”Legacy. The provider is retired: intake has been closed (
accepts_new_proofs = false) since 2026-09-05, which takes nothing away from anyone already verified. This section records what the integration depends on, for anyone reproducing or auditing it.
World Developer Portal. The integration is a World Developer Portal app. The
canister uses its rp_id (rp_…); the frontend widget uses the legacy-format
app_id (app_…), which lives in
src/frontend/src/config/worldid.js. The
Portal registers the action the canister is configured with
(ohshii-governance-vote-v1) and enables the credentials the canister accepts.
Managed mode. The Portal runs in Managed mode. Switching to Self-Managed
(deferred to GA, see §5) means registering
the Ethereum address returned by get_world_id_rp_public_key as the relying-party
signing key with World. The canister signs rp_context the same way in both
modes, so the switch needs no canister change.
Canister configuration. The values live on-chain and are set through
set_world_id_config by an ONS proposal (see §1 for
the current record and §7 for the setters). The
“Optional” state (enabled = false) lets wallets verify without blocking voting.
Public signals anyone can watch.
get_world_id_daily_counter()againstget_world_id_daily_cap()shows how close the daily ceiling is; the cap is raised or lowered withset_world_id_daily_cap.- A
world_id_cycles_insufficientevent inget_recent_eventsmeans the canister is below the 2 T floor and the World ID entry points are refusing until it is topped up. - Changing
rp_key_nameinvalidates the public-key cache and requires re-registering the new Ethereum address with World.
12. Implementation map
Section titled “12. Implementation map”| Concern | Path |
|---|---|
Signing pipeline (EIP-191 wrap, hash_to_field, low-S normalisation, v recovery) | src/ohshii_governance/src/worldid_signing.rs |
| All stable storage (config, verified set, nullifiers, daily cap/counter, pubkey cache) | src/ohshii_governance/src/memory.rs |
| Verification flow, escape hatches, RAII lock, vote gate | src/ohshii_governance/src/lib.rs — register_world_id_verification, vote, admin_* |
| Governed v3/v4 request-preset toggle | src/ohshii_governance/src/types.rs (WorldIdRequestPreset, WorldIdConfig::request_preset + getter), lib.rs set_world_id_request_preset, src/frontend/src/config/worldid.js decodeWorldIdRequestMode |
| Veto-override re-attestation (fresh personhood per round, caller-driven SONS path) | src/ohshii_governance/src/lib.rs — request_override_rp_signature, attest_fresh_override_personhood, attest_fresh_override_personhood_for_sons, verify_fresh_override_proof_for, did_attest_override, has_attested_fresh_override; per-override nullifier map in memory.rs |
| External HTTPS + request/response types (v3 + v4 verify bodies) | src/ohshii_governance/src/external_types.rs pub mod worldid |
| Candid interface | src/ohshii_governance/ohshii_governance.did |
| Frontend modal | src/frontend/src/components/Governance/WorldIDVerificationModal.jsx |
| Verify page | src/frontend/src/pages/VerifyPage.jsx |
| Vote-side gate | src/frontend/src/components/Governance/VotePanel.jsx |
| Frontend config (app_id, action, environment) | src/frontend/src/config/worldid.js |
| CSP headers | src/frontend/.ic-assets.json5 |
| ONS proposal entries (governance controls) | src/frontend/src/components/Governance/CreateProposalForm.jsx |
References
Section titled “References”- World Help Center · What are World ID Credentials and how do I use them?
- World Foundation · Privacy FAQs
- World ID technical docs · Core concepts
- World ID technical docs · 3.0 → 4.0 migration guide
- World ID API reference · Developer Portal
verifyendpoint - World ID technical docs · IDKit integration guide
- World ID technical docs · RP signatures specification
- World ID technical docs · Legacy presets
The privacy and data-handling claims in this document are paraphrased from the sources above as of the date this document was last updated. World Foundation and Tools for Humanity may revise their policies, product behaviour or documentation at any time — consult the current official sources before drawing hard conclusions.
See also
Section titled “See also”- ARCHITECTURE.md — Canister map and governance scope table.
- GOVERNANCE.md — ONS vs SONS, proposal categories, snapshot/restore.
- WORKFLOWS.md — Create LGE, token, vote, create/execute proposal, backend vs frontend upgrade, self-snapshot flows.
- LOGGING_SYSTEM.md — Three-tier logging architecture, stable memory log details, frontend monitoring.
- FRONTEND.md — Frontend architecture: React + JSX + JSDoc
@ts-checktype-checking, the canister-types.js + utils/canister.js bridges, Candid wire conventions, auth providers, and the TypeScript version policy.