Skip to content

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:

ConditionRequirement
Bounty ≥ $500 (the worldid.min_bounty_for_orb_usd threshold)Worker must have world_id_verified = true and world_id_level = "orb"
Bounty < $500No 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 for world_id_level == "orb", which requires a physical Orb scan.

The Verification Flow (IDKit)

Step by step:

  1. 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.
  2. IDKit widget (frontend). WorldIdVerification.tsx opens IDKitRequestWidget from @worldcoin/idkit v4 with the orbLegacy preset and displays a QR code.
  3. Proof (World App). The worker scans the QR with the World App, which produces a zero-knowledge proof, a nullifier_hash, and a merkle_root.
  4. Cloud API v4 verification (backend). The backend posts the proof to https://developer.world.org/api/v4/verify/{rp_id} and validates the response.
  5. Storage. The nullifier_hash and proof are written to world_id_verifications; the executor row is updated with world_id_verified = true and world_id_level.
  6. 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_verifications has UNIQUE (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:

  1. In-memory pre-check — the backend checks whether the nullifier already exists before spending a Cloud API call.
  2. DB constraintuq_world_id_nullifier is UNIQUE at the database level, so even simultaneous requests can't both pass.
  3. One executor, one verificationuq_world_id_executor prevents a single executor from holding two verifications.
  4. Audit log — every reuse attempt is logged as SYBIL_ATTEMPT.

Endpoints

EndpointPurpose
GET /api/v1/world-id/rp-signature?action=verify-workerReturns the RP signature + nonce for IDKit. 503 if WORLD_ID_SIGNING_KEY is unset.
POST /api/v1/world-id/verifyVerifies 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:

VariableRequiredFormatDescription
WORLD_ID_APP_IDYesapp_staging_* / app_*App identifier from the World Developer Portal. Sent to the frontend to configure IDKit.
WORLD_ID_RP_IDYesStringRelying Party ID. Used in the verify URL /api/v4/verify/{rp_id}.
WORLD_ID_SIGNING_KEYYesHex, 64 charssecp256k1 private key for RP signing. Never commit or log. Accepts with or without 0x.
EM_WORLD_ID_ENABLEDNotrue/falseMaster switch for enforcement in apply_to_task(). Default true.

Feature flags (read from platform_config, Supabase, with code defaults):

FlagDefaultDescription
feature.world_id_enabledtrueEnables the whole integration (endpoints + enforcement).
feature.world_id_required_for_high_valuetrueTurns on Orb enforcement for tasks above the threshold.
worldid.min_bounty_for_orb_usd500.00Minimum bounty (USD) that requires Orb verification.

The threshold can be changed without a redeploy via the admin API:

bash
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