Skip to main content

Iris Verification System

Purpose: Biometric identity verification using iris scanning to prove creator authenticity and prevent fraud.

Why Iris?

MethodFraud RiskUser FrictionCost
EmailHighLowFree
PhoneMediumLow~$0.01
ID DocumentMediumHigh~$2-5
Iris ScanVery LowMedium~$0.10
Key insight: Iris patterns are unique to each person (more unique than fingerprints) and cannot be replicated or stolen like passwords.

Integration Options

Worldcoin provides World ID - a privacy-preserving proof-of-personhood.
User scans iris at Orb → Gets World ID → Proves uniqueness
Pros:
  • Existing infrastructure (5M+ verified)
  • Privacy-preserving (zero-knowledge proofs)
  • No PII storage required
  • SDK available
Cons:
  • Requires Orb access (limited locations)
  • Third-party dependency

Option 2: Custom Implementation

Build using iris recognition APIs (e.g., IriTech, EyeLock). Pros:
  • Full control
  • No third-party dependency
Cons:
  • Higher development cost
  • Regulatory complexity
  • PII storage requirements

Database Schema

iris_verifications (Proposed)

CREATE TABLE iris_verifications (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  creator_id uuid REFERENCES creator_profiles(id) UNIQUE,
  
  -- Verification method
  provider text NOT NULL,                 -- 'worldcoin', 'internal'
  
  -- Worldcoin integration
  world_id_nullifier_hash text,           -- Unique identifier (not the iris itself)
  world_id_proof jsonb,                   -- ZK proof for verification
  world_id_merkle_root text,
  
  -- Verification status
  status text DEFAULT 'pending',          -- 'pending', 'verified', 'failed', 'revoked'
  
  -- Timestamps
  initiated_at timestamptz DEFAULT now(),
  verified_at timestamptz,
  expires_at timestamptz,                 -- Optional: require re-verification
  
  -- Audit
  verification_ip inet,
  verification_device text,
  failure_reason text,
  
  created_at timestamptz DEFAULT now(),
  updated_at timestamptz DEFAULT now()
);

CREATE INDEX idx_iris_creator ON iris_verifications(creator_id);
CREATE INDEX idx_iris_status ON iris_verifications(status);
CREATE INDEX idx_iris_worldid ON iris_verifications(world_id_nullifier_hash);

Worldcoin Integration Flow

┌─────────────────────────────────────────────────────────────────────────┐
│                    WORLDCOIN VERIFICATION FLOW                          │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   USER                        OUR APP                     WORLDCOIN     │
│   ┌─────────┐                ┌─────────┐                ┌─────────┐    │
│   │ Clicks  │                │ Shows   │                │         │    │
│   │ Verify  │────Request────▶│ QR Code │                │         │    │
│   └─────────┘                └─────────┘                └─────────┘    │
│        │                          │                          │         │
│        │                          │                          │         │
│   ┌─────────┐                     │                     ┌─────────┐    │
│   │ Scans   │                     │                     │ Verifies│    │
│   │ with    │─────────────────────┼────────────────────▶│ Proof   │    │
│   │ World   │                     │                     └─────────┘    │
│   │ App     │                     │                          │         │
│   └─────────┘                     │                          │         │
│        │                          │                          │         │
│        │                     ┌─────────┐                     │         │
│        │                     │ Receives│◀────Callback────────┘         │
│        │                     │ Proof   │                               │
│        │                     └─────────┘                               │
│        │                          │                                    │
│        │                     ┌─────────┐                               │
│        │                     │ Stores  │                               │
│        └─────Complete───────▶│ Updates │                               │
│                              │ Status  │                               │
│                              └─────────┘                               │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Implementation

1. Initiate Verification

import { IDKitWidget } from '@worldcoin/idkit';

function VerifyButton({ creatorId }) {
  const handleVerify = async (proof) => {
    // Send proof to our backend
    const response = await fetch('/api/verify-iris', {
      method: 'POST',
      body: JSON.stringify({
        creator_id: creatorId,
        proof: proof
      })
    });
    
    if (response.ok) {
      // Update UI to show verified status
    }
  };

  return (
    <IDKitWidget
      app_id={process.env.NEXT_PUBLIC_WORLDCOIN_APP_ID}
      action="verify-creator"
      onSuccess={handleVerify}
      credential_types={['orb']}  // Require Orb verification, not just device
    >
      {({ open }) => (
        <button onClick={open}>
          Verify with World ID
        </button>
      )}
    </IDKitWidget>
  );
}

2. Backend Verification

// /api/verify-iris
import { verifyCloudProof } from '@worldcoin/idkit';

export async function POST(request) {
  const { creator_id, proof } = await request.json();
  
  // 1. Verify the proof with Worldcoin
  const verifyResult = await verifyCloudProof(
    proof,
    process.env.WORLDCOIN_APP_ID,
    'verify-creator'
  );
  
  if (!verifyResult.success) {
    return Response.json({ error: 'Invalid proof' }, { status: 400 });
  }
  
  // 2. Check if nullifier hash already used (prevents double registration)
  const existing = await supabase
    .from('iris_verifications')
    .select('id')
    .eq('world_id_nullifier_hash', proof.nullifier_hash)
    .single();
  
  if (existing.data) {
    return Response.json({ 
      error: 'This World ID is already linked to another account' 
    }, { status: 409 });
  }
  
  // 3. Store verification
  await supabase
    .from('iris_verifications')
    .insert({
      creator_id,
      provider: 'worldcoin',
      world_id_nullifier_hash: proof.nullifier_hash,
      world_id_proof: proof,
      world_id_merkle_root: proof.merkle_root,
      status: 'verified',
      verified_at: new Date()
    });
  
  // 4. Update creator profile
  await supabase
    .from('creator_profiles')
    .update({
      iris_verified: true,
      iris_signature_hash: proof.nullifier_hash,
      iris_verified_at: new Date(),
      verification_level: 'iris_verified'
    })
    .eq('id', creator_id);
  
  return Response.json({ success: true });
}

3. Verification Check

// Check if creator is verified before allowing actions
async function requireIrisVerification(creatorId) {
  const { data: creator } = await supabase
    .from('creator_profiles')
    .select('iris_verified')
    .eq('id', creatorId)
    .single();
  
  if (!creator?.iris_verified) {
    throw new Error('Iris verification required');
  }
  
  return true;
}

Privacy Considerations

What We Store

DataStored?Purpose
Raw iris scan❌ Never-
Iris template❌ Never-
Nullifier hash✅ YesPrevent duplicate registrations
ZK proof✅ YesAudit trail
Verification timestamp✅ YesCompliance

What We DON’T Store

  • Biometric data (iris scans, templates)
  • Location of verification
  • Device details (beyond basic audit)

GDPR Compliance

// User can request deletion
async function handleDeletionRequest(creatorId) {
  // Remove verification record
  await supabase
    .from('iris_verifications')
    .delete()
    .eq('creator_id', creatorId);
  
  // Update profile
  await supabase
    .from('creator_profiles')
    .update({
      iris_verified: false,
      iris_signature_hash: null,
      iris_verified_at: null,
      verification_level: 'email_verified'
    })
    .eq('id', creatorId);
  
  // Note: Worldcoin handles their own deletion
  // We only delete our reference to their proof
}

Verification Status UI

function VerificationBadge({ creator }) {
  const levels = {
    unverified: { icon: '○', color: 'gray', label: 'Unverified' },
    email_verified: { icon: '◐', color: 'yellow', label: 'Email Verified' },
    id_verified: { icon: '◑', color: 'blue', label: 'ID Verified' },
    iris_verified: { icon: '●', color: 'green', label: 'Iris Verified' }
  };
  
  const level = levels[creator.verification_level] || levels.unverified;
  
  return (
    <span className={`badge badge-${level.color}`}>
      {level.icon} {level.label}
    </span>
  );
}

Fraud Prevention

Duplicate Detection

-- Prevent same World ID on multiple accounts
CREATE UNIQUE INDEX idx_iris_unique_worldid 
ON iris_verifications(world_id_nullifier_hash) 
WHERE status = 'verified';

Revocation

// Admin can revoke verification
async function revokeVerification(creatorId, reason) {
  await supabase
    .from('iris_verifications')
    .update({
      status: 'revoked',
      failure_reason: reason
    })
    .eq('creator_id', creatorId);
  
  await supabase
    .from('creator_profiles')
    .update({
      iris_verified: false,
      verification_level: 'email_verified'
    })
    .eq('id', creatorId);
  
  // Disable active licenses
  await supabase
    .from('creator_licenses')
    .update({ is_active: false })
    .eq('creator_id', creatorId);
}

Metrics

-- Verification funnel
SELECT 
  COUNT(*) FILTER (WHERE verification_level = 'unverified') as unverified,
  COUNT(*) FILTER (WHERE verification_level = 'email_verified') as email_verified,
  COUNT(*) FILTER (WHERE verification_level = 'id_verified') as id_verified,
  COUNT(*) FILTER (WHERE verification_level = 'iris_verified') as iris_verified
FROM creator_profiles;

-- Verification conversion rate
SELECT 
  COUNT(*) FILTER (WHERE status = 'verified') * 100.0 / COUNT(*) as conversion_rate
FROM iris_verifications;

-- Time to verify
SELECT 
  AVG(EXTRACT(EPOCH FROM (verified_at - initiated_at))) as avg_seconds
FROM iris_verifications
WHERE status = 'verified';

Worldcoin Setup

1. Create App

Go to https://developer.worldcoin.org and create an app.

2. Environment Variables

# .env.local
NEXT_PUBLIC_WORLDCOIN_APP_ID=app_xxxxxxxxxxxxx
WORLDCOIN_APP_ID=app_xxxxxxxxxxxxx

3. Install SDK

npm install @worldcoin/idkit

Alternative: Device Verification

For users without Orb access, offer device-based World ID (lower security):
<IDKitWidget
  credential_types={['orb', 'device']}  // Accept both
  // ...
>
Trade-off:
  • Device = Higher conversion, lower security
  • Orb only = Lower conversion, maximum security
Recommendation: Start with Orb-only for UCI, device for basic platform features.
DocumentWhat It Covers
uci-system.mdOverall UCI architecture
licensing-flow.mdHow licenses work
payouts.mdCreator payments
SCHEMA.mdcreator_profiles table