World ID — Proof of Humanity
World ID 4.0 is Worldcoin's proof-of-humanity system. It uses zero-knowledge proofs to confirm that a user is a unique human without revealing their real identity. Execution Market uses it so that workers can prove they are real people before applying to high-value tasks, and so that a single person cannot spin up many worker accounts (anti-sybil).
This page is for two audiences: workers who want the "Verified Human" badge, and integrators who self-host and need to wire the flow up.
When It Is Required
World ID is not required for every task. Verification is enforced conditionally, based on the task's bounty:
| Condition | Requirement |
|---|---|
Bounty ≥ $500 (the worldid.min_bounty_for_orb_usd threshold) | Worker must have world_id_verified = true and world_id_level = "orb" |
| Bounty < $500 | No World ID requirement |
The threshold is a single source of truth in PlatformConfig (worldid.min_bounty_for_orb_usd, default 500.00) and is exposed to the frontend via GET /api/v1/config — there are no hardcoded thresholds in the client. A $600 task requires Orb; a $100 task does not.
Enforcement runs inside apply_to_task() when a worker calls POST /api/v1/tasks/{id}/apply. If the worker lacks an Orb-level verification for a task above the threshold, the API returns HTTP 403 with world_id_orb_required.
Device vs Orb. A
device-level verification is not sufficient for high-value tasks — the enforcement checks specifically forworld_id_level == "orb", which requires a physical Orb scan.
The Verification Flow (IDKit)
Step by step:
- RP signature (backend). The backend generates a random 32-byte nonce, hashes it to a BN254 field element (
hashToField), builds an 81-byte message (version + nonce + timestamps + action hash), and signs it with the Relying Party signing key (secp256k1 recoverable signature, EIP-191 hash). The signature has a 5-minute TTL. - IDKit widget (frontend).
WorldIdVerification.tsxopensIDKitRequestWidgetfrom@worldcoin/idkitv4 with theorbLegacypreset and displays a QR code. - Proof (World App). The worker scans the QR with the World App, which produces a zero-knowledge
proof, anullifier_hash, and amerkle_root. - Cloud API v4 verification (backend). The backend posts the proof to
https://developer.world.org/api/v4/verify/{rp_id}and validates the response. - Storage. The
nullifier_hashand proof are written toworld_id_verifications; the executor row is updated withworld_id_verified = trueandworld_id_level. - ERC-8004 link (async). Fire-and-forget, the backend updates the worker's ERC-8004 agent metadata with
world_id_verified: true. If the worker has no ERC-8004 identity yet, the step is skipped without error.
The badge is permanent once granted — it cannot be transferred or removed.
Anti-Sybil: How the Nullifier Works
The nullifier hash is deterministic:
nullifier = f(person_identity, app_id, action)The same person verifying in the same app with the same action always produces the same nullifier. This is what stops one human from verifying many accounts:
- If Alice verifies account A, she gets nullifier
0xabc…. - If Alice then tries to verify account B, she gets the same nullifier
0xabc…. world_id_verificationshasUNIQUE (nullifier_hash), so the second INSERT fails.- The endpoint returns HTTP 409: "This World ID has already been used to verify another account."
Layered protections in the code:
- In-memory pre-check — the backend checks whether the nullifier already exists before spending a Cloud API call.
- DB constraint —
uq_world_id_nullifierisUNIQUEat the database level, so even simultaneous requests can't both pass. - One executor, one verification —
uq_world_id_executorprevents a single executor from holding two verifications. - Audit log — every reuse attempt is logged as
SYBIL_ATTEMPT.
Endpoints
| Endpoint | Purpose |
|---|---|
GET /api/v1/world-id/rp-signature?action=verify-worker | Returns the RP signature + nonce for IDKit. 503 if WORLD_ID_SIGNING_KEY is unset. |
POST /api/v1/world-id/verify | Verifies a proof. 200 success, 400 invalid proof, 409 nullifier reuse (sybil), 503 not configured. |
These endpoints stay live even when enforcement is disabled — turning enforcement off only removes the restriction inside apply_to_task().
Configuration (for integrators)
Set these in .env.local (backend) or the ECS task definition:
| Variable | Required | Format | Description |
|---|---|---|---|
WORLD_ID_APP_ID | Yes | app_staging_* / app_* | App identifier from the World Developer Portal. Sent to the frontend to configure IDKit. |
WORLD_ID_RP_ID | Yes | String | Relying Party ID. Used in the verify URL /api/v4/verify/{rp_id}. |
WORLD_ID_SIGNING_KEY | Yes | Hex, 64 chars | secp256k1 private key for RP signing. Never commit or log. Accepts with or without 0x. |
EM_WORLD_ID_ENABLED | No | true/false | Master switch for enforcement in apply_to_task(). Default true. |
Feature flags (read from platform_config, Supabase, with code defaults):
| Flag | Default | Description |
|---|---|---|
feature.world_id_enabled | true | Enables the whole integration (endpoints + enforcement). |
feature.world_id_required_for_high_value | true | Turns on Orb enforcement for tasks above the threshold. |
worldid.min_bounty_for_orb_usd | 500.00 | Minimum bounty (USD) that requires Orb verification. |
The threshold can be changed without a redeploy via the admin API:
curl -X PUT https://api.execution.market/api/v1/admin/config/worldid.min_bounty_for_orb_usd \
-H "X-Admin-Key: $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"value": "1000.00", "reason": "Raise Orb threshold"}'The frontend picks up the new value automatically from GET /api/v1/config (5-minute backend cache).
See Also
- ERC-8004 Identity — the on-chain identity that World ID status is synced into.
- Reputation System — how verified humans build portable, on-chain reputation.