Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

1. Abstract

This spec introduces a protocol fee that Relying Parties (RPs) pay in order to authorize proofs for specific World ID(s). Paid users (World ID holders) must provide a valid proof of the RP’s payment in order to present proofs to the RP in a given billing period. During a billing period, a paid World ID can perform any number of proofs.

Important

This spec is introduced as an experiment, and it applies only to select RPs and only to Proofs of Embedding Similarity (WIP-202). Other Proofs and other RPs keep their existing behavior. This spec is expected to be updated based on experimentation results, and may inform the billing mechanics introduced throughout the World ID Protocol. Accordingly, no organization or individual is making a commitment, promise, or obligation to develop, implement, or make available any feature, product, or functionality described in this spec, and there is no guarantee that they will ever exist or perform as described in the future.

2. Motivation

Fees are charged to RPs receiving proofs, not individual end users making proofs, because RPs can more easily estimate the value generated by World ID and compare it to the fees charged for using World ID. It also follows that RPs usually pay fees for services they use.

Monthly per-World ID billing lets an RP budget for distinct users without paying for each repeat proof. They make it easier for RPs to trade off per-monthly-user value against fees compared to per-proof fees. Repeated Embedding Similarity comparisons motivate this design. The World ID protocol is uniquely able to support such a fee mechanism because it exposes “unique human” as a protocol primitive.

3. High-Level Overview

This section introduces a non-normative high-level description of the billing mechanism.

  1. This spec is experimentally introduced. This means that it will apply only to a specific set of RPs and only to Embedding Similarity Proofs. The administrator selects these RPs through an allowlist in the Billing Contract. This is likely to be extended in the future with a spec update.
  2. An RP must pay for fees during a billing period in order to request proofs from a user.
  3. Each calendar month is valid from 00:00:00 UTC+14:00 on its first day until 00:00:00 Anywhere on Earth (AoE, UTC−12:00) on the next month’s first day. Periods overlap by 26 hours.
  4. Billing is done through a smart contract (the “Billing Contract”).
  5. Registration capacity is priced in USD and purchased with WLD.
  6. Capacity is purchased for one RP and one billing period. Each unit permits one distinct World ID to register. A capacity of 2 means that the RP can receive proofs from two distinct World IDs during that period.
  7. When an RP requests a proof from a user (see ProofRequest), the RP signs the request with its own action, whose first byte marks the request as a billed proof. The same signature also authorizes usage of a capacity of at most one.
  8. A user generates a Uniqueness Proof for the billing action corresponding to the current period using a valid credential. The billing action is derived from the RP and the period; the RP does not directly sign it. This proof results in a special type of nullifier denominated the billingNullifier, which is submitted once to the Billing Contract with the RP’s authorization to claim capacity. The Authenticator caches the nullifier and its private OPRF output for the billing period.
  9. Billing for Proofs of Embedding Similarity is enforced by TEE Verifiers (WIP-201) where a valid proof of payment (through a billingNullifier) must be presented. If a proof is not presented, the Verifier rejects the request. The Verifier signs the checked billingNullifier into the Flamingo Token, and the Embedding Similarity circuit (WIP-202) proves that it belongs to the World ID making the proof.
  10. An RP may purchase or top up registration capacity for the current AoE month and the next calendar month. Current-month purchases are prorated by seconds left in the AoE month. Future months are paid in full. Capacity cannot move between RPs or periods.
  11. Unused capacity receives no rebate and does not roll over.
  12. Users do not pay for fees; only RPs (or an entity acting on an RP’s behalf) pay.
  13. Fees are set per World ID and period in the Billing Contract. An administrator may update the USD fee amount on-chain. Payments settle in WLD; earlier purchases remain unchanged. An oracle price is used to convert between USD and WLD.

4. Specification

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

The Field ($F$), the Curve (BabyJubJub), and the Hashing ($H_t$) definitions MUST follow the requirements of WIP-100.

4.1 Constants

ConstantValueMeaning
CHAIN_ID480World Chain mainnet, which hosts the production Billing Contract.
WLD_TOKEN0x2cFc85d8E48F8EAB294be644d9E25C3030863003WLD ERC-20 on World Chain (18 decimals). The production fee token.
PERIOD_OVERLAP93600 seconds (26 hours)Overlap between consecutive billing periods (§4.3).
REGISTRATION_CONFIRMATIONS12 blocksDepth below the provider’s latest block at which TEE Verifiers read billing state (§4.8).
MAX_RP_STATUS_AGE60 secondsMaximum age of an isBilledRp read a TEE Verifier relies on (§4.8).
BILLED_ACTION_PREFIX0x03Most significant byte of an RP action that may authorize registration (§4.5). Reserved for Proofs of Embedding Similarity.

4.2 Billing Contract

The Billing Contract enables the RP to purchase capacity and the registration of payment-authorized World IDs for a given RP.

  1. RPs MAY purchase registration capacity through purchaseCapacity for the current period or the next period. Importantly anyone can purchase capacity for an RP, but each purchase belongs to its rpId and periodStart.

  2. Users MAY register against that capacity through register. They supply their period billingNullifier, a Uniqueness Proof, its credential verification inputs and the RP’s RegistrationAuthorization.

  3. The following interface is normative. Implementations MUST also enforce the requirements below.

    interface WorldIDBilling {
        struct BillingContext {
            uint256 chainId;
            address billingContract;
            uint64 rpId;
            uint64 periodStart;
        }
    
        struct RegistrationAuthorization {
            uint8 version;
            uint160 oprfKeyId;
            uint256 nonce;
            uint64 createdAt;
            uint64 expiresAt;
            uint256 signedAction;
            bytes signature;
            bytes wip101Data;
        }
    
        struct Registration {
            RegistrationAuthorization authorization;
            uint64 issuerSchemaId;
            uint64 expiresAtMin;
            uint256 billingNullifier;
            uint256[5] proof;
        }
    
        function quotePurchase(BillingContext calldata scope, uint256 capacityAmount)
            external view returns (uint256 wldAmount);
    
        function purchaseCapacity(
            BillingContext calldata scope,
            uint256 capacityAmount,
            uint256 maxWldAmount
        ) external;
    
        function register(
            BillingContext calldata scope,
            RegistrationAuthorization calldata authorization,
            uint64 issuerSchemaId,
            uint64 expiresAtMin,
            uint256 billingNullifier,
            uint256[5] calldata proof
        ) external returns (bool success);
    
        function registerMany(BillingContext calldata scope, Registration[] calldata registrations)
            external returns (bool[] memory successes);
    
        function isRegistered(uint256 billingNullifier) external view returns (bool);
        function registrationOf(uint256 billingNullifier)
            external view returns (uint64 rpId, uint64 periodStart, uint64 registeredAt);
        function getPeriodCapacity(uint64 rpId, uint64 periodStart)
            external view returns (uint256 capacity, uint256 registeredCount);
        function usedNonces(uint64 rpId, uint256 nonce) external view returns (bool);
        function getPinnedOprfKey(uint64 rpId, uint64 periodStart)
            external view returns (uint160 oprfKeyId, uint256[2] memory oprfPublicKey);
        function isBilledRp(uint64 rpId) external view returns (bool);
        function setBilledRp(uint64 rpId, bool billed) external;
    
        function getBillingPeriod(uint64 timestamp)
            external view returns (uint64 periodStart, uint64 periodEnd);
        function getActivePeriods(uint64 timestamp)
            external view returns (uint64 currentPeriodStart, uint64 nextPeriodStart);
        function getBillingAction(BillingContext calldata scope) external pure returns (uint256);
        function getFeeToken() external view returns (address);
        function setFeeToken(address feeToken) external;
        function getFeeRecipient() external view returns (address);
        function setFeeRecipient(address feeRecipient) external;
        function priceSource() external view returns (address);
        function setPriceSource(address priceSource) external;
        function pricePerWorldID() external view returns (uint256);
        function setPricePerWorldID(uint256 pricePerWorldID) external;
        function maxPriceAge() external view returns (uint64);
        function setMaxPriceAge(uint64 maxPriceAge) external;
        function maxRequestAge() external view returns (uint64);
        function maxRequestLifetime() external view returns (uint64);
        function setRequestLimits(uint64 maxRequestAge, uint64 maxRequestLifetime) external;
        function owner() external view returns (address);
        function paused() external view returns (bool);
        function setPaused(bool paused) external;
    
        event CapacityPurchased(
            uint64 indexed rpId,
            uint64 indexed periodStart,
            address indexed payer,
            uint256 capacityAmount,
            uint256 wldPaid,
            uint256 usdPerWld,
            uint256 pricePerWorldID
        );
        event Registered(
            uint64 indexed rpId,
            uint64 indexed periodStart,
            uint256 indexed billingNullifier
        );
        event DuplicateRegistration(
            uint64 indexed rpId,
            uint64 indexed periodStart,
            uint256 indexed billingNullifier,
            uint256 nonce
        );
        event OprfKeyPinned(
            uint64 indexed rpId,
            uint64 indexed periodStart,
            uint160 oprfKeyId
        );
        event BilledRpUpdated(uint64 indexed rpId, bool billed);
    }
    
  4. The contract MUST enforce these requirements.

    • Configuration: Production deployments run on CHAIN_ID with fee token WLD_TOKEN (§4.1); the fee recipient is the designated protocol fee address. The initialized proxy owner is the administrator. Only that owner may perform administrative operations. Address settings MUST be nonzero; the price source MUST implement §4.9. Configuration changes MUST emit events and MUST NOT alter purchased capacity, registrations, period boundaries or pinned keys. Request limits MUST agree with the OPRF nodes (§4.5).
    • Periods: getBillingPeriod returns the AoE month containing timestamp and its exclusive periodEnd (§4.3). The contract MUST reject a context unless scope.chainId == block.chainid, scope.billingContract == address(this) and periodStart is canonical (§4.3).
    • Purchases: quotePurchase and purchaseCapacity MUST validate the context and the current-period/next-period purchase window. purchaseCapacity MUST quote the prorated price (§4.9) at execution and reject wldPaid > maxWldAmount. It MUST add exactly capacityAmount to that RP and period, transfer exactly wldPaid from the caller to the configured fee recipient, and require the recipient’s balance to increase by exactly wldPaid. Payment, capacity and CapacityPurchased MUST be atomic and protected against reentrancy.
    • Registration: register MUST require an active period under §4.3, reject a used nonce for that RP and verify all authorization and proof inputs under §§4.4–4.5. A new nullifier requires registrations to be unpaused and registeredCount < capacity; it increments the count once, records the nullifier with its rpId and periodStart and emits Registered. A valid duplicate returns false and consumes no further capacity, including while new registrations are paused. It still requires fresh valid authorization, consumes that authorization’s nonce and emits DuplicateRegistration. CapacityPurchased MUST carry the usdPerWld and pricePerWorldID that produced wldPaid (§4.9).
    • Atomicity: purchaseCapacity, register and registerMany MUST be protected against reentrancy. registerMany applies register to each entry and reverts entirely if any entry fails. isRegistered reports whether the nullifier is registered. registrationOf returns the rpId and periodStart it was registered under and the registration time, or zeros if it is not registered; billingAction already scopes each nullifier to one RP and period.
    • Billed RPs: isBilledRp reports whether an RP’s Proofs of Embedding Similarity are billed (§4.7). Only the owner may call setBilledRp; it takes effect immediately and MUST emit BilledRpUpdated. Purchases and registrations MUST NOT depend on it, so capacity can be bought before an RP is billed and in-flight registrations survive its removal.

4.3 Billing Context

Every RP uses the same AoE calendar months. periodStart is 00:00:00 AoE on the month’s first day (12:00:00 UTC). periodEnd is 00:00:00 AoE on the next month’s first day and is exclusive. The active interval is [periodStart - PERIOD_OVERLAP, periodEnd), clipped at Unix timestamp zero. Months overlap by PERIOD_OVERLAP (§4.1). Either active context MUST be accepted. Capacity, registrations and billing actions remain separate for each period. Nonces are consumed per RP across periods.

A periodStart is canonical if and only if it equals getBillingPeriod(periodStart).periodStart, i.e. it is 00:00:00 AoE on a month’s first day. The Billing Contract and billing verifiers MUST reject a noncanonical periodStart. getBillingPeriod returns the AoE month containing timestamp and its exclusive end; months beginning before Unix timestamp zero MUST be rejected. Clients MUST use the selected context’s active interval for registration and proof acceptance. getActivePeriods(timestamp) returns the periodStart of the month containing timestamp and, while timestamp is within PERIOD_OVERLAP of the next month’s start, that next periodStart (otherwise zero); the Contract’s registration check uses the same rule. Purchases and top-ups remain limited to the purchase window in §4.2; the overlap does not permit purchases after periodEnd. Any future configurable-period extension MUST expose its boundaries on-chain and preserve already purchased periods.

uint256 billingAction = uint256(keccak256(abi.encode(
    "world-id-billing-v1",
    scope.chainId, scope.billingContract, scope.rpId, scope.periodStart
))) >> 8;

billingAction: Field is the billing OPRF input. It is derived from scope and it is not directly signed by the RP, instead the RP authorizes its own ProofRequest.action to avoid the need for double signing (§4.5). The billing nullifier is UniquenessNullifier(rpOprfKey, worldIdLeafIndex, scope.rpId, billingAction). UniquenessNullifier means the existing Uniqueness Proof nullifier computation, using these inputs and the unblinded OPRF response under the selected key.

4.4 Registration Proof

Registration MUST reuse the existing Uniqueness Proof circuit, public-input order, proving artifacts and verification key. This spec introduces no new circuit or trusted setup. The user MUST present a valid credential from a registered issuer/schema; the billing nullifier remains independent of which credential is used.

4.4.1 Inputs

The Authenticator derives the billing query H_4(DS_QUERY; leaf_index, scope.rpId, billingAction) (§4.7) and sends it with the RP’s request (§4.5), which supplies the OPRF key and the nonce. The nonce authenticates the request and is a public input of the proof (§4.4.1), but it is not an OPRF-query input. Note that the action in the RP’s request is only used for authorization, it’s not used on the OPRF query or nullifier computation. The registration proof omits the signal and session creation, and sets the minimum credential genesis issuance time to zero.

The Contract MUST construct the existing uniqueness verifier’s public inputs as follows:

InputValue
nullifier, action, rpId, noncebillingNullifier, billingAction, scope.rpId, authorization.nonce.
issuerSchemaId, expiresAtMinSupplied by the Authenticator to register. They are not matched against the RP’s request; only §4.4.2 applies.
signalHash, credentialGenesisIssuedAtMin, sessionIdZero.
Account root, tree depth and credential issuer public keyRoot from proof[4], depth MAX_DEPTH (WIP-100 §5.1), and issuer key from CredentialSchemaIssuerRegistry.
OPRF public keyThe RP’s OPRF public key pinned for the context (§4.4.2).

proof: uint256[5] is the World ID Proof Envelope of WIP-100 §3.7.2.

4.4.2 Verification

These checks apply to the Contract’s register. TEE Verifiers check billing as in §4.7.

All existing account, authenticator signature, OPRF, nullifier and credential constraints MUST remain enforced, including WIP-100 field, scalar and point validation. The Contract MUST validate the account root and issuer/schema against the existing registries and enforce the existing uniqueness verifier’s credential expiry freshness policy.

For the first registration in a billing context, the Contract MUST resolve the RP’s existing authorized OPRF key from the registries and pin its key ID and public key.

4.5 RP Authorization

The RP’s normal ProofRequest authorizes registration. The RP signs nothing extra, keeps its own action, and does not need to know whether the user is registered. An Authenticator without a cached registration for the active context (§4.6) registers with that request before proving.

The request’s action is the RP’s own action, and its most significant byte MUST be BILLED_ACTION_PREFIX (§4.1). Only Proof of Embedding Similarity requests carry such an action (WIP-202 §4.4). The RP signature does not cover the proof type, so the prefix is what marks a request as one that may consume capacity. The RP signs the RP signature message with that action as usual: version || nonce || createdAt || expiresAt || signedAction, 81 bytes. This is the packed message, not the SHA-256 value returned by ProofRequest::digest_hash. A request without an action cannot authorize registration.

To reiterate from the original specs, it is strongly RECOMMENDED that RPs always generate fresh private keys from a cryptographically-secure randomness source for signing keys and store it in secure environments (e.g. HSMs). If an RP registers a signing key that someone else possesses, or a smart contract that someone else controls, that party can consume capacity on the RP’s behalf.

The registration proof always uses billingAction (§4.3) as the action for the OPRF Query and subsequently the nullifier computation. A valid RP signature over signedAction authorizes exactly two actions for the same rpId: a proof for the signedAction itself (which is processed out of the scope of this spec), and billing authorization for the current active billing context. It MUST NOT authorize any other action. Billing authorization requires signedAction to carry BILLED_ACTION_PREFIX; a signature over any other action, e.g. a Uniqueness Proof’s (most significant byte 0x00), MUST NOT authorize registration.

RegistrationAuthorization copies the request’s version, nonce, timestamps, OPRF key ID, action (as signedAction), signature and WIP-101 data. The request’s rp_id and oprf_key_id MUST match scope.rpId and the context’s pinned key (§4.4.2). The Contract MUST reject a version it does not support.

  • Contract: register MUST reject a signedAction whose most significant byte is not BILLED_ACTION_PREFIX. It MUST verify the authorization as OPRF nodes verify RP requests, with signedAction as the action, and MUST verify the proof with billingAction as the action. A registered signer without code is verified by EIP-191 recovery, and wip101Data MUST be empty. A signer with code MUST support IWIP101 through ERC-165 and is verified through WIP-101 verifyRpRequest with signedAction, with wip101Data of at most 1024 bytes.
  • OPRF nodes: Nodes MUST accept a billing query: an OPRF query whose action is billingAction, carrying scope and the RP’s signedAction as signature verification data, as session create-and-bind queries carry the uniqueness action. Nodes MUST verify the RP signature over signedAction, and MUST recompute billingAction from scope and check that scope.chainId and scope.billingContract match their configuration, scope.rpId matches the request, and scope.periodStart is canonical and active (§4.3). Billing queries MUST use a dedicated nonce scope, so one RP request authorizes at most one application query and one billing query. Nodes MUST reject a billing query for any action other than the recomputed billingAction, or whose signedAction does not carry BILLED_ACTION_PREFIX. They MUST accept such a signedAction as signature verification data on session-seed queries, so that a Proof of Embedding Similarity can re-derive its session.
  • Freshness: The Contract MUST check freshness as OPRF nodes do, with now the block timestamp: now < expiresAt, |now - createdAt| <= maxRequestAge and expiresAt - createdAt <= maxRequestLifetime. Accepting a createdAt up to maxRequestAge in the future tolerates RP clock skew; with a strict createdAt <= now, an RP whose clock runs ahead would get requests the nodes accept and the Contract rejects. The uint64 limits MUST agree with the OPRF nodes: maxRequestAge with their createdAt tolerance in both directions, and maxRequestLifetime with their maximum expiresAt - createdAt.

Unsigned request fields MUST NOT be treated as RP-authenticated.

4.6 Authenticator Cache

The Authenticator MUST:

  • Cache the billing nullifier, billingAction and the unblinded OPRF output of the billing query by account, BillingContext, and the RP’s OPRF key ID and OPRF public key. The Proof of Embedding Similarity needs no other OPRF witness (§4.7).
  • Keep OPRF witnesses, credential witnesses and query authorization inputs private. Together with the public billingNullifier, the cached OPRF output reveals the account’s leaf index to a search over leaf indices.
  • Decide whether to register from isBilledRp. A Verifier rejecting a request because the RP is billed means that read was stale; re-read it and register.
  • Choose the context for a request in this order: a cached registration for any active context (§4.3); otherwise the next period, if it is active and getPeriodCapacity shows capacity left; otherwise the current period. During the overlap, a registration under the next period covers the rest of the overlap and the whole next period with one unit of capacity. An Authenticator that lost its cache cannot tell whether it is registered for the current period, and each RP request authorizes only one billing query (§4.5). So during the overlap it may register under the next period while already registered for the current one. That uses one unit of the next period’s capacity early, a unit the World ID needs anyway if it returns in that period.
  • After registering, wait until the registration is REGISTRATION_CONFIRMATIONS blocks deep before sending the Verifier request (§4.8).
  • Drop an entry a Verifier reports as unregistered, e.g. after a reorganization (§6), and register again with a later RP request.
  • Refresh account membership, RP authorization and credential verification inputs before generating a new registration proof.
  • Expire entries at period end.
  • Recover lost entries, including after switching Authenticators, through a fresh authorized query for the same World ID and BillingContext under the same pinned key. The nullifier MUST remain unchanged. An existing registration remains valid and recovery MUST NOT consume more capacity.
  • Not apply single-use nullifier replay protection to billingNullifier within its BillingContext, so that retries after a failed register and recovery remain possible. Before exempting a nullifier, the Authenticator MUST recompute billingAction for a trusted Billing Contract and a canonical, active periodStart.

Without a cached registration for the active context, the Authenticator makes a billing query for billingAction from the same RP request, in addition to any query the RP’s own proof needs. A Proof of Embedding Similarity needs one only to re-derive a session.

4.7 Billing Verification

Billing for the Proof of Embedding Similarity is enforced by the WIP-201 TEE Verifier, for the RPs isBilledRp reports (§4.2). The sealed request carries billing_nullifier (WIP-201 §4.4.1): the billingNullifier of the Authenticator’s registration for aud in an active period, or 0 if it holds none.

The TEE Verifier MUST read the Billing Contract it is configured with, on its configured chain (CHAIN_ID in production, §4.1). The sealed request names neither, and the Verifier MUST NOT take chain targets from it. Before running the Engine, it MUST read isBilledRp(aud), where aud is the RP’s rpId (WIP-201), as §4.8 requires, and then:

  • Billed RP. Reject the request unless billing_nullifier is nonzero and registrationOf(billing_nullifier) returns aud as its rpId and a periodStart whose context is active (§4.3). Sign billing_nullifier into the Flamingo Token’s billing_nullifier claim.
  • RP not billed. Sign billing_nullifier = 0, whatever the request carries.

The Verifier does not verify the RP’s signature. Capacity is consumed only by register, which does (§4.5), and the RP checks its own nonce when it verifies the proof (WIP-201 §4.7).

The Verifier never sees the Proof of Embedding Similarity, which is produced after the Flamingo Token and verified by the RP. The RP is the billed party, so billing MUST NOT depend on any check the RP performs. Instead, the Proof of Embedding Similarity (WIP-202 constraint 7) MUST prove that a nonzero billing_nullifier belongs to the World ID it proves, for the RP aud. With billingAction and the cached OPRF output R (§4.6) as private inputs, it constrains

billing_nullifier = H_4(DS_OPRF_PROOF; H_4(DS_QUERY; leaf_index, rp_id, billingAction), R.x, R.y)

with rp_id the request’s aud, DS_QUERY and DS_OPRF_PROOF as in WIP-202 §4.2, and with the same leaf_index as its Merkle inclusion proof. This is the UniquenessNullifier of §4.3. The registration proof already verified R under the pinned key (§4.4), and only registered nullifiers reach the Flamingo Token, so the circuit does not verify the OPRF evaluation again and takes no billing public inputs. The cached output does not depend on a Merkle root, so the proof uses its own fresh root and WIP-202 Root Freshness is unchanged.

4.8 Chain State

TEE Verifiers MUST:

  • Read state through an approved provider with TLS terminating inside the enclave. The provider is trusted for state and finality.
  • Read isBilledRp and registrationOf at the block REGISTRATION_CONFIRMATIONS below the provider’s latest block, not at the latest block or at finality (§6).
  • Use the Verifier’s time (WIP-201 §4.4.2) as now for the active period (§4.3) and for validating the provider’s TLS certificates. Block timestamps MUST NOT serve as the Verifier’s time.
  • Cache a positive registration by billingNullifier, with its rpId and periodStart, only once its registeredAt is at or before the timestamp of the provider’s finalized block, and re-read it until then. Bound the cache and expire entries at period end; no cache entry may outlive the period’s duration plus 24 hours of grace.
  • Fail closed. If billing state cannot be read, reject the request; never treat the RP as not billed instead. Never rely on an isBilledRp read older than MAX_RP_STATUS_AGE, except that while reads fail, an RP last read as billed MAY still be treated as billed.

During provider outages, a cached registration MAY serve a request for an RP last read as billed, subject to trusted time and security policy; every other request is rejected, including for RPs that are not billed. Provider failure MUST NOT fail process liveness: these rejections are an explicit degraded mode, and Verifiers SHOULD expose their rate as a metric. TEE Verifiers SHOULD apply a two-second timeout to each request-path chain read, make no retries, and share concurrent lookups for the same key. Any sealed request can trigger reads, e.g. with an unseen aud or billing_nullifier, so Verifiers MUST bound concurrent reads and reject requests beyond that bound rather than queue them.

4.9 Fees

Fees are configured in USD per World ID per period and paid in WLD. Each unit of registration capacity permits one distinct World ID to register.

The RP approves the Billing Contract’s WLD allowance, then calls purchaseCapacity. The contract transfers WLD from the payer to the designated protocol fee address and adds capacity atomically (§4.2). Approval alone does not pay the fee.

ValueTypeUnits
pricePerWorldID, usdPerWlduint256USD scaled by 10^18.
TOKEN_UNITuint25610^decimals for the configured WLD token; MUST fit in uint256.
observedAt, maxPriceAgeuint64Exchange-rate timestamp and maximum age, in seconds.

pricePerWorldID is the full-month price. Current-month purchases and top-ups MUST be prorated by seconds left in the AoE month. Future months pay in full. Overlap adds no fee.

duration  = periodEnd - periodStart
remaining = periodEnd - max(block.timestamp, periodStart)
charge    = ceil(capacityAmount * pricePerWorldID * remaining * TOKEN_UNIT / (duration * usdPerWld))

For example, a purchase with 10 days left in a 30-day AoE month costs 10/30, about 33.33% of the full-month price. duration is the AoE month’s own length: 28, 29, 30 or 31 days.

Charges are in WLD base units. The contract MUST use checked USD totals and full-precision conversion.

The price source MUST be an on-chain adapter for the Chainlink WLD/USD Data Stream. A report submitter obtains a signed report and submits it to the adapter. The adapter MUST verify it through the configured Chainlink verifier, require the configured feed ID, and normalize its price to USD scaled by 10^18. It MUST expose only verified usdPerWld and observedAt values to the Billing Contract. Unverified reports MUST NOT update the stored rate.

quotePurchase and purchaseCapacity read that verified rate; a getter alone does not obtain or authenticate a report. The charge MUST NOT exceed the payer’s maxWldAmount. Zero, future-dated, stale or unavailable prices MUST reject the purchase without affecting existing capacity. The feed ID, verifier, adapter, report format, source scale and maxPriceAge MUST be selected and tested before release (§8).

Unused capacity receives no refund and does not roll over. Top-ups and later parameter changes MUST NOT alter earlier purchases.

Warning

OPRF key rotation is not currently supported. A rotation that replaces an RP’s pinned key before period end invalidates that period’s billingNullifiers without refund, and the Contract rejects every registration for that period until it ends.

5. Rationale

  • Billing nullifier: Counts one World ID across credentials without publishing its account leaf index.
  • Cached OPRF output: Avoids repeated billing OPRF queries. With billingAction, it is all the Proof of Embedding Similarity needs.
  • Nullifier in the Flamingo Token: The RP is the billed party, so the nullifier the Verifier checked reaches the proof through the Verifier’s signature, not through the RP. It stays private in the proof, which keeps proofs unlinkable within a period.
  • No OPRF verification in the Embedding Similarity circuit: Registration already verified the OPRF output. A preimage of the registered nullifier binds it to the World ID and RP at a fraction of the cost (WIP-202 §5).
  • Allowlist in the Billing Contract: Authenticators and Verifiers read the same state, and changing it needs no new enclave measurement.
  • Confirmation depth instead of finality: On World Chain, the finalized block trails the latest by about 15–20 minutes, longer than an RP request lives; REGISTRATION_CONFIRMATIONS take about 24 seconds (§6).
  • No RP signature in the Verifier: Billing depends only on registrations, and register already verifies the RP’s signature. Checking it again in the Verifier would add fields to the sealed request, a registry read and a freshness check, for no billing guarantee.
  • Failing closed: The host relays all of a Verifier’s traffic and could block its chain reads. Treating an unreadable RP as not billed would let it remove billing. Every RP is expected to be billed eventually.
  • Registration capacity: One unit covers one distinct World ID for one RP and period, matching the RP’s monthly user budget.
  • Overlapping months: Capacity covers the calendar month in every time zone. Preferring the next period for registrations during the overlap means a user first seen there who returns next month uses one unit of capacity, not two.
  • Prorating: An RP that starts or tops up mid-month pays only for the time left in the AoE month.
  • No rebate or rollover: Keeps each month’s capacity and revenue independent and avoids refund or carry-forward accounting.
  • Reusing the RP request: The RP cannot know whether a user is already registered, so its normal request must be enough to register.
  • Derived billing action: Deriving billingAction instead of having the RP sign it keeps the RP’s action free for its own proof. Proofs of Embedding Similarity do not use the RP’s action, but Uniqueness Proofs do, so this keeps billing extensible to other proof types.
  • Reserved action prefix: The RP signature does not cover the proof type. Without the prefix, a Uniqueness Proof request would also authorize registration, and a modified Authenticator could spend a billed RP’s capacity on users who never made a Proof of Embedding Similarity. A prefix in the signed action marks the request type at no extra signing cost. OPRF nodes already reject Uniqueness queries whose action is not 0x00-prefixed, so an embedding request cannot authorize a Uniqueness Proof either.

6. Security

An RP request is bearer authorization until bound to a holder. Consuming the nonce limits registrations but does not bind authorization to the intended holder.

Any RP-signed request whose action carries BILLED_ACTION_PREFIX can authorize one registration in an active context of that RP, once per nonce. It yields only the billingNullifier in addition to the nullifier for the signed action. OPRF nodes and the Contract MUST treat the recomputed billingAction as the only additional action a signature authorizes; accepting any other derived action would let a signature over one action yield nullifiers for actions the RP never approved.

The registration and the application proof share the RP’s nonce, which is public in register calldata. An observer who also sees the application proof’s nonce can link the application proof to the billingNullifier.

A paid-state lookup alone does not prove that the prover owns the registered World ID. The Verifier signs the checked nullifier into the Flamingo Token, and the Embedding Similarity circuit binds it to the prover (§4.7). This holds against a modified Authenticator, which can neither obtain a Flamingo Token for a billed RP without a registered nullifier nor prove with one that is not its own.

It does not hold against an RP that colludes with the user’s Authenticator. Together they can verify the Flamingo Token outside the circuit and skip WIP-202 constraint 7, or use a Verifier measurement that skips billing. WIP-201 places this collusion out of scope, and so does this experiment, although its cost falls on the fee recipient rather than on the RP.

Verifiers read registrations REGISTRATION_CONFIRMATIONS blocks deep rather than at finality (§4.8). Blocks not yet posted to L1 can still be reorganized, e.g. on a sequencer failover or a batcher failure. The chance of a reorganization that deep is very low, and this experiment accepts it. If a registration is reorganized out after a Verifier admitted it, Flamingo Tokens were issued without consumed capacity. Verifiers re-read a registration until it is finalized (§4.8), so this is limited to requests admitted before the reorganization. The sequencer usually re-includes the transaction; if it then fails, e.g. on exhausted capacity, the Verifier rejects the next request and the Authenticator registers again (§4.6).

Registrations are linkable within their billing context. The Proof of Embedding Similarity keeps billing_nullifier private, so only the proof whose request registered is linkable to it, through the nonce. Implementations MUST NOT log credentials, private witnesses or sealed payloads.

7. Backwards Compatibility

This spec is additive. It adds the Billing Contract and a billing check for select RPs’ Proofs of Embedding Similarity. OPRF nodes gain a billing query mode and nonce scope (§4.5) and need the Billing Contract’s chain and address configured; existing queries are unchanged. The Flamingo Token gains a billing_nullifier claim under a new digest label (WIP-201), and the Proof of Embedding Similarity a constraint (WIP-202); Verifiers, Authenticators and RPs MUST upgrade together. Other proof types, RPs and contracts are unchanged.

The billing action encoding and pinned keys MUST remain fixed for active periods. Rollout, rollback and pauses MUST preserve purchased capacity and registrations.

8. Experimental Release

Before enablement:

  • Select RPs, deployment addresses, the proxy administrator, designated protocol fee address, initial USD price and request limits. Select the Chainlink feed, verifier, adapter, report format, scale and price-age limit (§4.9).
  • The contract and OPRF nodes MUST adopt the billing authorization of §4.5. OPRF nodes supporting billing queries MUST be deployed before enablement. WIP-202 requests MUST carry an action, including with a session_id (WIP-202 §4.4), and Verifiers MUST sign the Flamingo Token’s billing_nullifier claim (§4.7).
  • Add each selected RP through setBilledRp only once it has capacity for the active period; until then, its users’ proofs fail. Calling setBilledRp(rpId, false) is the kill switch: while the chain is reachable, Verifiers stop billing the RP within MAX_RP_STATUS_AGE of the change reaching REGISTRATION_CONFIRMATIONS blocks.
  • WIP-101 contract-backed RPs are excluded until OPRF nodes support signature verification data for them.

The proxy owner updates pricePerWorldID and other billing settings through the authorized on-chain setters. Changing the price does not require a node software release and MUST preserve earlier purchases.

TEE operators deploy compatible verifier images. Authenticators and RPs approve the accepted attestation measurements and keys under WIP-201. The Engine Provider signs engine releases; the RP’s engine policy decides which releases to accept. Operators MUST NOT treat an unsigned or unapproved release as accepted.

OPRF operators deploy compatible node software and keep pinned period keys available through period end. Clients verify OPRF proofs against the registry-authorized public keys, including the pinned key for an active billing context. Before enablement, record the operators, policy owners and contract upgrade owner and agree on compatible versions and key retention.

9. Rejected Alternatives

  • Per-proof charging: Rejected for this experiment because it charges repeated comparisons and makes monthly costs less predictable. Any future per-proof scheme requires a separate proposal defining pre-purchase and expiry rules.
  • Credential-specific registration: Charges one World ID more than once across credentials or renewals.
  • Off-chain-only billing: Rejected for this experiment because capacity and registration would depend on a billing operator.
  • RP signs billingAction directly: Works only when the RP’s action is unused, as in Proofs of Embedding Similarity. It prevents billing Uniqueness Proofs, which need the RP’s own action, and makes RPs compute a period-dependent action for every request.
  • Arbitrary registration action: The billing unit would become one World ID per action, so a changing or random action would bill per proof. It would also publish the RP’s application nullifier on-chain and make it linkable across periods.