World ID proofs can be fully verified on-chain. After all, the source of truth for the decentralized protocol is on-chain. To verify a World ID proof, your smart contract will embed a call to the verifyProof method of the World ID contract, and then execute the rest of its logic as usual.

We provide a Foundry starter kit and a Hardhat starter kit to help you get started with World ID. We strongly recommend using one of these starter kits to get started with World ID.

Note that calling the verifyProof function by itself does not provide sybil-resistance, or prevent proof reuse, it just verifies that the proof is valid. To prevent sybil attacks, see the sybil-resistance section below.

Supported Chains

ChainProduction AddressStaging Address
Polygon logoPolygonsemaphore.wld.ethstaging.semaphore.wld.eth
Ethereum logo Ethereum MainnetComing soonComing soon
Optimism logo OptimismComing soonComing soon

Calling verifyProof

The verifyProof function is meant to be called by your smart contract. If you just want to verify a proof from your backend, you should use the /verify method of the API instead.

The verifyProof method takes the following arguments:

  • root - The World ID root to verify against. This is obtained from the IDKit widget, and should just be passed as-is.
  • groupId - This should always be 1 for World ID.
  • signal - The signal to verify. See the signal section below.
  • nullifierHash - Anonymous user ID. This is obtained from the IDKit widget, and should just be passed as-is.
  • action - The action to verify. See the action section below.
  • proof - The proof to verify. This is obtained from the IDKit widget, and should be unpacked into a uint256[8] before being passed to the method.

The proof argument is returned from IDKit as a string, but depending how you're calling your smart contract (when using ethers.js or wagmi, for example), you might be required to unpack it into a uint256[8] before passing it to the verifyProof method. To unpack it, use the following code:

import { defaultAbiCoder as abi } from '@ethers/utils'

const unpackedProof = abi.decode(['uint256[8]'], proof)[0]

The verifyProof method reverts if the proof is invalid, meaning you can just call it as part of your smart contract's logic and execute the rest of your logic after as usual.

Custom signals

Signals can be used to validate that a transaction has not been tampered with. By including other parameters your smart contract expects in the signal, you can ensure that the proof verification is only successful if those other parameters haven't been tampered with.

For example, a smart contract performing an airdrop might include the receiver address in the signal, while a contract allowing users to vote on a governance proposal might include the vote.

To get started, you'll need to pass the signal to the IDKit widget. You can do this with the signal prop:

import { solidityEncode } from '@worldcoin/idkit'

return (
		// ...
		signal={solidityEncode(['address'], [receiverAddress])}
		{/* ... */}

Then, in your smart contract, you abi.encodePacked the signal and call hashToField on it.

    // ...
    // ...

Custom actions

Actions are key to uniqueness on the World ID protocol. The same action will provide the same nullifierHash for the same user. By default, your action will be your app id, abi.encodePacked and hashed to field.

uint256 action = abi.encodePacked(appId).hashToField();

If you want to use a custom action, you can pass it to the IDKit widget with the action prop:

import { solidityEncode } from '@worldcoin/idkit'

return (
	<IDKitWidget app_id={appId} action={solidityEncode(['uint256'], [proposalId])}>
		{/* ... */}

Then, in your smart contract, you abi.encodePacked the action and call hashToField on it.

// we recommend memoizing the appId part on the constructor to save gas
uint256 action = abi.encodePacked(abi.encodePacked(appId).hashToField(), proposalId).hashToField();

To put together the two examples below, an application that lets users vote on governance proposals anonymously (but only lets them vote once) would add the proposal id to the action and the contents of the vote to the signal.


While the World ID protocol makes it very easy to make your contracts sybil-resistant, this takes a little more than just calling the verifyProof function. To make your contract sybil-resistant, you'll need to do the following:

  • Store the nullifierHash of each user that has successfully verified a proof.
  • When a user attempts to verify a proof, check that the nullifierHash is not already in the list of used nullifierHashes.

Here's an example function doing the above. You can also use the World ID starter kits to get started with sybil-resistance.

/// @param signal An arbitrary input from the user, usually the user's wallet address
/// @param root The root (returned by the IDKit widget).
/// @param nullifierHash The nullifier hash for this proof, preventing double signaling (returned by the IDKit widget).
/// @param proof The zero-knowledge proof that demonstrates the claimer is registered with World ID (returned by the IDKit widget).
function verifyAndExecute(
    address signal,
    uint256 root,
    uint256 nullifierHash,
    uint256[8] calldata proof
) public {
    // First, we make sure this person hasn't done this before
    if (nullifierHashes[nullifierHash]) revert InvalidNullifier();

    // We now verify the provided proof is valid and the user is verified by World ID

    // We now record the user has done this, so they can't do it again (proof of uniqueness)
    nullifierHashes[nullifierHash] = true;

    // Finally, execute your logic here, for example issue a token, NFT, etc...

Network support

The World ID protocol currently lives in the Polygon network, and the verifier contract is deployed at semaphore.wld.eth on Polygon mainnet and staging.semaphore.wld.eth on Polygon Mumbai testnet.

We will very soon be launching the World ID protocol on Ethereum mainnet and Optimism, and will update this documentation accordingly.

Smart Contract Verifications

As an alternative to verifying proofs with the Developer Portal, proofs can be verified by directly calling the smart contract on-chain. This is a tradeoff that applications may want to make to not rely on a centralized API. Keep in mind that the current batch of registered users can take anywhere from 5-30 minutes to be posted on-chain, so there is a delay associated with these smart contract based calls.

The World ID Semaphore contracts are deployed to Polygon Mainnet and the Mumbai testnet. The contract addresses are:



Verify Proof


Verifies a proof against the contract on-chain. Here we are using the Alchemy API to make the call, but you can use any Ethereum provider (Infura, Quicknode, etc.)

The most recent batch of users will not be available on-chain until the next batch is published. This can take anywhere from 5-30 minutes.

Required attributes

  • Name

    The contract address to check the proof against.

  • Name

    The payload data to send to the contract, encoded to the contract's ABI using ethers encodeFunctionData method. More details can be found here.

Possible Responses

  • 200 OK - The proof is valid and the credential is verified.
  • 200 Invalid Merkle Root - The provided merkle root is invalid. Please check your parameters.
  • 200 Invalid Proof - The provided proof is invalid. Please check your parameters and encoding method.


curl -X POST "/v2/{alchemy_api_key}" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "eth_call",
    "params": [
        "to": "0xD81dE4BCEf43840a2883e5730d014630eA6b7c4A",
        "data": "0x3bc778e31084a22c024e103ced6be2a51c7..."


	"jsonrpc": "2.0",
	"id": null,
	"result": "0x"