# Self Docs — full text > Self is a privacy-first, open-source identity protocol built on zero-knowledge proofs. This file concatenates every documentation page for LLM consumption. All new integrations must use Self Enterprise (`@selfxyz/enterprise-sdk`). The open-source Self Pass SDK (`@selfxyz/core` + `@selfxyz/qrcode`) is legacy and deprecated — do not use it for new integrations. --- # Welcome to Self Source: https://docs.self.xyz/docs/welcome/ Self is an open-source identity protocol built on zero-knowledge proofs. Your users prove facts about their government ID (age, nationality, sanctions status) without exposing personal data; they verify once in the Self app and can then disclose attributes to any integrated application. These docs cover **[Self Enterprise](/docs/self-enterprise/get-started/what-is-self-enterprise/)**, the managed way to use the protocol: configure a flow in the dashboard, call one SDK, and receive a signed result. :::cta{title="Start building" href="https://dashboard.self.xyz" button="Open the dashboard"} Sign up free and deploy your first verification flow in minutes. ::: ## The five workspaces Every verification starts by picking a workspace in the [dashboard](https://dashboard.self.xyz). Each answers one question about your user: | Workspace | The question it answers | Typical use | | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------ | | **[Pre-KYC](/docs/self-enterprise/workspaces/pre-kyc/)** | "Who is this user?" (genuine document, age, country rules, OFAC, optional data reveals) | Onboarding, compliance gating | | **[Age Verification](/docs/self-enterprise/workspaces/age-verification/)** | "Is this user old enough?" (a minimum age, nothing else revealed) | Age-gated content and commerce | | **[Proof of Human](/docs/self-enterprise/workspaces/proof-of-human/)** | "Is this a unique, real person?" (one person, one account) | Sybil resistance, anti-bot, airdrops | | **[Sovereign](/docs/self-enterprise/workspaces/sovereign/)** | "Is this user from a country I allow?" (a nationality allowlist) | Region-restricted access | | **[Custom Config](/docs/self-enterprise/workspaces/custom-config/)** | "Give me exactly these identity fields, under my custody" (early access, Enterprise plan) | Own-vault KYC, data residency | :::tip **New here?** Go from zero to a verified user in ten minutes with the **[Quickstart](/docs/self-enterprise/get-started/quickstart/)**. ::: ## Start with Self Enterprise | Page | What it covers | | ----------------------------------------------------------------------------------------- | --------------------------------------------------- | | **[What is Self Enterprise](/docs/self-enterprise/get-started/what-is-self-enterprise/)** | The overview, and who it's for. | | **[Quickstart](/docs/self-enterprise/get-started/quickstart/)** | Verified user in ten minutes. | | **[Core concepts](/docs/self-enterprise/get-started/concepts/)** | Orgs, flows, sessions, keys, webhooks. | | **[How verification works](/docs/self-enterprise/get-started/how-it-works/)** | The zero-knowledge model, in plain language. | | **[SDK](/docs/self-enterprise/sdk/nodejs/)** | The `@selfxyz/enterprise-sdk` client. | | **[Webhooks](/docs/self-enterprise/webhooks/overview/)** | Signed events, signature verification, idempotency. | ## Resources ::::cards :::card{title="Skills" href="/skills/" icon="sparkles"} Agent skills that teach AI coding tools how to build with Self. ::: :::card{title="MCP" href="/mcp/" icon="server"} Connect the Self MCP server to your editor or agent. ::: :::card{title="npm package" href="https://www.npmjs.com/package/@selfxyz/enterprise-sdk" icon="package"} Install the Enterprise SDK, the Node.js client for sessions and webhooks. ::: :::card{title="Interactive coverage map" href="https://map.self.xyz/" icon="map"} Explore which documents and countries are supported, worldwide. ::: :::: --- # What is Self Enterprise Source: https://docs.self.xyz/docs/self-enterprise/get-started/what-is-self-enterprise/ [Self Enterprise](https://dashboard.self.xyz) is the managed plane for zero-knowledge identity verification. You configure flows in a dashboard, call a single SDK, and receive signed webhook events when users verify. We handle credential issuance, proof verification, and the verification record. The integration is small: one `sessions.create(...)` call, a hosted page the user opens, and a webhook on your backend. ## What it replaces Self Enterprise is the managed offering on top of the open Self protocol. It bundles the parts you'd otherwise build and operate yourself: | Concern | Self SDK (Legacy) | Self Enterprise | | ----------------------------- | --------------------------------------------- | --------------------------------------------------- | | Configure a verification flow | Encode disclosures in your contract / backend | Configure in the dashboard, deploy a version | | Issue credentials to users | Run your own backend + hosted page | Hand the user a `verificationUrl` from the API | | Verify a proof | Run the verifier yourself (or on-chain) | We verify the proof; you get a signed webhook | | Store the result | Build your own store + audit log | Activity log in the dashboard, plus signed webhooks | If your team wants a fast path from "we want ZK identity verification" to a working integration, Enterprise is it. ## Who is it for? - **Consumer apps** running sybil resistance, age gates, or geographic compliance. Anyone who needs verified identity but doesn't want to build verification infrastructure. - **Fintech / regulated platforms** that need KYC-grade verification with auditable trails. - **Marketplaces** that want to verify both sides of a trade without holding PII. - **Internal compliance teams** at protocols who'd otherwise hand-roll a verifier. ## How it fits together ![How Self Enterprise fits together: you configure a flow in the dashboard, your backend creates a session, Self serves a hosted page, the user verifies in the Self app, and a signed webhook returns the result](/docs/docs-assets/enterprise-flow.svg) 1. **You configure a flow** on the Self dashboard, picking one of five workspaces: **Pre-KYC**, **Age Verification**, **Proof of Human**, **Sovereign**, or **Custom Config**. 2. **You call `sessions.create(...)`** via the Enterprise SDK when a user needs to verify. We return a `verificationUrl`. 3. **You send the user to that URL.** Self serves a hosted page with a QR code (or a deeplink button on mobile). 4. **The user verifies in the Self mobile app**, scanning the QR or tapping the link. The app produces a ZK proof of the requested attributes and submits it to Self. 5. **We verify the proof** and fire a signed webhook to your backend. You read the verified attributes and proceed. :::cta{title="Take the guided tour" href="/quickstart/" button="Guided tour"} Walk the end-to-end integration interactively, from sign-up to your first verified user. ::: --- # Quickstart Source: https://docs.self.xyz/docs/self-enterprise/get-started/quickstart/ End-to-end: from sign-up to a verified user, in about ten minutes. ## Prerequisites - A Self.xyz dashboard account. - The **Self mobile app** on your phone (iOS or Android). You'll verify with a [mock passport](/docs/self-enterprise/guides/using-mock-passports/) in step 8. - Node 20+ (for the SDK). - A way to receive a webhook locally, [ngrok](https://ngrok.com), Cloudflare Tunnel, or any public HTTPS endpoint. :::cta{href="https://dashboard.self.xyz" button="Open the dashboard"} Every step below starts here. Sign-up is free, no card required. ::: ## 1. Sign up Sign in with an email code or Google; any email address works. On first sign-in Self creates your organization for you; it owns flows, API keys, webhook subscriptions, and the billing relationship. You can rename it under **Settings → General** and invite teammates from **Settings → People**. ## 2. Create a flow ::video{id="R8xhtU802022rSV1OHcmq2bsmDVhUGx8HTcBLlUYMOcdg" title="Create and deploy a flow"} The dashboard home shows a card for each of the core [workspaces](/docs/self-enterprise/workspaces/): - **[Pre-KYC](/docs/self-enterprise/workspaces/pre-kyc/)**: identity attributes (document, age, country rules, OFAC, optional data reveals). - **[Age Verification](/docs/self-enterprise/workspaces/age-verification/)**: a minimum age (for example 18 or 21), nothing else. - **[Proof of Human](/docs/self-enterprise/workspaces/proof-of-human/)**: one person, one account. - **[Sovereign](/docs/self-enterprise/workspaces/sovereign/)**: a nationality allowlist. ([Custom Config](/docs/self-enterprise/workspaces/custom-config/), the fifth workspace, is in early access on the Enterprise plan and lives in the sidebar.) Most workspaces also offer a **Security level**, an **OFAC** toggle, and a [**Verification mode**](/docs/self-enterprise/flows/verification-modes/); keep the defaults for this walkthrough. Open a workspace and click **new config**. For this walkthrough, **Age Verification** is the quickest: set a **minimum age** and click **Deploy**. (The config name is edited inline in the page title, and the org name/icon live under **Settings → General**, see [Configure a workspace](/docs/self-enterprise/dashboard/configure-a-workspace/).) Once deployed, your configuration has a `flowId`, shown on the workspace's **Test** and **Live** tabs. Copy it. :::note A deployed config can't be edited: to change it, [archive it and create a new one](/docs/self-enterprise/flows/anatomy/#immutable-once-deployed). ::: > **Test vs. live:** the same `flowId` serves both environments: the API key decides. Sessions created with a test key (`sk_test_…`) accept mock passports and never bill credits. See [Test vs. live](/docs/self-enterprise/flows/test-vs-live/). ## 3. Create an API key Go to **Developer → API keys**, keep the **Test** tab selected, and click **Generate key**. The key (`sk_test_...`) is shown once, store it as `SELF_API_KEY` in your backend's secret manager. See [API keys](/docs/self-enterprise/dashboard/api-keys/). ::video{id="ADulhisGe8O018prz8ws00YS00aSOPDpCS7V7NPLuj39so" title="Generate a test API key"} ## 4. Install the SDK ```bash npm install @selfxyz/enterprise-sdk ``` ## 5. Create a verification session ```ts import { SelfClient } from '@selfxyz/enterprise-sdk'; const self = new SelfClient({ apiKey: process.env.SELF_API_KEY! }); const session = await self.sessions.create({ flowId: '', externalUuid: user.id, // your stable id for this user (any string up to 256 chars). }); console.log(session.verificationUrl); // hand this to the user. ``` The user opens `verificationUrl` in their Self app, produces a proof, and the app submits it back to us. ## 6. Add a webhook endpoint In **Developer → Webhooks**, select the **Test** tab (the endpoint inherits the environment from the active tab), click **Add webhook**, paste the full URL to your handler (e.g. `https:///webhooks/self`, including the path), and click **Save**. The endpoint saves right away and the dashboard reveals a signing secret (`whsec_...`) **once**, store it as `SELF_WEBHOOK_SECRET`. Every endpoint receives the `verification.completed` event. Once your handler (step 7) is deployed, you can hit **Send Test Request** on the endpoint to confirm it's reachable. See [Webhooks](/docs/self-enterprise/dashboard/webhooks/) for the full detail. ::video{id="Qbks00Q7k3Xrdjhf5ughCDQzjyKHAtxVrIYzFz89gHcU" title="Add a webhook endpoint"} ## 7. Verify webhook deliveries ```ts // app/api/webhooks/self/route.ts import { SelfWebhooks } from '@selfxyz/enterprise-sdk'; export async function POST(req: Request) { const raw = await req.text(); // raw body is required for signature verification. const headers = Object.fromEntries(req.headers); try { const event = SelfWebhooks.verify(raw, headers, process.env.SELF_WEBHOOK_SECRET!); if (event.type === 'verification.completed') { // event.verification_id, event.external_uuid, event.proof_attributes. console.log('verified:', event); } return new Response('ok', { status: 200 }); } catch (err) { return new Response('bad signature', { status: 400 }); } } ``` ## 8. Test the loop Create a [mock passport](/docs/self-enterprise/guides/using-mock-passports/) in the Self app (on the app's first screen, tap the **Passport** button five times), then open the `verificationUrl` from step 5, scan the QR code, and watch your webhook handler fire. The dashboard's **Activity log** tab on the flow shows the verification end-to-end. --- # Core concepts Source: https://docs.self.xyz/docs/self-enterprise/get-started/concepts/ A few objects, and you have the whole model. ## Organization Your team's shared space. It owns everything else: workspaces, flows, API keys, webhooks, members, and billing. Every account gets its own **default organization** on first sign-in, and a person belongs to **one organization at a time**: accepting an invite moves you into the inviting org, and [leaving one](/docs/self-enterprise/dashboard/people/#leave-an-organization) returns you to your default org. Each member has a role, **owner**, **admin**, or **member**, which decides who can manage keys, webhooks, and billing. See [People](/docs/self-enterprise/dashboard/people/). ## Workspace The kind of verification you run: **[Pre-KYC](/docs/self-enterprise/workspaces/pre-kyc/)**, **[Age Verification](/docs/self-enterprise/workspaces/age-verification/)**, **[Proof of Human](/docs/self-enterprise/workspaces/proof-of-human/)**, **[Sovereign](/docs/self-enterprise/workspaces/sovereign/)**, or **[Custom Config](/docs/self-enterprise/workspaces/custom-config/)**. The workspace decides what the user proves and which rules you can set. See the [workspaces overview](/docs/self-enterprise/workspaces/). ## Flow A deployed, versioned configuration of one workspace (the dashboard calls it a _config_; the SDK calls it a _flow_ and identifies it by `flowId`). It pins the workspace, its rules, and its [verification mode](/docs/self-enterprise/flows/verification-modes/) (Backend by default, or On-chain). Once deployed it's immutable, to change it you archive it and create a new one. You pass its `flowId` to `sessions.create(...)`. See [Anatomy of a flow](/docs/self-enterprise/flows/anatomy/). ## Session One verification attempt: one user, one flow. Your backend creates it with `sessions.create(...)`, hands the returned `verificationUrl` to the user, and the session lives until they verify or it expires. ![Session lifecycle: a pending session completes as valid, invalid, error, or expired](/docs/docs-assets/session-lifecycle.svg) On any terminal status we record it in your [activity log](/docs/self-enterprise/dashboard/activity-log/) and fire the `verification.completed` webhook. Each session has a credit cost, fixed when it's created and charged when the verification completes (test sessions are never billed). ## API key A bearer secret the SDK uses, scoped to one org and one environment: - `sk_test_…`: test flows, mock passports, never billed. - `sk_live_…`: production, real proofs, real credits. Test and live are fully isolated; each key is bound to one environment (the prefix reflects which). You generate keys under **Developer → API keys**. See [API keys](/docs/self-enterprise/dashboard/api-keys/). ## Webhook endpoint An HTTPS URL you register under **Developer → Webhooks** to receive signed events like `verification.completed`. We deliver with automatic retries; verify the signature with the SDK. See the [event catalog](/docs/self-enterprise/webhooks/events/). ## Related - [How verification works](/docs/self-enterprise/get-started/how-it-works/): the zero-knowledge model underneath. - [Anatomy of a flow](/docs/self-enterprise/flows/anatomy/): rules, documents, and versions in detail. - [Billing](/docs/self-enterprise/billing/credits-and-usage/): credits and metering. --- # How verification works Source: https://docs.self.xyz/docs/self-enterprise/get-started/how-it-works/ You never have to touch the cryptography, the SDK and webhooks hide it. But the model is worth a minute, especially for reasoning about privacy. For the full cryptographic detail, see the [Self Pass protocol docs](/docs/self-pass/architecture/zk-proof-architecture/). ## The idea in one line The user's phone proves a **fact** about their government ID ("over 18", "not sanctioned"), and Self checks that proof. You get the fact, never the underlying data. ## The flow ![The zero-knowledge flow: the Self app reads the ID chip and builds a proof on the device, only the proof leaves the phone, Self verifies it, and your backend receives the fact via webhook](/docs/docs-assets/zk-proof-flow.svg) 1. The user reads their ID chip with the Self app, **once**. The credential then lives on their device. 2. For each verification, the app builds a fresh **zero-knowledge proof** of exactly what your flow asks, nothing more. 3. Self verifies the proof and sends your backend the result. The raw ID data never leaves the phone. :::note The document is signed by the issuing government. Self maintains the certificate registry that makes a proof trustworthy, so a forged or unsupported document simply fails. See [supported countries](/docs/self-enterprise/reference/supported-countries/). ::: ## What you get vs. what you never see | You receive | You never see | | ---------------------------------------------------------------------------- | --------------------------- | | A pass/fail outcome for the predicates your flow checks (age, OFAC, country) | Date of birth | | Any explicit reveal you asked for | Name | | A stable per-user nullifier for uniqueness (scoped to your org) | Passport or document number | | The document type used | The chip data or biometrics | | A verification ID for your audit log | Anything you didn't ask for | A `valid` status means every predicate your flow asked for passed; the webhook's `proof_attributes` echoes which predicates were checked (it is the flow's predicate config, not the underlying values). This is the whole privacy guarantee: **the proof attests to exactly what your flow asks, and nothing else leaks.** See [Disclosures](/docs/self-enterprise/flows/disclosures/#what-is-never-disclosed). ## One account per person (nullifiers) Need to stop duplicates? Each verification carries a **nullifier**: a value derived from the document that is the same for the same person **within your organization** (so you can catch repeats, including across different workspaces and flows in the same org), reveals nothing about who they are, and is different and unlinkable in **every other organization** (so users can't be tracked across services). The nullifier is scoped to your organization, not to an individual workspace. ## What your users see The `verificationUrl` you get from `sessions.create(...)` opens Self's **hosted verification page**. You don't build or style it, but it's worth knowing what your users experience: ![Self hosted verification page](/docs/docs-assets/hosted-page.avif) - **Branding.** The page heading adapts to the workspace (for example "Age Verification — Verify your age with Self", "Proof of Humanity Verification"). Your organization's icon (set under **Settings → General**) appears in the QR code and is carried into the Self app together with your configuration's name, so the experience feels like yours. - **Desktop vs. mobile.** On desktop the page shows a **QR code** to scan with the Self app. On mobile it shows an **Open Self app** button (a deeplink), plus the iOS/Android download links for users who don't have the app yet. - **Returning the user.** After verifying in the app, the user is sent back to the web flow and the page polls for the result, then redirects. If you passed `successUrl` / `failureUrl` to `sessions.create(...)`, the user lands there (with a `?userId=` query parameter appended); otherwise the page shows a built-in **success** or **failure** result screen. - **On-chain flows.** For an [on-chain flow](/docs/self-enterprise/flows/onchain-verification/), the page first asks the user to connect the wallet that will receive their soulbound token and sign a gas-free message, then shows the QR code as usual. Treat the redirect as a UX convenience, not a trust signal. The authoritative result is the signed `verification.completed` **webhook**, verify that before granting access. ## Going deeper The circuits, the commitment scheme, the certificate trees, and the on-chain verification hub are documented in the legacy Self Pass section: - [ZK Proof Architecture](/docs/self-pass/architecture/zk-proof-architecture/) - [Verification in the IdentityVerificationHub](/docs/self-pass/architecture/verification-hub/) - [OFAC & CSCA Auto-Updaters](/docs/self-pass/architecture/ofac-csca-auto-updaters/) --- # Workspaces overview Source: https://docs.self.xyz/docs/self-enterprise/workspaces/ Every verification starts by picking a **workspace** in the [dashboard](https://dashboard.self.xyz). A workspace is the kind of question you ask about a user; the flow you configure decides the exact rules. There are five: | Workspace | The question it answers | Cost | | -------------------------------------------------------------------------- | --------------------------------------------------------- | ---------- | | **[Pre-KYC](/docs/self-enterprise/workspaces/pre-kyc/)** | "Who is this user?" (identity rules, plus data reveals) | 25 credits | | **[Age Verification](/docs/self-enterprise/workspaces/age-verification/)** | "Is this user old enough?" | 10 credits | | **[Proof of Human](/docs/self-enterprise/workspaces/proof-of-human/)** | "Is this a unique, real person?" | 10 credits | | **[Sovereign](/docs/self-enterprise/workspaces/sovereign/)** | "Is this user from a country I allow?" | 10 credits | | **[Custom Config](/docs/self-enterprise/workspaces/custom-config/)** | "Give me exactly these identity fields, under my custody" | 25 credits | ## How to choose Ask what your product decision actually depends on, and pick the narrowest workspace that answers it. Narrower workspaces disclose less about your users and cost less: - You need **identity attributes** (name, date of birth, document details) or several rules at once → **[Pre-KYC](/docs/self-enterprise/workspaces/pre-kyc/)**. It's the only workspace with data reveals. - You only need an **age floor** → **[Age Verification](/docs/self-enterprise/workspaces/age-verification/)**. Nothing else is asked or revealed. - You need **one account per person** → **[Proof of Human](/docs/self-enterprise/workspaces/proof-of-human/)**. It's built around the [nullifier](/docs/self-enterprise/get-started/how-it-works/#one-account-per-person-nullifiers). - You need to restrict access to **specific nationalities** → **[Sovereign](/docs/self-enterprise/workspaces/sovereign/)**. The allowlist is the whole workspace. - You need the **raw identity data itself, stored by you and not by Self** → **[Custom Config](/docs/self-enterprise/workspaces/custom-config/)** (early access, Enterprise plan). Your organization takes custody of the disclosed fields; Self keeps nothing. ## What they all share Whatever the workspace, the mechanics are identical: - **The same integration.** One `sessions.create(...)` call, a hosted page the user opens, and a signed `verification.completed` webhook. Switching workspaces later is a config change, not a code change. - **The same privacy model.** The user proves facts with a zero-knowledge proof; you receive pass or fail results (plus any fields you explicitly requested), never the underlying document. See [How verification works](/docs/self-enterprise/get-started/how-it-works/). Custom Config goes one step further: the disclosed data is delivered **only to you**, and Self stores none of it. - **A security level.** Every workspace lets you choose **Standard** (the document is genuine) or **Hi-security** (the user physically scanned the document's chip, the default). See [Disclosures](/docs/self-enterprise/flows/disclosures/#security-level). - **Both [verification modes](/docs/self-enterprise/flows/verification-modes/)**: Backend (the default) or On-chain. The exception is Custom Config, which is backend-only. - **Test and live environments**, mock passports, and the same [per-verification pricing model](/docs/self-enterprise/billing/credits-and-usage/). A workspace holds **one active configuration at a time**, and a deployed configuration is immutable: to change it, archive it and create a new one. The exception is Custom Config, where an organization runs **many active configs side by side**. See [Anatomy of a flow](/docs/self-enterprise/flows/anatomy/). :::cta{title="Found your workspace?" href="https://dashboard.self.xyz" button="Create it in the dashboard"} Deploy a configuration and test it with a mock passport before writing any code. ::: ## Related - [Configure a workspace](/docs/self-enterprise/dashboard/configure-a-workspace/): building the flow in the dashboard. - [Quickstart](/docs/self-enterprise/get-started/quickstart/): from sign-up to a verified user in ten minutes. - [Credits and usage](/docs/self-enterprise/billing/credits-and-usage/): how the costs above are charged. --- # Pre-KYC Source: https://docs.self.xyz/docs/self-enterprise/workspaces/pre-kyc/ **Pre-KYC** answers "**Who is this user?**". It verifies the user holds a genuine government document and, optionally, that they meet an age floor, come from an allowed country, and clear the OFAC sanctions list. It's the richest workspace: the only one where you can also ask the user to **reveal** document fields. Use it when you need identity-grade assurance before (or instead of) a full KYC process (hence the name). ## What the user proves - They hold a **genuine, government-signed document** (at your chosen [security level](/docs/self-enterprise/flows/disclosures/#security-level)). - Optionally, that they meet a **minimum age**, without revealing their birth date. - Optionally, that their document's country is **not on your denylist** (country stays hidden) or **is on your allowlist** (nationality is revealed). - Optionally, that they **clear the OFAC sanctions list**. ## What you receive - A `valid` / `invalid` result via the [`verification.completed` webhook](/docs/self-enterprise/webhooks/events/), with the enforced rules echoed in `proof_attributes`. - A [nullifier](/docs/self-enterprise/get-started/how-it-works/#one-account-per-person-nullifiers) for duplicate detection. - The values of any **Additional data** reveals you enabled. Pre-KYC is the only workspace with reveals: `Full name`, `ID number`, `Date of birth`, `Gender`, `Nationality`, `Expiration date`, `Issuing state` Each reveal is off by default. A reveal returns real personal data, so request only what you need and handle it accordingly. See [Disclosures](/docs/self-enterprise/flows/disclosures/#additional-data-reveals). For example, a `valid` webhook for a flow with an 18+ floor, a country denylist, OFAC on, and the **Full name** and **Nationality** reveals enabled carries: ```json { "minimumAge": 18, "excludedCountries": ["PRK", "IRN"], "ofac": true, "name": "JANE DOE", "nationality": "USA" } ``` The first three keys echo the rules that were enforced (the pass/fail answer is the event's `status`); `name` and `nationality` are reveal values, present only on `valid` and only for fields you enabled. ## Rules you can configure | Rule | Default | Notes | | ------------------ | ----------- | ---------------------------------------------------------------------------------- | | Security level | Hi-security | Standard also accepts non-chip documents like KYC attestations. | | Minimum age | 18 | 13–110. Pass/fail only. The birth date stays hidden. | | Excluded countries | — | Denylist. The user's country is never revealed. | | Included countries | — | Allowlist, mutually exclusive with the denylist. Forces the Nationality reveal on. | | OFAC | On | Checked against the daily-updated US Treasury list. | | Additional data | All off | The seven reveals above. | ## Use cases - **Fintech and regulated onboarding**: screen users (age, geography, sanctions) before running an expensive full-KYC provider, or replace one where attribute-level verification is enough. - **Marketplaces**: verify both sides of a trade without storing PII yourself. - **Compliance gating**: block sanctioned or restricted-region users at signup with an auditable record. - **Account recovery / high-trust actions**: re-verify a user's identity attributes before sensitive changes. ## Cost **25 credits** per completed verification (tied with Custom Config for the highest), reflecting the document-level assurance and reveals. See [Credits and usage](/docs/self-enterprise/billing/credits-and-usage/). --- # Age Verification Source: https://docs.self.xyz/docs/self-enterprise/workspaces/age-verification/ **Age Verification** answers "**Is this user old enough?**". The user proves they meet the minimum age you set, backed by a genuine government document, and you never see their date of birth. Nothing else is asked, and nothing else is revealed. It's the narrowest, fastest workspace to configure: set one number and deploy. ## What the user proves - They hold a **genuine, government-signed document** (at your chosen [security level](/docs/self-enterprise/flows/disclosures/#security-level)). - They are **at least the age you set**, a pass/fail fact computed on their device from the document's birth date, which never leaves the phone. - Optionally, that they **clear the OFAC sanctions list**. ## What you receive - A `valid` / `invalid` result via the [`verification.completed` webhook](/docs/self-enterprise/webhooks/events/): `valid` means "old enough", `invalid` means a rule failed. - The enforced rules echoed in `proof_attributes` (for example `{ "minimumAge": 21, "ofac": true }`). - A [nullifier](/docs/self-enterprise/get-started/how-it-works/#one-account-per-person-nullifiers) for duplicate detection. For example, a flow with a 21+ floor and OFAC on carries: ```json { "minimumAge": 21, "ofac": true } ``` That's the whole object: the rules that were enforced, echoed back. The pass/fail answer is the event's `status`, and no personal data appears. No name, no birth date, no document number. An age gate that holds zero personal data is the point. ## Rules you can configure | Rule | Default | Notes | | -------------- | ----------- | ---------------------------------------------------------------- | | Security level | Hi-security | Standard also accepts non-chip documents. | | Minimum age | 21 | 13–110. Pick the threshold your regulation requires (18, 21, …). | | OFAC | On | Checked against the daily-updated US Treasury list. | No country rules and no data reveals. If you need those, use [Pre-KYC](/docs/self-enterprise/workspaces/pre-kyc/). ## Use cases - **Age-restricted commerce**: alcohol, tobacco, vaping, CBD. Prove 18+/21+ at checkout without collecting IDs. - **Age-gated content and platforms**: adult content, gambling, and social features with regulatory age floors. - **Regulatory compliance**: meet age-assurance rules (e.g. for app stores or regional online-safety laws) with a documented, privacy-preserving check instead of a self-declared checkbox. - **Events and communities**: gate registration to adults without storing anyone's birth date. ## Cost **10 credits** per completed verification. See [Credits and usage](/docs/self-enterprise/billing/credits-and-usage/). --- # Proof of Human Source: https://docs.self.xyz/docs/self-enterprise/workspaces/proof-of-human/ **Proof of Human** answers "**Is this a unique, real person?**". The user proves they hold a genuine government document, and the proof carries a **nullifier** that is the same every time the same person verifies with you. One person, one account, enforced cryptographically instead of with CAPTCHAs and phone numbers. ## What the user proves - They hold a **genuine, government-signed document** (at your chosen [security level](/docs/self-enterprise/flows/disclosures/#security-level)), so they're a real person, not a bot or a burner identity. - Optionally, that they **clear the OFAC sanctions list**. ## What you receive - A `valid` / `invalid` result via the [`verification.completed` webhook](/docs/self-enterprise/webhooks/events/). - The **`nullifier`**: the heart of this workspace. It's derived from the document, **stable per person within your organization** (the same across all your workspaces and flows), and **unlinkable across organizations**, so users can't be tracked between services. Store it with a uniqueness constraint and duplicates reject themselves: ```ts // on `verification.completed` with status 'valid': await db.query( 'INSERT INTO verified_humans (nullifier, user_id) VALUES ($1, $2) ON CONFLICT (nullifier) DO NOTHING', [event.nullifier, event.external_uuid], ); ``` No name, no nationality, no document details. You learn that the person is real and unique, and nothing else. See [How verification works](/docs/self-enterprise/get-started/how-it-works/#one-account-per-person-nullifiers). The webhook's `proof_attributes` is minimal, just the enforced rules (uniqueness itself arrives as the separate `nullifier` field): ```json { "ofac": true } ``` ## Rules you can configure | Rule | Default | Notes | | -------------- | ----------- | --------------------------------------------------- | | Security level | Hi-security | Standard also accepts non-chip documents. | | OFAC | On | Checked against the daily-updated US Treasury list. | No age or country rules and no reveals: uniqueness is the workspace. If you need more, use [Pre-KYC](/docs/self-enterprise/workspaces/pre-kyc/). ## Use cases - **Sybil resistance for airdrops and rewards**: one claim per human, not per wallet. The [on-chain mode](/docs/self-enterprise/flows/onchain-verification/) is a natural fit: each verified person gets a soulbound token, and its contract enforces one token per person. - **Anti-bot for signups and communities**: keep automated accounts out of launches, waitlists, and forums without punishing real users with friction. - **One-person-one-vote**: governance, polls, and review systems where duplicate identities break the mechanism. - **Fair usage limits**: free tiers, promotions, and referral programs that can't be farmed with throwaway accounts. ## Cost **10 credits** per completed verification. See [Credits and usage](/docs/self-enterprise/billing/credits-and-usage/). --- # Sovereign Source: https://docs.self.xyz/docs/self-enterprise/workspaces/sovereign/ **Sovereign** answers "**Is this user from a country I allow?**". The user proves they hold a genuine government document from a nationality on your allowlist. The allowlist is the entire configuration: there are no age or OFAC rules alongside it. Where [Pre-KYC](/docs/self-enterprise/workspaces/pre-kyc/)'s country rules are one option among many, Sovereign is purpose-built for the "citizens of these countries only" case. ## What the user proves - They hold a **genuine, government-signed document** (at your chosen [security level](/docs/self-enterprise/flows/disclosures/#security-level)). - Their **nationality is on your allowlist**. ## What you receive - A `valid` / `invalid` result via the [`verification.completed` webhook](/docs/self-enterprise/webhooks/events/). - The user's **nationality**. Because the allowlist is checked against the holder's disclosed nationality, every Sovereign flow reveals it. This is the one predicate that implies a reveal. See [Disclosures](/docs/self-enterprise/flows/disclosures/#country-predicate). - A [nullifier](/docs/self-enterprise/get-started/how-it-works/#one-account-per-person-nullifiers) for duplicate detection. For example, a `valid` webhook for an EU-style allowlist carries: ```json { "includedCountries": ["AUT", "BEL", "DEU"], "nationality": "AUT" } ``` The allowlist echoes the rule that was enforced; `nationality` is the disclosed value it was checked against, present on `valid`. ## Rules you can configure | Rule | Default | Notes | | ------------------ | ----------- | -------------------------------------------------------------------------------------------------------- | | Security level | Hi-security | Standard also accepts non-chip documents. | | Included countries | — | Required, non-empty. ISO 3166-1 alpha-3 codes, with region presets (EU, APAC, LATAM, GCC) in the picker. | That's the whole workspace: no OFAC, no age, no denylist, no reveals beyond the implied nationality. On-chain Sovereign flows cap the allowlist at **41 countries**. See [Configuration limits](/docs/self-enterprise/flows/onchain-verification/#configuration-limits). :::note The allowlist needs the document to carry a verifiable nationality, so users verify with a document type that supports country rules. See [Supported documents](/docs/self-enterprise/flows/supported-documents/#which-document-satisfies-which-rule). ::: ## Use cases - **National or regional services**: products for citizens of one country or bloc, such as government-adjacent services, national loyalty programs, domestic-only platforms. - **Regulatory market restrictions**: offerings only available to residents of licensed jurisdictions (trading platforms, regulated goods), enforced with proof instead of an IP check. - **Diaspora and community access**: communities, discounts, or benefits reserved for holders of a specific nationality. - **Geo-fenced launches**: restrict a rollout to specific markets with document-grade assurance a VPN can't bypass. ## Cost **10 credits** per completed verification. See [Credits and usage](/docs/self-enterprise/billing/credits-and-usage/). --- # Custom Config Source: https://docs.self.xyz/docs/self-enterprise/workspaces/custom-config/ **Custom Config** answers "**give me exactly these identity fields, under my custody**". You pick which document fields the user discloses, and your organization becomes the store of record: the verified result and every disclosed value are delivered to your webhook endpoint, and **Self never stores any of it**. Where the other workspaces answer a yes/no question and Self retains the proof, **Custom Config is built for teams with their own compliance or data-residency requirements** who need the raw data and don't want a copy left behind. :::cta{title="Enterprise only" href="mailto:sales@self.xyz" button="Contact sales"} Custom Config is in early access and requires an Enterprise plan. Disclosed data is delivered directly to your server; Self stores none of it. ::: ## How custody changes the deal - **Self keeps nothing.** The result and the disclosed fields are never written to Self's storage. Once delivered, no copy exists anywhere on Self's side. - **Your server holds the only copy.** Self sends each result to your [webhook endpoint](/docs/self-enterprise/dashboard/webhooks/) once, and that delivery is the sole permanent record. Save what you need when it arrives; there is nothing to fetch from Self later. - **A live webhook endpoint is mandatory.** You can't deploy a Custom Config or start live verifications until your organization has one set up, otherwise users would verify into the void. Test verifications are exempt, so you can build your receiver first. - **You receive the proof on every outcome.** Even when a verification fails or expires, whatever proof material exists is delivered to you rather than kept by Self. For the exact payload fields and error codes behind this (`storage_state: 'skipped'`, the `409 no_webhook_endpoint` rejection), see the [event catalog](/docs/self-enterprise/webhooks/events/) and [error handling](/docs/self-enterprise/sdk/error-handling/). ## What the user proves - They hold a **genuine, government-signed document** (at your chosen [security level](/docs/self-enterprise/flows/disclosures/#security-level)). - Optionally, that they **clear the OFAC sanctions list**. ## What you receive - A `valid` / `invalid` result via the [`verification.completed` webhook](/docs/self-enterprise/webhooks/events/), with `product: 'custom_config'` for routing on a shared endpoint. - **Every disclosed field you requested**, in `proof_attributes` on a `valid` event, using the same reveal mechanism as [Pre-KYC's Additional data](/docs/self-enterprise/flows/disclosures/#additional-data-reveals). - The **raw proof envelope** in `proof`, on every status. - A [nullifier](/docs/self-enterprise/get-started/how-it-works/#one-account-per-person-nullifiers) for duplicate detection. For example, a `valid` webhook for a config that requests **all seven fields** with OFAC on carries: ```json { "ofac": true, "name": "JANE DOE", "idNumber": "L898902C3", "dateOfBirth": "900115", "gender": "F", "nationality": "USA", "expiryDate": "300630", "issuingState": "USA" } ``` `ofac` echoes the enforced rule; the rest is one key per enabled disclosure, present only on `valid` (fields the document didn't carry are dropped). Values arrive as they appear on the document, so passport dates use the MRZ `YYMMDD` format. ## Rules you can configure | Rule | Default | Notes | | ---------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------- | | Disclosed fields | — | Required, at least one: `Full name`, `ID number`, `Date of birth`, `Gender`, `Nationality`, `Expiration date`, `Issuing state`. | | OFAC check | on | Match against the US Treasury OFAC sanctions list; only pass/fail is disclosed. | | Security level | Hi-security | Standard also accepts non-chip documents. | Two more ways Custom Config differs from the other workspaces: - **Backend mode only.** On-chain verification can't deliver disclosed data to your webhook, so it's rejected at deploy. See [Verification modes](/docs/self-enterprise/flows/verification-modes/). - **Many active configs.** The other workspaces keep one active configuration at a time; a Custom Config workspace runs as many as you need side by side, each with its own `flowId`. The dashboard lists them with per-config **Activity log** and **Settings** actions. ## Use cases - **Regulated onboarding with your own KYC vault**: collect name, date of birth, and document number straight into your compliance systems, with document-grade assurance and no third-party copy. - **Data-residency requirements**: keep disclosed PII inside your own infrastructure and jurisdiction from the moment it exists. - **Custom risk models**: feed exact document fields into your own scoring instead of consuming pass/fail answers. ## Cost **25 credits** per completed verification. See [Credits and usage](/docs/self-enterprise/billing/credits-and-usage/). --- # Dashboard overview Source: https://docs.self.xyz/docs/self-enterprise/dashboard/overview/ The [dashboard](https://dashboard.self.xyz) is where you configure workspaces, manage keys, watch traffic, and pay for what you use. This page is a map. ![Dashboard home](/docs/docs-assets/dashboard-home.avif) ## Home The landing surface. It greets you, surfaces a low-credit alert when your balance runs low, and shows a card for each of the core workspaces (Pre-KYC, Age Verification, Proof of Human, Sovereign) alongside a **Resources** row: **Developer Docs**, **Mobile SDK**, and **Status Page**. ## Workspaces Each workspace has its own page and holds **one active configuration at a time** ([Custom Config](/docs/self-enterprise/workspaces/custom-config/) is the exception: its page lists many active configurations). The page splits into tabs: - [Configure](/docs/self-enterprise/dashboard/configure-a-workspace/): the [verification mode](/docs/self-enterprise/flows/verification-modes/), disclosure rules, and (for Pre-KYC) additional data. - **Test** and **Live**: the per-environment integration tabs. Each shows the configuration's `flowId`, SDK snippets, and a link to your [API keys](/docs/self-enterprise/dashboard/api-keys/); for on-chain configurations, also that environment's [contract card](/docs/self-enterprise/flows/onchain-verification/#the-contract). They unlock once the configuration is deployed. - [Activity log](/docs/self-enterprise/dashboard/activity-log/): verifications and errors over time. - **Configuration settings**: who created the configuration and when, a usage and credit snapshot, and the control to archive it. ## Archive Where a workspace's previous configurations go once you replace them. Archived configurations have their proof requests **deactivated**, but their activity log stays viewable for audit. ## Developer Org-scoped developer surfaces, in the sidebar under **Developer**: - [**Webhooks**](/docs/self-enterprise/dashboard/webhooks/): register public HTTPS endpoints and manage signing secrets. - [**API keys**](/docs/self-enterprise/dashboard/api-keys/): generate and revoke the `sk_test_…` / `sk_live_…` keys your backend uses. ## Settings Org-wide configuration, organized into tabs: - **General**: organization name and icon, your profile, theme, sign out, and [leaving the organization](/docs/self-enterprise/dashboard/people/#leave-an-organization). - **Notifications**: which emails Self sends you. Two toggles, **Product tips** (setup and integration nudges) and **Product updates** (new features and announcements), both per person rather than per organization. Billing emails, credits, payments, and subscription changes, are always sent and have no toggle. Product emails also carry an unsubscribe link, which switches Product tips off without a login. - [**Usage & Billing**](/docs/self-enterprise/dashboard/billing/): plan, credit balance, and usage. - **Audit**: audit streaming is **coming soon**; this tab shows its status until it ships. - [**People**](/docs/self-enterprise/dashboard/people/): members and invites. Visible only to owners and admins. - **System Status**: live service status. --- # Configure a workspace Source: https://docs.self.xyz/docs/self-enterprise/dashboard/configure-a-workspace/ A configuration always starts by choosing one of the five workspaces. The workspace decides which rules you can set: the matrix under [Disclosure rules](#disclosure-rules) shows exactly which rules each one carries, and the [workspaces overview](/docs/self-enterprise/workspaces/) covers what each workspace verifies and when to use it. You then build the configuration in the **Configure** tab. ![New configuration in a workspace](/docs/docs-assets/new-config.avif) ## The Configure tab The Configure tab has its cards on the left. On the right, a live **proof-request preview** shows what the user will see, and a **credit usage** callout shows what each verification will cost. Once the configuration is deployed, a **Try this flow** callout also appears. ### Organization details This card is read-only. It shows your **Organization name** and **Organization icon** (the icon shown on the proof request and the hosted verification page) with a **Manage organization** link into **Settings → General**. The **configuration name** is set inline in the page title at the top of the Configure tab, not in this card. ### Verification mode Every workspace offers two [verification modes](/docs/self-enterprise/flows/verification-modes/): - **Backend** (the default): proofs are verified by Self's servers; results arrive through webhooks and the API. - **On-chain**: proofs are verified on Celo, and each verified user receives a soulbound token (SBT). Deploying also [deploys the flow's contracts](/docs/self-enterprise/flows/onchain-verification/) to both Celo networks. Like every other setting, the mode is frozen at deploy. Keep **Backend** unless other smart contracts need to read your verification results. **Custom Config is backend-only**: its results exist solely as webhook deliveries, which the chain can't produce. ### Disclosure rules The predicates the user must satisfy. The user proves each one without revealing the underlying value. Which rules a configuration can carry depends on its workspace: | Rule | Pre-KYC | Age Verification | Proof of Human | Sovereign | Custom Config | | ------------------ | ------------------------------------------------------- | ------------------------------- | ------------------------------- | ------------------------------------------------------- | ------------------------------------------------------- | | Security level | | | | | | | OFAC check | | | | | | | Minimum age | | | | | | | Excluded countries | | | | | | | Included countries | optional | | | required | | | Data reveals | optional | | | | required | What each rule means: - **Security level**: **Standard** verifies the document is genuine; **Hi-security** (the default) also verifies the user physically scanned the document's chip. - **OFAC check** (on by default): match against the US Treasury OFAC sanctions list, which Self keeps updated daily. Only the pass or fail result is disclosed. - **Minimum age**: the age threshold (13–110, default 18 for Pre-KYC and 21 for Age Verification). Only the pass or fail result is disclosed, never the date of birth. - **Excluded countries**: documents issued by a country on this list fail. Only the pass or fail result is disclosed, not the user's country. - **Included countries**: an allowlist, only holders from a country on this list pass. In Pre-KYC it's mutually exclusive with the excluded list (setting one clears and disables the other); in Sovereign it's the workspace's core rule and can't be empty. The list is enforced against the holder's **disclosed nationality**, so it **forces the Nationality reveal on** and discloses the user's nationality to you. Country lists use ISO 3166-1 alpha-3 codes, and the picker offers **region presets** (EU, APAC, LATAM, GCC) that add or remove a whole bloc at once. On-chain configurations cap the lists at **40 excluded** or **41 included** countries; the picker enforces this as you select. ### Additional data (Pre-KYC, Custom Config) For **Pre-KYC** and **Custom Config**, beyond the pass or fail rules, you can ask the user to disclose specific document fields. Each is an explicit reveal, **off by default**, request only what you need: `Full name`, `ID number`, `Date of birth`, `Gender`, `Nationality`, `Expiration date`, `Issuing state`. In Pre-KYC, when an **Included countries** rule is set, the **Nationality** toggle is forced on and locked, because the allowlist is checked against the disclosed nationality. In Custom Config the disclosures **are** the configuration: at least one field must be on, and a custody banner above the cards reminds you that the disclosed values go straight to your webhook and are never stored by Self. ![Deployed Pre-KYC configuration](/docs/docs-assets/published-kyc-product.avif) ## Deploy Click **Deploy** to take the configuration live; the dashboard validates it first (**Reset parameters** clears your edits). Once deployed, the **Test** and **Live** tabs show its `flowId` and SDK snippets. A deployed configuration is [immutable](/docs/self-enterprise/flows/anatomy/#immutable-once-deployed): to change anything, archive it and create a new one. In-flight sessions keep the version they were created against, so archiving never breaks an open session. Two workspace-specific notes: - **On-chain**: the status pill tracks the contract deploys (**Deploying → Deployed**), and a failed deploy can be retried from its environment tab. See [Deploy status](/docs/self-enterprise/flows/onchain-verification/#deploy-status). - **Custom Config**: Deploy stays disabled until your organization has a live [webhook endpoint](/docs/self-enterprise/dashboard/webhooks/), since results are delivered only there. It's also the one workspace that holds **many active configurations**, shown as a list; each row has an **Activity log** shortcut and a gear to the config's **Settings** tab, where renaming and archiving live. ## Related - [Disclosures](/docs/self-enterprise/flows/disclosures/): what each rule and reveal proves. - [Supported documents](/docs/self-enterprise/flows/supported-documents/): which documents work where. - [API keys](/docs/self-enterprise/dashboard/api-keys/): generate the keys your backend uses. --- # Activity log Source: https://docs.self.xyz/docs/self-enterprise/dashboard/activity-log/ The **Activity log** tab on a workspace shows the verifications that ran against its active configuration. It's read-only, fed live from your verification records. ![Activity log](/docs/docs-assets/activity-log.avif) It's empty until the configuration is deployed and traffic starts arriving. ## What you see Finished verifications, grouped into two collapsible sections, **Passed** and **Failed**. A `valid` result lands in **Passed**; `invalid` and `error` land in **Failed**. Sessions that are still pending, and sessions the user never finished, aren't listed. Your webhook still receives their events, and [`sessions.get(...)`](/docs/self-enterprise/sdk/nodejs/) still reports their status. Each row shows, in order: | Field | Meaning | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Document | The document type from the verification's metadata (e.g. passport); shows `Document` when none is reported. | | Security | The tier of the document actually presented: **High security** (chip-backed) or **Standard**. Shows a dash when the proof never verified. | | Date | When the verification finished. | | Environment | A **Test** or **Production** badge. | | Verification ID | The verification's identifier. Click it to copy. | For an [on-chain flow](/docs/self-enterprise/flows/onchain-verification/), rows also show a **Transaction** column: the mint transaction as a shortened hash linked to the block explorer (a dash for verifications that never reached a transaction). For a [Custom Config](/docs/self-enterprise/workspaces/custom-config/), rows omit the document and security cells, since under org custody Self retains neither. Click a row to open a popover with the proof-request details. ## What you won't see - Personal data the user did not disclose. If your flow asks for an age threshold, you see the pass or fail result, not the date of birth. - The raw document or proof. Self verifies it; you never see the underlying scan. --- # API keys Source: https://docs.self.xyz/docs/self-enterprise/dashboard/api-keys/ API keys authenticate the SDK. They're org-scoped (not tied to a workspace), and you generate them on the **Developer → API keys** page. ![Generate a test API key](/docs/docs-assets/api-key-test.avif) ## Generating a key On the **Developer → API keys** page: 1. Pick the environment with the **Test | Live** tab strip. The list and the key you generate are scoped to the selected tab. 2. Click **Generate key** (top right) and confirm. The key is **revealed once**, right after creation. Copy it immediately into your secret manager (GCP Secret Manager, AWS Secrets Manager, 1Password, and so on). Afterwards only a masked form (the last few characters) is shown, you can't retrieve the full key again. :::note You can hold at most **2 active keys per environment**. Once you hit the limit the button reads "Limit reached for test/live keys", revoke an old key to generate a new one. Generating and revoking keys is **owner/admin** only; members see a read-only list. ::: ## Key shape ``` sk_test_… sk_live_… ``` The prefix encodes the environment and is part of the credential format. A test key only creates sessions against test configurations, a live key only against live ones, with no cross-environment access. Detecting `sk_live_` in commits is a useful pre-push hook. ## Using a key Hand it to the Enterprise SDK at initialization: ```ts import { SelfClient } from '@selfxyz/enterprise-sdk'; const self = new SelfClient({ apiKey: process.env.SELF_API_KEY! }); ``` The SDK uses it on every call to `sessions.create(...)` and `sessions.get(...)`. See the [SDK reference](/docs/self-enterprise/sdk/nodejs/). ## Revocation Revoke any key from the same **API keys** page. There's no undo, generate a new one if needed. The page lists your existing keys by their last few characters and when they were created. ## Security notes - Don't put keys in front-end code. They authorize session creation, which costs money. - Don't commit keys. Use environment variables and secret managers. - If a service's key leaks, you only have to rotate that one. --- # Webhooks Source: https://docs.self.xyz/docs/self-enterprise/dashboard/webhooks/ **Developer → Webhooks**. Register the HTTPS endpoints Self posts events to. For the delivery model, signatures, retries, and payloads, see the [Webhooks section](/docs/self-enterprise/webhooks/overview/). ![Webhooks settings](/docs/docs-assets/webhook.avif) ## Add an endpoint First pick the environment with the **Test | Live** tab strip at the top of the page; the endpoint you create is stamped with the selected environment. Then click **Add webhook** and fill in: - **Webhook URL** (required): the **full** public HTTPS URL including your handler's path, e.g. `https://yourapp.com/webhooks/self`, not just the domain. - **Name** (optional): a label for the endpoint. Two rules are enforced when you save, in the dashboard and equally on the API, so you can't bypass them by calling directly: - **`https://` only.** Endpoints receive signed proof payloads; plain `http://` is rejected. - **A publicly reachable host.** Private, loopback, link-local and other reserved addresses are rejected, as are internal hostnames like `localhost`. To develop against a machine you can't expose, put a tunnel (ngrok, Cloudflare Tunnel, or similar) in front of it and register the public URL. :::warning Always include the path. If you register a bare domain (`https://yourapp.com`), deliveries often land on your web app's page route instead of your webhook handler, where they're silently swallowed. ::: Click **Save**. The endpoint is saved right away and the **signing secret** (`whsec_...`) is revealed **once** in a banner. This is the only time you'll see it, copy it into your secret manager as `SELF_WEBHOOK_SECRET`. Every endpoint receives the `verification.completed` event; there's no per-endpoint event picker. Branch on `event.type` in your handler. See the [event catalog](/docs/self-enterprise/webhooks/events/). ## Verify the signature Once you have the `whsec_...` secret, wire up real signature verification so you only act on genuine events. See [Verify webhooks](/docs/self-enterprise/webhooks/verify-webhooks/). ## Send a test request A saved endpoint has a **Send Test Request** button. It delivers a sample event to your URL and reports whether the endpoint accepted it (returned `2xx`). When it doesn't, the dialog shows the **status code your endpoint returned and the start of its response body**, which is usually enough to tell a wrong path from a crashing handler. It's repeatable, use it to confirm your handler is reachable and responding before you rely on live traffic. Sending a test is optional and never required to save an endpoint. :::note A live endpoint is a **prerequisite for [Custom Config](/docs/self-enterprise/workspaces/custom-config/)**: those flows deliver results only to your webhook (Self stores nothing), so Deploy is disabled and live `sessions.create(...)` returns `409 no_webhook_endpoint` until one exists here. Deleting your last live endpoint puts deployed Custom Configs back in that blocked state. ::: ## Related - [Webhooks: overview](/docs/self-enterprise/webhooks/overview/): delivery, retries, and idempotency. - [Verify webhooks](/docs/self-enterprise/webhooks/verify-webhooks/): how to validate a delivery. - [Event catalog](/docs/self-enterprise/webhooks/events/): every event and its payload. --- # People Source: https://docs.self.xyz/docs/self-enterprise/dashboard/people/ **Settings → People**. Invite teammates and see who's on your organization. Inviting is limited to `owner` and `admin` roles. ![Invite a teammate](/docs/docs-assets/member-invite.avif) ## Invite a teammate In the **Invite teammates** card, enter an email, pick a **role** (`member` or `admin`; owners can also invite another `owner`), and click **Send invitation**. Self emails them a join link. - The invitation is bound to the **exact email you entered**; only that address can accept it. - A person belongs to **one organization at a time**, so accepting your invite moves them out of their current org (their own default org is kept for them to [return to](#leave-an-organization) later). - Acceptance is refused while the invitee's current organization still has an **active paid subscription**; they need to cancel or downgrade that plan first. ## Team members The **Team members** card lists everyone on the org and every outstanding invite: | Column | Notes | | ------------ | ----------------------------------------------------------- | | Name / Email | Stacked. Name fills in once the person accepts. | | Role | **Owner**, **Admin**, or **Member**. | | Status | **Active** (joined), **Invited** (pending), or **Expired**. | | Added | When they were invited or joined. | Pending and expired invitations have a **⋯** menu with **Resend** and **Revoke**. Active members can be removed via **Remove from organization** (owner/admin only). A few guardrails apply: you can't remove yourself (to take yourself off an org, [leave it](#leave-an-organization)), an admin can't remove an owner, and the last owner can't be removed. Removing a member or revoking their invite doesn't touch any API keys, those belong to the org, not the person. ## Leave an organization Anyone can leave an org they were invited to, no owner or admin needed: **Settings → General → Leave organization**. Leaving returns you to your default organization (the one created when you first signed up), and you can be invited back later. A few guardrails here too: - You can't leave your **default organization**: there's nowhere to return to, so the action isn't shown there. - The **last owner** of an org with other members must promote another owner before leaving. - An org with an **active paid subscription** can't be left; cancel or downgrade the plan first. A subscription already set to cancel at period end doesn't block. If you were the only member, the org is archived when you leave. ## Roles Every member has one of three roles: | Role | Can do | | ------------ | ------------------------------------------------------------------------------------------------------------------------------ | | **`owner`** | Everything, including all billing actions. | | **`admin`** | Invite teammates, manage flows, generate and revoke API keys, manage webhooks, deploy on-chain contracts, and manage the plan. | | **`member`** | Read access: view flows, the activity log, usage, and webhook delivery history. | Only `owner` and `admin` can invite teammates, manage API keys, manage webhook endpoints, and deploy or retry on-chain contracts. --- # Billing Source: https://docs.self.xyz/docs/self-enterprise/dashboard/billing/ **Settings → Usage & Billing**. See your credit usage and manage your plan. For the credit model and how cost is computed, see [Credits and usage](/docs/self-enterprise/billing/credits-and-usage/). ![Usage & Billing](/docs/docs-assets/billing.avif) ## What's on the tab ### Credits used A meter showing how many credits you've used against your plan's allotment, and how many remain. Enterprise plans have no fixed cap, so the meter shows the remaining balance only. ### Your Self Organization Plan Your current plan (**Free**, **Starter**, or **Enterprise**), its price, and the cycle line: **Next invoice on {date}** when it renews, or **Subscription ends on {date}** if you've cancelled. The cycle line also surfaces payment problems, and these take priority over dates: - **Payment failed — update your payment method to restore your credits**: your last renewal charge didn't go through. Fix the payment method via **Update billing details**. - **Finish setting up your subscription**: checkout started but never completed. - **Renewal pending**: the cycle rolled over and the renewal is being processed. ## Actions These actions are owner/admin only; plain members see the meter and plan summary as read-only. - **Manage plan**: opens the pricing dialog to change or upgrade your plan. Free is the default, Starter is self-serve, Enterprise is arranged with sales. See [Plans](/docs/self-enterprise/billing/plans/). - **Update billing details**: opens the secure Stripe customer portal, where you manage your payment method and view past invoices. Self never stores your card details, only a customer reference. ## Running low To add capacity, upgrade your plan from **Manage plan** (or talk to sales about Enterprise). ## Related - [Plans](/docs/self-enterprise/billing/plans/): what each tier includes. - [Credits and usage](/docs/self-enterprise/billing/credits-and-usage/): how the per-verification cost works. --- # Node.js / TypeScript SDK Source: https://docs.self.xyz/docs/self-enterprise/sdk/nodejs/ `@selfxyz/enterprise-sdk` is the official Node/TS client for Self Enterprise. It wraps session creation, retrieval, and webhook signature verification with typed payloads. ## Install ::::tabs :::tab{label="npm"} ```bash npm install @selfxyz/enterprise-sdk ``` ::: :::tab{label="pnpm"} ```bash pnpm add @selfxyz/enterprise-sdk ``` ::: :::tab{label="yarn"} ```bash yarn add @selfxyz/enterprise-sdk ``` ::: :::: Node 20+. ESM-only. ## Initialize ```ts import { SelfClient } from '@selfxyz/enterprise-sdk'; const self = new SelfClient({ apiKey: process.env.SELF_API_KEY!, // sk_test_... or sk_live_... }); ``` `apiKey` is the only option, and the key is bound to one environment (test or live). ## Sessions ### Create Get the `flowId` by deploying a configuration in the dashboard, it's shown on the workspace's **Test** and **Live** tabs. See [Configure a workspace](/docs/self-enterprise/dashboard/configure-a-workspace/). ```ts const session = await self.sessions.create({ flowId: '9c0b4f1c-1d6c-4f1b-a8c4-9f0fa0a8d9e2', externalUuid: 'user-42', // your stable id for the user (any string up to 256 chars) // optional: expiresInSeconds: 3600, metadata: { campaign: 'winter-2026' }, successUrl: 'https://app.example.com/verified', // must be http(s) failureUrl: 'https://app.example.com/failed', // must be http(s) }); session.verificationUrl; // give to the user session.id; // store on your side session.expiresAt; // ISO-8601 ``` Returns `Session`. The essentials: `id` is what you store and match against webhook `verification_id`, `verificationUrl` is what you hand to the user, and `flowVersionId` pins the flow version so a later deploy doesn't disturb in-flight sessions. The full field table is in the [SDK API reference](/docs/self-enterprise/sdk/api-reference/#sessionscreateinput). ### Get ```ts const detail = await self.sessions.get('7f3b2a1e-9c4d-4b2a-8e1f-2c6d5a4b3c2d'); // the session id detail.status; // 'pending' | 'valid' | 'invalid' | 'error' | 'expired' detail.proofAttributes; // enforced predicate config, e.g. { minimumAge: 18, ofac: true } detail.storage.state; // 'pending' | 'committed' | 'failed' | 'skipped' ``` Returns `SessionDetail`; the full field table is in the [SDK API reference](/docs/self-enterprise/sdk/api-reference/#sessionsgetid). The `id` must be a UUID: anything else throws `SelfValidationError` without making a request. ## Types The SDK exports the canonical Zod schemas and their inferred types: ```ts import type { SelfClientOptions, // SelfClient constructor options CreateSessionInput, // what you pass to .create() Session, // what .create() returns SessionDetail, // what .get() returns WebhookEvent, // the webhook event payload type VerificationCompletedPayload, // the verification.completed payload } from '@selfxyz/enterprise-sdk'; ``` You can also import the schemas themselves for runtime validation: ```ts import { createSessionBody, webhookEvent } from '@selfxyz/enterprise-sdk'; const parsed = createSessionBody.parse(input); ``` ## Error handling Every non-2xx response throws `SelfApiError` (`statusCode`, `code`, `message`, `details`, `requestId`); bad arguments throw `SelfValidationError` before any request is sent. The SDK doesn't retry, handle transient `429` and `5xx` responses yourself. See [Error handling](/docs/self-enterprise/sdk/error-handling/) for the catch pattern and the full code catalog. --- # SDK error handling Source: https://docs.self.xyz/docs/self-enterprise/sdk/error-handling/ The SDK throws three error classes. Catch them by type. ## `SelfApiError` Thrown when the API returns a non-2xx response. It carries: ```ts import { SelfApiError } from '@selfxyz/enterprise-sdk'; try { await self.sessions.create({ flowId, externalUuid }); } catch (err) { if (err instanceof SelfApiError) { err.statusCode; // number (HTTP status) err.code; // string ('validation_failed', 'not_found', 'unauthenticated', ...) err.message; // string (human-readable) err.details; // Record | undefined err.requestId; // string | undefined (X-Request-Id; quote it to support) } else { throw err; // network error, or something unexpected } } ``` ### Switching on the error Branch on `statusCode` for the HTTP-level case and on `code` for the specific reason: ```ts try { await self.sessions.create({ flowId, externalUuid }); } catch (err) { if (!(err instanceof SelfApiError)) throw err; switch (err.statusCode) { case 400: return badRequest(err.details); // details.issues lists the bad fields case 404: return notFound('That flow or session no longer exists'); case 402: return paymentRequired(err.details); // { balance, required, planTier } case 429: return tryLater(); // back off and retry default: return serverError(err); } } ``` ### Error codes `err.code` is a stable, machine-readable string. The most common from `sessions.create(...)`: | `code` | `statusCode` | When | What to do | | -------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | `validation_failed` | 400 | Request body didn't match the schema. `details.issues` lists offending fields. | Fix the request; do not retry. | | `unauthenticated` | 401 | Missing, malformed, or revoked API key. | Check your key. | | `unauthenticated` | 402 | Org credit balance too low (credit gate). `details` carries `balance`, `required`, and `planTier`. | Top up or upgrade. Branch on **`statusCode` 402**, not the code. | | `forbidden` | 403 | Key recognized but blocked at the key layer (for example a disabled key). | Generate a fresh key and retire the old one. | | `org_suspended` | 403 | Your organization is suspended. Sessions, in-flight verifications, and webhook deliveries are all refused until it's lifted. | Don't retry. Contact [team@self.xyz](mailto:team@self.xyz). | | `not_found` | 404 | `flowId` / `sessionId` doesn't exist, is archived, or belongs to another org. | Verify the ID in the dashboard. | | `conflict` | 409 | The flow exists but has no deployed version; for an [on-chain flow](/docs/self-enterprise/flows/onchain-verification/), no deployed contract for this environment (`details.discriminator: 'no_deployment'`); or a **live [Custom Config](/docs/self-enterprise/workspaces/custom-config/) session with no active live webhook endpoint** (`details.discriminator: 'no_webhook_endpoint'`), because those flows deliver results only to your webhook, so a session without a receiver is refused. | Deploy the flow (and wait for the contract deploy to finish), or register a live webhook endpoint, then retry. | | `rate_limited` | 429 | Per-key rate limit exceeded. | Honor `Retry-After` and back off. | | `vendor_unavailable` | 503 | A dependency is temporarily unavailable. | Retry with backoff. | | `internal_error` | 500 | Server-side error. | Retry with backoff; if it persists, contact support. | | `unknown_error` | varies | The API returned an error the SDK couldn't map to a known code. | Inspect `statusCode` and `requestId`; retry transient statuses. | ## `SelfValidationError` Thrown when the arguments you pass fail schema validation, **before** any request goes out. The most common cause is a `flowId` that isn't a UUID, or an empty / over-long (>256 chars) `externalUuid`. It also comes from `SelfWebhooks.verify(...)` when a verified payload doesn't match any known event shape. ```ts import { SelfValidationError } from '@selfxyz/enterprise-sdk'; try { await self.sessions.create({ flowId, externalUuid }); } catch (err) { if (err instanceof SelfValidationError) { err.issues; // ZodIssue[], which fields are wrong err.message; // e.g. "Invalid sessions.create input: flowId (invalid uuid)" } } ``` Fix the input; it's a programming error, not a transient one. ## `WebhookVerificationError` Thrown by `SelfWebhooks.verify(...)` when the signature doesn't match the body, the timestamp is too old, or required headers are missing. ```ts import { WebhookVerificationError } from '@selfxyz/enterprise-sdk'; try { const event = SelfWebhooks.verify(raw, headers, secret); } catch (err) { if (err instanceof WebhookVerificationError) { // Respond 400; a bad signature won't get better on retry. } } ``` A separate failure mode: if the signature checks out but the body doesn't match any known event schema, `verify(...)` throws `SelfValidationError`. That usually means the server is sending a newer payload than your SDK version knows; upgrade the SDK. For example, an out-of-date SDK rejects [Custom Config](/docs/self-enterprise/workspaces/custom-config/) events with `Invalid enum value ... received 'skipped'` on `storage_state`; the current SDK accepts them. ## Retries The SDK does **not** retry. A failed request throws immediately. Handle transient responses yourself: retry `429` (after `Retry-After`) and `5xx` with exponential backoff, and never retry a `4xx` other than `429`. ## Logging Log the status and code so failures are diagnosable: ```ts log.error({ msg: 'self_api_error', statusCode: err.statusCode, code: err.code, requestId: err.requestId, details: err.details, }); ``` ## Related - [SDK reference](/docs/self-enterprise/sdk/nodejs/). - [Verify webhooks](/docs/self-enterprise/webhooks/verify-webhooks/). --- # SDK API reference Source: https://docs.self.xyz/docs/self-enterprise/sdk/api-reference/ The public surface of `@selfxyz/enterprise-sdk` you use to create sessions and verify webhooks. For a guided walkthrough, start with the [SDK overview](/docs/self-enterprise/sdk/nodejs/). :::note The Enterprise SDK is **backend-only**, and there's no frontend SDK or QR code to render yourself. Your frontend just redirects the user to the `verificationUrl` a session returns; Self hosts the QR and deeplink. Everything below runs on your server. ::: ## Exports ```ts import { SelfClient, // the API client SelfWebhooks, // webhook signature verification SelfApiError, // thrown on a non-2xx API response SelfValidationError, // thrown on invalid input / unknown payload WebhookVerificationError, // thrown on a bad webhook signature } from '@selfxyz/enterprise-sdk'; import type { SelfClientOptions, // SelfClient constructor options CreateSessionInput, // argument to sessions.create() Session, // return of sessions.create() SessionDetail, // return of sessions.get() WebhookEvent, // the webhook event payload type WebhookHeaders, // the request headers verify() reads VerificationCompletedPayload, // the verification.completed payload } from '@selfxyz/enterprise-sdk'; ``` The Zod schemas behind those types (`createSessionBody`, `createSessionResponse`, `sessionDetailResponse`, `verificationCompleted`, `webhookEvent`) are also exported as values if you want runtime validation. ## `SelfClient` ```ts new SelfClient({ apiKey: string }); ``` The API client. `apiKey` is your `sk_test_…` or `sk_live_…` key, and the key is bound to one environment. Throws if `apiKey` is missing. ### `sessions.create(input)` ```ts sessions.create(input: CreateSessionInput): Promise ``` Creates a verification session against the deployed flow and returns a URL to send the user to. **`CreateSessionInput`** | Field | Type | Required | Notes | | ------------------ | ------------- | :-----------------------------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `flowId` | string (UUID) | | The flow to verify against. | | `externalUuid` | string | | Your stable identifier for the user, echoed back on the webhook. Any opaque string, 1-256 chars (`order-42/user-1` is fine). Not an idempotency key, see the note below. | | `expiresInSeconds` | number | | How long the session stays openable. Integer, 60 to 86400. Default 3600. | | `metadata` | object | | Arbitrary JSON attached to the session. Max 4 KB serialized. | | `successUrl` | string (URL) | | Where the hosted page sends the user on success. Must be an `http(s)` URL; other schemes are rejected before the request is sent. | | `failureUrl` | string (URL) | | Where the hosted page sends the user on failure. Same `http(s)` rule. | :::note `externalUuid` is **not** an idempotency key. Every `sessions.create(...)` call creates a new session with its own `id`: two calls with the same `externalUuid` both succeed and return distinct sessions, and multiple pending sessions for the same user are allowed. If you want one session per user, dedup on your side: store the pending session's `id` keyed by your user, and reuse it (via [`sessions.get(...)`](#sessionsgetid)) instead of creating another. ::: **`Session`** (the return) | Field | Type | Notes | | ------------------- | ----------- | ----------------------------------------------------------- | | `id` | string | The session ID. Match it against webhook `verification_id`. | | `externalUuid` | string | Echoes your input. | | `status` | `'pending'` | Always `pending` at creation. | | `flowVersionId` | string | The pinned flow version. | | `verificationToken` | string | Opaque token (`verify__…`). | | `verificationUrl` | string | The URL to hand to the user. | | `createdAt` | string | ISO-8601. | | `expiresAt` | string | ISO-8601. | | `completedAt` | null | Always `null` at creation. | Throws [`SelfApiError`](#selfapierror) on a non-2xx response. ### `sessions.get(id)` ```ts sessions.get(id: string): Promise ``` Reads a session's current state. `id` must be a UUID: anything else throws [`SelfValidationError`](#selfvalidationerror) before a request is sent. **`SessionDetail`** (the return) | Field | Type | Notes | | ------------------ | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | The session ID. | | `status` | `'pending' \| 'valid' \| 'invalid' \| 'error' \| 'expired'` | Current state. | | `createdAt` | string | ISO-8601. | | `completedAt` | string \| null | ISO-8601, `null` until terminal. | | `expiresAt` | string | ISO-8601. | | `flowVersionId` | string | The pinned flow version. | | `externalUuid` | string | Your identifier from create. | | `metadata` | object \| null | What you passed to create. | | `predicatesConfig` | object \| null | The rules this ran against. | | `proofAttributes` | object \| null | The enforced predicate config echoed back (`minimumAge`, `excludedCountries`, `includedCountries`, `ofac`), `null` until valid. Not disclosed booleans, see the [event catalog](/docs/self-enterprise/webhooks/events/). | | `storage` | object | `{ state: 'pending' \| 'committed' \| 'failed' \| 'skipped'; uri: string \| null; credentialId: string \| null }`. [Custom Config](/docs/self-enterprise/workspaces/custom-config/) and on-chain sessions report `state: 'skipped'`: no off-chain record is kept for them, and `uri` stays `null`. | Throws [`SelfApiError`](#selfapierror) (for example `404` if the ID is unknown). :::note Sessions from an [on-chain flow](/docs/self-enterprise/flows/onchain-verification/) additionally return `verification_mode: 'onchain'` and an `onchain` object (`contract_address`, `scope_seed`, `network`). ::: --- ## `SelfWebhooks` ### `SelfWebhooks.verify(payload, headers, secret)` ```ts SelfWebhooks.verify( payload: string | Buffer, headers: Record, secret: string, ): WebhookEvent ``` Verifies a webhook signature and returns the typed, parsed event. | Parameter | Notes | | --------- | ----------------------------------------------------------------------------------------------- | | `payload` | The **raw** request body (string or Buffer), not a parsed object. | | `headers` | The request headers from the delivery (pass the full headers object; they carry the signature). | | `secret` | The endpoint's signing secret (`whsec_…`). | Throws [`WebhookVerificationError`](#webhookverificationerror) if the signature is invalid or the timestamp is stale, and [`SelfValidationError`](#selfvalidationerror) if the body doesn't match any known event shape (usually an out-of-date SDK). **`WebhookEvent`** is the webhook payload type. The event delivered to your endpoints is `verification.completed` (`VerificationCompletedPayload`): ```ts if (event.type === 'verification.completed' && event.status === 'valid') { // event is VerificationCompletedPayload here await markUserVerified(event.external_uuid, event.nullifier); } ``` See the [event catalog](/docs/self-enterprise/webhooks/events/) for the payload's fields. ## Errors ### `SelfApiError` Thrown when the API returns a non-2xx response. | Property | Type | Notes | | ------------ | -------------------------------------- | ---------------------------------------------------------------------- | | `statusCode` | number | HTTP status. | | `code` | string | Stable machine-readable code (`validation_failed`, `not_found`, ...). | | `message` | string | Human-readable. | | `details` | `Record` \| undefined | Extra context (for example validation `issues`). | | `requestId` | string \| undefined | The `X-Request-Id` response header, when present. Quote it to support. | See [Error handling](/docs/self-enterprise/sdk/error-handling/) for the code catalog. ### `SelfValidationError` Thrown when arguments fail schema validation **before** a request is sent (for example a `flowId` that isn't a UUID, or an empty `externalUuid`), and by `SelfWebhooks.verify(...)` when a verified payload doesn't match any known event shape. | Property | Type | Notes | | --------- | ------------ | ---------------------------- | | `issues` | `ZodIssue[]` | Which fields failed and why. | | `message` | string | Human-readable summary. | ### `WebhookVerificationError` Thrown by `SelfWebhooks.verify(...)` on a bad signature, stale timestamp, missing signature headers, or a malformed signing secret. Respond `400`, a bad signature won't pass on retry. ## Related - [SDK overview](/docs/self-enterprise/sdk/nodejs/): the guided version. - [Verify webhooks](/docs/self-enterprise/webhooks/verify-webhooks/): framework wiring. - [Error handling](/docs/self-enterprise/sdk/error-handling/): the error catalog. --- # Webhooks: overview Source: https://docs.self.xyz/docs/self-enterprise/webhooks/overview/ Webhooks are the canonical way to learn that a verification finished. Polling works but it's lossy under retries and expensive at scale. ## How they work 1. You register an HTTPS endpoint in the dashboard ([Developer → Webhooks](/docs/self-enterprise/dashboard/webhooks/)) and get the signing secret when you save it. (You can send a test request afterwards to confirm it's reachable.) 2. When an event fires, we POST a JSON payload to your endpoint, signed so you can verify it came from us. 3. Your handler verifies the signature (via the SDK) and processes the event. 4. You return `2xx` to acknowledge. A `408`, `429`, `5xx`, or timeout triggers a retry; any other `4xx` is treated as a permanent rejection and not retried. ![Webhook delivery: Self sends a signed POST to your endpoint, which verifies the signature and acks with 2xx; a 408, 429, 5xx, or timeout is retried with backoff](/docs/docs-assets/webhook-delivery.svg) ## What we send A single POST with a JSON body, typed as `WebhookEvent`. One event is delivered: | Event type | Fires when | | ------------------------ | ----------------------------------------------------- | | `verification.completed` | Off-chain verification finishes (success or failure). | See [Event catalog](/docs/self-enterprise/webhooks/events/) for the full payload. ## Ordering and delivery guarantees - **At-least-once.** The same event can arrive more than once (a retry after a failed delivery, a network blip). Make your handler idempotent. - **Bounded latency.** Typically under a second from verification to delivery. Under retry backoff, latency can grow to minutes. ## Idempotency The event carries a stable ID derived from the verification, so retries of the same event share it: - `verification.completed`: `-completed` Dedupe in your handler on `verification_id`. See [Best practices](/docs/self-enterprise/webhooks/best-practices/). ## Reliability Failed deliveries (`408`, `429`, `5xx`, timeout, or connection error) are retried automatically on an exponential schedule over a span of days, so a brief outage on your side recovers on its own. Any other `4xx` response is a permanent rejection and is not retried, so never return `400` for a transient failure like a database hiccup (see [Best practices](/docs/self-enterprise/webhooks/best-practices/#6-return-the-right-status-codes)). Every delivery is signed, see [Verify webhooks](/docs/self-enterprise/webhooks/verify-webhooks/). :::warning For [Custom Config](/docs/self-enterprise/workspaces/custom-config/) flows the webhook delivery is the **only copy** of the verification result and disclosed data: Self stores none of it (`storage_state: 'skipped'`). Persist the payload before returning `2xx`. This is also why a live Custom Config can't deploy or create sessions until your organization has an active live endpoint. ::: --- # Verify webhooks Source: https://docs.self.xyz/docs/self-enterprise/webhooks/verify-webhooks/ Self signs every webhook delivery with **HMAC-SHA256**. Verify the signature before trusting the payload, and don't roll your own check: the SDK's `SelfWebhooks.verify(...)` does it correctly in one call and returns a typed event. `verify(...)` checks two things: - The signature matches the raw request body and your `whsec_...` signing secret. - The timestamp is recent (a 5-minute tolerance, which defends against replay). ## Setup You need: - The **raw** request body (string or Buffer). Not the JSON-parsed object, signature verification operates on the byte string. - The request headers from the delivery. Pass them through as-is; the SDK reads the signature and timestamp off them. - The signing secret (`whsec_...`) for the webhook endpoint. You get it once, when you [add the endpoint](/docs/self-enterprise/dashboard/webhooks/); rotation works by deleting and recreating the endpoint. ## Next.js (App Router) ```ts // app/api/webhooks/self/route.ts import { SelfWebhooks, WebhookVerificationError } from '@selfxyz/enterprise-sdk'; export async function POST(req: Request) { const raw = await req.text(); // raw body, before any JSON parsing const headers = Object.fromEntries(req.headers); // pass all headers through try { const event = SelfWebhooks.verify(raw, headers, process.env.SELF_WEBHOOK_SECRET!); if (event.type === 'verification.completed') { // event.verification_id, event.external_uuid, event.proof_attributes, event.status } return new Response('ok', { status: 200 }); } catch (err) { if (err instanceof WebhookVerificationError) { return new Response('bad signature', { status: 400 }); } // Unknown payload shape (SelfValidationError) or server bug: log and 5xx so Self retries. return new Response('error', { status: 500 }); } } ``` Any other framework works the same way: read the raw request body before JSON parsing, pass `(rawBody, headers, secret)` to `SelfWebhooks.verify(...)`, and branch on `event.type`. ## Type narrowing `event` is typed as `WebhookEvent`; narrow it on `event.type` and TypeScript does the rest: ```ts if (event.type === 'verification.completed') { event.status; // 'valid' | 'invalid' | 'error' | 'expired' event.proof_attributes; // the enforced predicate config, echoed back event.nullifier; // uniqueness id (string | null), for Sybil resistance // event.proof and event.storage_uri are also available } ``` ## Common failure modes - **Parsing the body before verification.** If a middleware or framework hook JSON-parses the request before your handler runs, the raw bytes are gone and verification will fail. In a Next.js route handler, read the body with `await req.text()` and don't call `req.json()` first. This is the most common failure by far. - **Trimming or transforming.** A trailing newline added by your proxy will break the signature. Configure the proxy to pass the body unchanged. - **Using the wrong secret.** Each endpoint has its own `whsec_...`. If you have multiple endpoints registered (e.g. staging + prod), don't share secrets across them. - **Clock skew.** A drifted server clock can fail the timestamp tolerance check. Make sure NTP is healthy. ## Redeliveries The same event can arrive more than once (a retry after a failed delivery, for example). The body and signature are valid each time, so verification succeeds normally. Make your handler idempotent, dedupe on the event's `verification_id`. See [Best practices](/docs/self-enterprise/webhooks/best-practices/) for idempotency patterns. --- # Event catalog Source: https://docs.self.xyz/docs/self-enterprise/webhooks/events/ Self delivers a single webhook event: `verification.completed`. Every registered endpoint receives it; there's no per-event subscription. ## `verification.completed` Fires when a verification finishes, either successfully or with a definitive failure, in both [verification modes](/docs/self-enterprise/flows/verification-modes/). This is the event integrations care about. ### Payload ```json { "type": "verification.completed", "verification_id": "7f3b2a1e-9c4d-4b2a-8e1f-2c6d5a4b3c2d", "external_uuid": "a1b2c3d4-5678-4e9a-b012-3456789abcde", "flow_id": "9c0b4f1c-1d6c-4f1b-a8c4-9f0fa0a8d9e2", "flow_version_id": "b1e2c3d4-5678-4abc-9def-0123456789ab", "environment": "live", "status": "valid", "product": "pre_kyc", "proof_attributes": { "minimumAge": 18, "excludedCountries": ["PRK", "IRN"], "ofac": true, "nationality": "USA", "name": "JANE DOE" }, "proof": { /* raw proof envelope for re-verification (see below) */ }, "nullifier": "0x9b1c…", "verified_at": "2026-05-29T17:33:21.412Z", "storage_state": "pending", "storage_uri": null } ``` :::note All fields are at the **root** of the event object; there is no `data` wrapper. Access them as `event.verification_id`, not `event.data.verification_id`. ::: ### Fields | Field | Type | Notes | | ----------------------------- | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `verification_id` | string | Stable, unique. Use as your dedup key. | | `external_uuid` | string | The identifier you passed when creating the session (any string up to 256 chars, echoed verbatim). | | `flow_id` / `flow_version_id` | string | Which flow + frozen version this verification ran against. | | `environment` | `'test' \| 'live'` | Matches the API key environment that created the session. | | `status` | `'valid' \| 'invalid' \| 'error' \| 'expired'` | See [statuses](#statuses) below. | | `reason` | string (optional) | Failure cause, present only on non-`valid` statuses. Human-readable, not an enum: log it, don't branch on it. | | `product` | string (optional) | Which workspace the flow belongs to (`pre_kyc`, `age_verification`, `proof_of_human`, `sovereign`, `custom_config`). Lets a handler on a shared org endpoint route per workspace. Omitted on events from before the field existed. | | `proof_attributes` | object | The flow's resolved **predicate config** that was enforced, echoed back: the same keys you set in the dashboard: `minimumAge`, `excludedCountries`, `includedCountries`, `ofac` (only the ones your flow uses). These are **not** disclosed booleans; a `valid` status is what tells you every predicate passed. On a `valid` event this object **also carries the disclosed reveal values** for any [Additional-data](/docs/self-enterprise/flows/disclosures/#additional-data-reveals) or [Custom Config](/docs/self-enterprise/workspaces/custom-config/) fields the flow requested (keyed `name`, `idNumber`, `dateOfBirth`, `gender`, `nationality`, `expiryDate`, `issuingState`), included only when the holder returned a non-empty value (predicate and reveal keys never collide). Non-`valid` statuses carry the predicate config only. Empty object if the flow had no predicates and requested no reveals. | | `proof` | object \| null | The raw submitted proof envelope (`attestationId`, `proof`, `publicSignals`, `userContextData`). Currently populated on every status, including `invalid`/`error`/`expired` (it's whatever the holder submitted). Most integrations ignore it. Self already verified the proof. | | `nullifier` | string \| null | Uniqueness identifier, stable per person within your organization. Present when `status === 'valid'`, `null` otherwise. See [nullifiers](/docs/self-enterprise/get-started/how-it-works/#one-account-per-person-nullifiers). | | `verified_at` | ISO-8601 | When verification completed. | | `storage_state` | `'pending' \| 'committed' \| 'failed' \| 'skipped'` | `pending` on most events: the completed event fires before async storage runs; for the final state, read the session with [`sessions.get(...)`](/docs/self-enterprise/sdk/nodejs/). **`skipped` means no storage will ever happen**: [Custom Config](/docs/self-enterprise/workspaces/custom-config/) verifications are under org custody, so this delivery is the sole permanent record: persist what you need now. (On-chain sessions also report `'skipped'` via `sessions.get(...)`: the chain is the record.) | | `storage_uri` | string \| null | Always `null` on this event. The committed URI, once available, is returned by [`sessions.get(...)`](/docs/self-enterprise/sdk/nodejs/), not the webhook. | ### Org custody (Custom Config) Events from a [Custom Config](/docs/self-enterprise/workspaces/custom-config/) flow carry `product: 'custom_config'` and `storage_state: 'skipped'`, and they change the handling contract in one important way: **the delivery is the only copy**. Self stores neither the proof nor the disclosed fields, no storage ever runs, and `sessions.get(...)` has nothing extra to return later. Write the disclosed `proof_attributes` values (and `proof`, if you keep it) to your own storage before acking with `2xx`. Deliveries retry for days if your endpoint is down, but a `2xx` you didn't persist is data gone for good. ### On-chain fields Verifications from an [on-chain flow](/docs/self-enterprise/flows/onchain-verification/) carry up to three extra fields. They are **omitted entirely** (not `null`) on backend-mode events, so existing handlers keep working unchanged: | Field | Type | Notes | | ------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------- | | `verification_mode` | `'backend' \| 'onchain'` | Which mode the flow verifies in. Omitted on events from before the field existed; treat absence as `backend`. | | `tx_hash` | string | The on-chain transaction, always present on a `valid` on-chain event (the mint transaction). | | `contract_address` | string | The flow's SBT contract on Celo for that environment. | On on-chain events, `proof_attributes` is an **empty object**: the rules are enforced by the contract, not evaluated by Self's servers, so there's nothing to echo back. Branch on `status` as usual. ### Statuses - `valid`: the proof verified and all flow predicates passed. This is the green-light case. - `invalid`: the proof verified but at least one predicate failed (e.g. user is 16, flow requires 18). - `error`: verification failed for a technical reason (unsupported document, malformed proof, signature mismatch). Not the user's fault necessarily. - `expired`: session expired before completion. Non-`valid` events may carry a human-readable `reason` string, but there is **no machine-readable failure code**: an `invalid` or `error` event tells you _that_ it failed, not _which_ predicate in enumerable form. Log `reason` for diagnosis; if you need to branch on cases (e.g. "underage" vs. "excluded country"), branch on the `proof_attributes` your flow enforced, or read the session with [`sessions.get(...)`](/docs/self-enterprise/sdk/nodejs/). ### Event ID (for deduplication) The deduplication key is `-completed` (the same for every status, so a retry of this event reuses it). ## Handling in TypeScript The payload is typed as `WebhookEvent`; branch on `event.type` to narrow it. `verification.completed` is the event delivered: ```ts import type { WebhookEvent } from '@selfxyz/enterprise-sdk'; function handle(event: WebhookEvent) { if (event.type === 'verification.completed') { // narrowed to VerificationCompletedPayload // event.verification_id, event.external_uuid, event.proof_attributes, event.status } } ``` ## Forward compatibility We may add fields to existing events without bumping major versions. The SDK's `webhookEvent` schema uses `.passthrough()`, so unknown fields are preserved (and ignored by your existing code). We may also add new event types over time, so branch on `event.type` and let your handler ignore any type it doesn't recognize. --- # Best practices Source: https://docs.self.xyz/docs/self-enterprise/webhooks/best-practices/ A small list of things that prevent the common production fires. ## 1. Make handlers idempotent We deliver at-least-once. Network blips and our own retries produce duplicates. Your handler should treat duplicates as a non-event. ### Dedup by event ID The simplest pattern: ```ts const eventId = `${event.verification_id}-${event.type}`; const inserted = await db.query( `INSERT INTO processed_events (event_id) VALUES ($1) ON CONFLICT DO NOTHING RETURNING 1`, [eventId], ); if (inserted.rowCount === 0) { return; // already processed } await applyVerification(event); ``` Use the `verification_id` from the event, it's stable across retries. ## 2. Acknowledge quickly, work async Return `2xx` within a few seconds. If your handler does expensive work, database fan-out, downstream API calls, emails, push to a queue and return immediately. ```ts export async function POST(req: Request) { const event = SelfWebhooks.verify(await req.text(), Object.fromEntries(req.headers), secret); await jobQueue.enqueue('process-verification', event); return new Response('ok', { status: 200 }); } ``` Slow handlers get retried, which produces duplicates, which compounds. Fast ack + queue is the only stable pattern. ## 3. Treat each delivery in isolation `verification.completed` is the only event delivered, so there's no cross-event ordering to reason about. Don't assume anything about the order of deliveries for different verifications either. If you need a verification's current storage state, read it with `sessions.get(...)` rather than inferring it from the webhook. ## 4. Distinguish status carefully `verification.completed` fires with four statuses ([definitions](/docs/self-enterprise/webhooks/events/#statuses)). Don't lump them: | Status | Common handling | | --------- | ---------------------------------------------------------------------- | | `valid` | Mark the user verified, unlock the gated feature. | | `invalid` | Tell the user _why_ a rule failed (e.g. age requirement); offer retry. | | `error` | Tell the user to retry; alert ops if the rate spikes. | | `expired` | Generate a new session if the user is still around. | A common bug: treating any non-`valid` as a final rejection. `invalid` and `error` are recoverable; `expired` definitely is. ## 5. Verify before doing anything Don't read `event.type` until `SelfWebhooks.verify(...)` has returned. An attacker can send any body to your endpoint; only the signature proves it came from us. ```ts // Wrong: export async function POST(req: Request) { const event = await req.json(); // ← trusting unverified body if (event.type === 'verification.completed') { grantAccess(event.external_uuid); // ← attacker can call this } } // Right: export async function POST(req: Request) { const event = SelfWebhooks.verify(await req.text(), Object.fromEntries(req.headers), secret); // ...now safe to act on event } ``` ## 6. Return the right status codes The delivery contract lives in the [webhooks overview](/docs/self-enterprise/webhooks/overview/#reliability): `2xx` acknowledges, `408`/`429`/`5xx`/timeouts are retried with backoff, and any other `4xx` is a permanent rejection with no retry. What that means for your handler: - Return `400` on signature failure. You want it dropped, not retried. - Return `500` (or just throw) on transient errors. You want it retried. - Don't return `400` because of a database hiccup. You'll silently drop events. ## 7. Use one endpoint per environment Don't share a single endpoint between staging and production. They'll have different secrets and different traffic profiles. Register one endpoint for `test` and one for `live`, each with its own URL and signing secret. ## 8. Monitor failures on your side Alert on your own handler's error rate (the `5xx` and signature-failure responses it returns). A handler that's been silently failing is a known production incident pattern, and you'll spot it faster from your own metrics than from the deliveries. Failed deliveries are retried automatically, so once you fix the handler the backlog drains on its own. ## 9. Log the verification ID Log `event.verification_id` on every handler invocation: ```ts log.info({ msg: 'webhook_received', verificationId: event.verification_id, type: event.type }); ``` When you ask support about a missing or wrong delivery, that's the ID to quote. --- # Anatomy of a flow Source: https://docs.self.xyz/docs/self-enterprise/flows/anatomy/ A **flow** is a deployed, versioned configuration of one workspace. It's what a `flowId` points to, and what you build in the dashboard's Configure tab. ## What a flow has - **A workspace**: Pre-KYC, Age Verification, Proof of Human, Sovereign, or Custom Config. The workspace decides what the user proves and which rules you can set. - **A name** (plus a slug and an optional description): how you identify the flow. - **A [verification mode](/docs/self-enterprise/flows/verification-modes/)**: Backend (the default) or On-chain (Custom Config is backend-only). A setting like any other: it's part of the deployed config. - **Rules**: the predicate config (minimum age, country allow/deny list, OFAC), see below. - From the Configure tab it also carries a **security level** (Standard or Hi-security) and any **Additional data** reveals you turn on. The **organization name and icon** (shown on the hosted page) come from the org, not the flow. Two things are **not** part of a flow: - **Document choice.** There's no per-flow document allowlist, the user verifies with whichever supported document they hold. See [Supported documents](/docs/self-enterprise/flows/supported-documents/). - **Redirect URLs.** `successUrl` and `failureUrl` are optional, per-session redirect URLs passed to `sessions.create(...)`. If a session omits them, the hosted page falls back to its own built-in result screens. ## Rules Rules are predicates over identity attributes. The user proves each one without revealing the underlying value. The deployed config is a small JSON object: ```json { "minimumAge": 18, "excludedCountries": ["PRK", "IRN"], "ofac": true } ``` | Field | Type | Notes | | ------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `minimumAge` | integer | Age floor (13–110). The user proves they are at least this old without revealing their birth date. | | `excludedCountries` | string array | Issuing countries to deny (denylist). ISO 3166-1 alpha-3 codes, sorted. Mutually exclusive with `includedCountries`. | | `includedCountries` | string array | Issuing countries to allow (allowlist). Enforced against the disclosed nationality, so it forces the Nationality reveal. Mutually exclusive with `excludedCountries`. | | `ofac` | boolean | When true, the user must clear the OFAC sanctions list. | A Proof of Human flow has no rule fields; uniqueness is intrinsic to the document. A **Sovereign** flow uses a different, minimal shape: a required, non-empty `includedCountries` allowlist plus a `securityLevel` (`standard` or `biometric`), and nothing else, no `ofac`, `minimumAge`, or `excludedCountries`. Its allowlist is enforced against the holder's disclosed nationality. ```json { "includedCountries": ["AUT", "BEL", "DEU"], "securityLevel": "biometric" } ``` A **[Custom Config](/docs/self-enterprise/workspaces/custom-config/)** flow also has its own shape: a required `disclosures` object (at least one field `true`), an `ofac` boolean, and a `securityLevel`, with no age or country predicates: ```json { "disclosures": { "fullName": true, "dateOfBirth": true, "documentNumber": true }, "ofac": true, "securityLevel": "biometric" } ``` This same config object is echoed back on the webhook as `proof_attributes`; a `valid` status means every predicate in it passed. See [Disclosures](/docs/self-enterprise/flows/disclosures/) and the [Event catalog](/docs/self-enterprise/webhooks/events/). ## Immutable once deployed A deployed flow **can't be edited**. Its config is frozen, captured as a version (`flowVersionId`), and every session pins to the exact version it ran against, so your audit log always shows what a user was verified against. To change anything, you don't edit, you **archive the flow and create a new one** (with its own `flowId`). A workspace keeps **one active flow at a time**, except [Custom Config](/docs/self-enterprise/workspaces/custom-config/), where an organization runs **many active flows side by side** and the dashboard shows them as a list. ## Lifecycle ![Flow lifecycle: create and deploy an active flow, archive it when done (history kept), and create a new flow with a new flowId when you need a change](/docs/docs-assets/flow-lifecycle.svg) Archived flows stop accepting new sessions but keep their history for audit. See [Archive](/docs/self-enterprise/dashboard/overview/#archive). :::note On the wire (for example on the session detail response) the five workspaces appear as `pre_kyc`, `age_verification`, `proof_of_human`, `sovereign`, and `custom_config`. When creating sessions you reference a `flowId`, never a workspace name. ::: ## Related - [Disclosures](/docs/self-enterprise/flows/disclosures/): every outcome a proof can attest to. - [Supported documents](/docs/self-enterprise/flows/supported-documents/): what each credential type can prove. - [Test vs. live](/docs/self-enterprise/flows/test-vs-live/). - [Configure a workspace](/docs/self-enterprise/dashboard/configure-a-workspace/). --- # Disclosures Source: https://docs.self.xyz/docs/self-enterprise/flows/disclosures/ A disclosure is what the user's proof attests to. You configure them per flow in the [Configure](/docs/self-enterprise/dashboard/configure-a-workspace/) tab, and which ones are available depends on the [workspace](/docs/self-enterprise/dashboard/configure-a-workspace/#disclosure-rules). There are two kinds: - **Predicates**: a pass or fail answer (age, country, OFAC). The underlying value stays hidden. - **Reveals**: the actual value of a document field, returned only when you ask for it. ## Security level Choose how strictly the document is checked: - **Standard**: the document is genuine. - **Hi-security** (the default): the user also physically scanned the document's chip. Each flow's predicate set is captured as a small config object, and that same object is echoed back to you on the webhook as `proof_attributes` (see [Event catalog](/docs/self-enterprise/webhooks/events/)). The keys below are what that config looks like. ## Age (predicate) Available in **Pre-KYC** and **Age Verification**. Set a minimum age; the user proves they meet it without revealing their birth date. ```json { "minimumAge": 18 } ``` ## Country (predicate) Available in **Pre-KYC** (optional) and **Sovereign** (required; it's the whole workspace). There are two mutually exclusive ways to restrict by issuing country (ISO 3166-1 alpha-3 codes): - **Excluded countries**: a denylist. The user proves their document's country isn't on it, **without revealing which country it is**. ```json { "excludedCountries": ["PRK", "IRN"] } ``` - **Included countries**: an allowlist. Only documents from a listed country pass. Because the allowlist is checked against the holder's **disclosed nationality**, an allowlist flow **reveals the user's nationality to you** (the Nationality reveal is forced on). ```json { "includedCountries": ["USA", "CAN"] } ``` For a **Sovereign** flow this allowlist is the entire configuration (it can't be empty, and there's no OFAC or age rule alongside it). The dashboard's picker offers **region presets** (EU, APAC, LATAM, GCC) to fill it a bloc at a time. [On-chain flows](/docs/self-enterprise/flows/onchain-verification/#configuration-limits) cap the lists at 40 excluded or 41 included countries. ## OFAC (predicate) Available in **Pre-KYC, Age Verification, Proof of Human, and Custom Config** (a toggle, on by default); **Sovereign** is the only workspace without an OFAC check. The user proves they don't match the OFAC sanctions list, which Self keeps updated daily. On a match, the verification is rejected. ```json { "ofac": true } ``` ## Proof of human (predicate) The core of the **Proof of Human** workspace, and present implicitly in the others since every verification is backed by a genuine document. The user proves they're a unique, real human via a [nullifier](/docs/self-enterprise/get-started/how-it-works/#one-account-per-person-nullifiers) derived from the document. ## Additional data (reveals) Available in **Pre-KYC** and **Custom Config** (where the reveals are the whole configuration). Beyond the pass or fail predicates, you can ask the user to disclose specific document fields. Each is **off by default**, so request only what you need: `Full name`, `ID number`, `Date of birth`, `Gender`, `Nationality`, `Expiration date`, `Issuing state`. A reveal returns the actual value, so it's personal data you then have to handle responsibly. The fields you enable are requested from the user's wallet, and on a successful verification their values are returned to you in the webhook's `proof_attributes` (see [Event catalog](/docs/self-enterprise/webhooks/events/)). ## What is never disclosed - Anything you didn't configure. Predicates return only a pass or fail; reveals return only the fields you turned on. - The user's biometric data and the raw document scan, those never leave the device. :::note The one predicate that implies a reveal is the **Included countries** allowlist: it forces the **Nationality** reveal on, so an allowlist flow does disclose the user's nationality. This applies to any Pre-KYC flow with an included list and to every **Sovereign** flow. An **Excluded countries** denylist does not. ::: --- # Verification modes Source: https://docs.self.xyz/docs/self-enterprise/flows/verification-modes/ Every flow has a **verification mode**, chosen on the Configure tab when you build it: - **Backend** (the default): proofs are verified by Self's servers. Results arrive through [webhooks](/docs/self-enterprise/webhooks/overview/) and the [API](/docs/self-enterprise/sdk/nodejs/). - **On-chain**: proofs are verified by a smart contract on **Celo**. Each verified user receives a **soulbound token (SBT)** in their wallet, a non-transferable, on-chain record that they passed your flow. You still get the same webhooks and API results. The mode is part of the flow's configuration, so like every other setting it is [immutable once deployed](/docs/self-enterprise/flows/anatomy/#immutable-once-deployed). To switch modes, archive the flow and create a new one. ## Which mode should I use? Pick **Backend** unless you specifically need an on-chain artifact. It supports every rule, has no wallet requirement for your users, and is the mode the rest of these docs assume. Pick **On-chain** when the verification result must be readable by other smart contracts or wallets, for example token-gated apps, airdrops with sybil resistance, or membership that other on-chain protocols can check without calling your backend. | | Backend | On-chain | | --------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | Where the proof is verified | Self's servers | A contract on Celo, deployed for your flow | | What the user needs | The Self app | The Self app **and** a wallet | | What the user receives | Nothing on-chain | A soulbound token (SBT) in their wallet | | Results in webhooks and API | Yes | Yes, plus the transaction hash and contract address | | Supported rules | All rules | All rules except **maximum age**, with [country-list limits](/docs/self-enterprise/flows/onchain-verification/#configuration-limits) | | Networks | — | Celo Sepolia (test), Celo Mainnet (live) | | Gas cost to you or the user | — | None, Self submits the transactions | ## What happens during a verification The lifecycle is the same shape in both modes: create a session, the user verifies, you get a result. Here's where they differ, step by step: **Backend** 1. Your backend calls `sessions.create(...)` and sends the user to the `verificationUrl`. 2. The user scans the QR code (or taps the deeplink) and approves in the Self app. 3. Self's servers verify the proof against the flow's rules. 4. The result lands everywhere at once: the `verification.completed` webhook, the session status, and the activity log. **On-chain** 1. Same `sessions.create(...)` call. The only precondition is that the flow's contract is deployed for that environment (a one-time step that happens when you deploy the flow). 2. Before the QR code, the hosted page asks the user to connect a wallet and sign a gas-free message. That wallet receives the soulbound token. 3. The proof is verified by the flow's contract on Celo, which mints the token. Self submits the transaction and pays the gas. 4. Once the transaction confirms, the same webhook, session status, and activity log update, now carrying the transaction hash and contract address. **What to expect:** on-chain adds the wallet step for the user and waits for the transaction to confirm, so results take a little longer to become terminal. Either way, treat the webhook as the authoritative result, exactly as in backend mode. ## What changes in your integration Very little. Sessions are created the same way, the hosted page handles the wallet steps, and the terminal statuses are the same (`valid`, `invalid`, `error`, `expired`). Three additions: - The [`verification.completed` webhook](/docs/self-enterprise/webhooks/events/) carries `verification_mode: 'onchain'`, plus `tx_hash` and `contract_address` when a token was minted. - The [Activity log](/docs/self-enterprise/dashboard/activity-log/) shows the mint transaction for each verification, linked to the block explorer. - Session creation requires the flow's contract to be deployed for that environment; otherwise the API returns `409` with a `no_deployment` discriminator. Deploys finish in about a minute, so in practice you only see this immediately after creating the flow. And one removal: on-chain verifications keep **no off-chain proof record**, the chain is the record. The session's `storage.state` reports `'skipped'` for them. ## Billing Both modes follow the same billing rules: `valid` and `invalid` results are charged, infrastructure failures and test-environment verifications are not. See [Credits and usage](/docs/self-enterprise/billing/credits-and-usage/#per-verification-cost). --- # On-chain verification and SBTs Source: https://docs.self.xyz/docs/self-enterprise/flows/onchain-verification/ An [on-chain flow](/docs/self-enterprise/flows/verification-modes/) verifies proofs with a smart contract on **Celo** instead of Self's servers. Each user who passes receives a **soulbound token (SBT)**: a non-transferable token in their wallet that any contract, wallet, or explorer can read as proof they passed your flow. You never write, deploy, or fund a contract yourself. The dashboard deploys one per flow, and Self pays all gas. ## The contract When you deploy an on-chain flow, Self deploys a dedicated SBT contract for it, one per environment: | Environment | Network | Explorer | | ----------- | ------------ | ---------- | | Test | Celo Sepolia | Blockscout | | Live | Celo Mainnet | Celoscan | Both contracts are created from the same configuration in a single **Deploy** click. Each one enforces your flow's rules (age, country lists, OFAC, security level) directly on-chain, so the rules can't drift from what verifiers see. The **Test** and **Live** tabs show a contract card per environment with the **contract address**, the **scope seed** (the flow's unique proof scope), and the **deploy transaction**, each linked to the block explorer. ### Deploy status Deploys usually finish in under a minute, and the dashboard polls automatically. The Configure tab shows one combined status: | Status | Meaning | | ---------------------- | ---------------------------------------------------------------------------------------- | | **Deploying** | At least one network's contract is still deploying. | | **Deployed** | Both contracts are live. | | **Partially deployed** | One network succeeded, the other failed. Retry the failed one from its Test or Live tab. | | **Deploy failed** | Both failed. Retry from the Test and Live tabs. | Deploying and retrying require the **owner** or **admin** role; members see the status but must ask a manager to retry. Your flow's configuration is saved either way, a failed deploy never loses work, and share links plus the **Try this flow** callout simply stay locked until the test contract is live. ## The soulbound token The SBT is an ERC-721 token named **Self Verification SBT** (symbol `SELFSBT`). The name and symbol are the same for every flow; the contract address is what identifies _your_ flow. Its rules are strict by design: - **Non-transferable.** It can never be moved, sold, or approved to another address. There is no way to burn it. - **One per person.** The proof's [nullifier](/docs/self-enterprise/get-started/how-it-works/#one-account-per-person-nullifiers) is recorded on mint. A second verification attempt by the same person fails with _"You have already verified your identity. Only one verification is allowed per person."_ - **One per wallet.** A wallet that already holds the flow's SBT can't receive another. A rejected attempt (wrong document type, nationality not on your allowlist) doesn't consume the person's one verification, they can retry with a document that meets the rules. ## What your users experience ![The hosted page for an on-chain flow, with the connect-wallet step before the QR code](/docs/docs-assets/hosted-page-onchain.avif) The [hosted page](/docs/self-enterprise/get-started/how-it-works/#what-your-users-see-the-hosted-page) adds two wallet steps before the usual QR code: 1. **Connect a wallet**: the wallet that will receive the SBT. MetaMask, Rainbow, WalletConnect-compatible wallets, and browser-extension wallets are supported. 2. **Sign a gas-free message**: a signature that confirms this wallet as the recipient. It costs nothing and sends no transaction. Switching accounts mid-flow asks for a fresh signature. Then the user scans the QR code with the Self app as usual. Self submits the mint transaction; **the user never pays gas**. :::note The receiving wallet must be a standard wallet address (an EOA). Smart-contract wallets such as Safe can't receive the SBT. ::: ## Configuration limits Most rules translate to the contract one-to-one: minimum age, OFAC, security level, and country lists are all enforced on-chain. Two limits apply, checked by the dashboard before you deploy: - **Maximum age isn't enforceable on-chain.** A flow with a maximum-age rule can't be deployed in on-chain mode. - **Country lists are capped**: at most **40 excluded countries** or **41 included countries**. These are protocol limits and can't be raised. If a country allowlist is set, the contract checks the user's disclosed nationality, exactly as in backend mode. ## Reading results Every surface you already use gains the on-chain details: - **Webhooks**: `verification.completed` includes `verification_mode: 'onchain'`, and `tx_hash` plus `contract_address` when a token was minted. See the [event catalog](/docs/self-enterprise/webhooks/events/#on-chain-fields). - **API**: the session detail response includes `verification_mode` and an `onchain` object with the contract address, scope seed, and network. - **Activity log**: each row shows the mint transaction, linked to the explorer, and the proof-request popover shows the contract address. - **On-chain**: any contract can check `balanceOf(wallet)` on your flow's SBT contract to gate access. A `valid` on-chain verification means the transaction confirmed and the token was minted. A verification the contract rejects (an already-verified person, a disallowed document, a nationality outside your allowlist) is `invalid`; infrastructure failures are `error`. Billing follows the same rules as off-chain: `valid` and `invalid` are charged, `error` is not. :::note On-chain verifications keep no off-chain proof record: the chain is the record. The session's `storage.state` reports `'skipped'` for them, and no storage events fire. ::: ## Related - [Verification modes](/docs/self-enterprise/flows/verification-modes/): choosing between backend and on-chain. - [Test vs. live](/docs/self-enterprise/flows/test-vs-live/): Celo Sepolia vs. Celo Mainnet mirrors the test/live split. --- # Supported documents Source: https://docs.self.xyz/docs/self-enterprise/flows/supported-documents/ A user verifies with whichever supported document they hold, there's no per-flow document picker. What differs is the geographic footprint and which rules each document can satisfy. ## Biometric passport (ICAO 9303 e-passport) **Highest assurance.** A passport-chip read produces a credential signed by the issuing state. - **Coverage:** 60+ countries. See [Supported countries](/docs/self-enterprise/reference/supported-countries/). - **Can satisfy:** every rule, age, excluded countries, OFAC, and proof of human. - **UX:** the user holds the passport to the phone's NFC reader. About 30 seconds end to end. - **Notes:** needs an NFC-capable phone (most iPhones and Android 8+). ## Aadhaar (India) India's universal government ID. - **Coverage:** India only. - **Can satisfy:** age and proof of human. Does **not** support country rules (Aadhaar doesn't encode nationality the same way) or OFAC directly. - **UX:** the user scans the QR code on their Aadhaar and provides their share code. - **Notes:** see the [Aadhaar spec](/docs/self-enterprise/reference/document-specifications/aadhaar/) for the cryptographic detail. ## KYC attestation (partner-issued) A credential issued by a Self partner KYC provider after a remote KYC check. - **Coverage:** depends on the issuer. Most partners cover the US, EU, UK, and a long tail. - **Can satisfy:** depends on the issuer's schema. Typically age, country, OFAC, and proof of human. - **UX:** the user completes a KYC flow once with the partner; later verifications reuse the attestation. - **Notes:** see the [KYC spec](/docs/self-enterprise/reference/document-specifications/kyc/) for the attestation format. ## Which document satisfies which rule | Rule | Biometric passport | Aadhaar | KYC attestation | | ------------------ | :-----------------------------: | :-----------------------------: | :----------------------------------------------------------------: | | Minimum age | | | issuer-dependent | | Excluded countries | | | issuer-dependent | | OFAC | | | issuer-dependent | | Proof of human | | | issuer-attested | If a user's document can't satisfy a rule your flow requires (for example Aadhaar against a country rule), that verification can't pass, the user would need a document that does. ## Related - [Supported countries](/docs/self-enterprise/reference/supported-countries/): passport coverage by country, with the accepted signature algorithms. - [Aadhaar spec](/docs/self-enterprise/reference/document-specifications/aadhaar/): how Aadhaar verification works and what it can disclose. - [KYC spec](/docs/self-enterprise/reference/document-specifications/kyc/): the partner-issued KYC attestation in detail. --- # Test vs. live Source: https://docs.self.xyz/docs/self-enterprise/flows/test-vs-live/ Two isolated environments. Same dashboard, same SDK, different keys. ## Why two environments - **Test** is for development. Verify with mock passports, iterate on rules and webhooks, never bill credits. - **Live** is for production. Real documents, real proofs, real billing. The environments don't share data, flows, sessions, or webhook endpoints. ## How they're separated - **API keys** are bound to one environment, reflected in their prefix: `sk_test_...` vs. `sk_live_...`. **The key is what decides the environment**: a session created with a test key is a test session, one created with a live key is a live session. - **Flows** serve both environments. A deployed configuration has a single `flowId` that works with either key. You don't create a separate flow for live. The workspace's **Test** and **Live** tabs show the same `flowId` with that environment's credentials and webhooks. - **Webhook endpoints** are per-environment (set on the Test/Live tab). Set up `https://staging.example.com/webhooks/self` for test and `https://prod.example.com/webhooks/self` for live, with separate secrets. Test sessions only deliver to test endpoints. - **Billing** never applies in test. Test verifications consume no credits, and credit balance is for live only. - **On-chain flows** map the split to networks: test sessions verify on **Celo Sepolia**, live sessions on **Celo Mainnet**, each against its own [contract](/docs/self-enterprise/flows/onchain-verification/#the-contract). ## Mock passports In test, you verify with a **mock passport** created in the Self app, with attributes you control (nationality, age, OFAC status). Mock credentials are cryptographically distinct from real ones and never verify against a live flow. See [Using mock passports](/docs/self-enterprise/guides/using-mock-passports/) for the steps. ## Going live The same `flowId` serves both environments, so going live is a key swap, not a re-deploy: 1. Generate a live API key under **Developer → API keys** (the **Live** tab). 2. Register your production webhook endpoint on the **Live** tab, with its own signing secret. 3. Point your backend at the live key (`sk_live_…`) and the live webhook secret. The `flowId` stays the same. For an on-chain flow, confirm the **Live** tab's contract shows **Deployed**: live sessions verify against the Celo Mainnet contract. ## Common test-mode patterns ### Run an integration test in CI ```ts // In CI environment: process.env.SELF_API_KEY = 'sk_test_...'; process.env.SELF_FLOW_ID = ''; process.env.SELF_WEBHOOK_SECRET = 'whsec_...'; // The test creates a session, simulates a mock-passport completion, // and asserts the webhook handler fires with the expected payload. ``` ### Run a staging environment Use test keys + a test webhook endpoint, pointing at your staging backend. Real verification flows for your QA team, no production billing exposure. ## What to never do - **Don't put a live key in a test environment.** Test environments tend to log requests, screenshot UIs, and store payloads in dev databases. A live key in any of those places is a security incident. - **Don't reuse a webhook signing secret across environments.** Each endpoint gets its own. Mixing them is how you end up running test events against your prod database. --- # Using mock passports Source: https://docs.self.xyz/docs/self-enterprise/guides/using-mock-passports/ Mock passports are how you exercise verification end-to-end without a real passport. Useful for development, CI, and demos. Mock passports only work against flows in your **test** environment (i.e. sessions created with an `sk_test_` key). They never verify against live flows. See [Test vs. live](/docs/self-enterprise/flows/test-vs-live/). ## Generating a mock passport 1. On the Self app's home screen, tap the **settings gear** in the top-right corner.
Self app home screen with the settings gear in the top-right corner
2. In Settings, tap **Manage ID documents**.
Self app settings with the Manage ID documents option
3. Tap **Generate Mock Document**. The Mock Document screen opens with a preview of the dev passport.
Mock Document screen with the generate mock document data card and a dev passport preview
4. Under **Mock Document Parameters**, set the attributes you want to test against (nationality, age, document expiry, OFAC status), then tap **Generate Mock Document**.
Mock document parameters: nationality, age, document expiry, and OFAC status, with the Generate Mock Document button
The generated document appears as a card on the home screen, ready to use. To try the whole loop end-to-end without writing any code, deploy a configuration and use the **Try this flow** link on its Configure tab; test sessions accept mock passports. To stop using a mock passport, select a different document from the home screen. Real documents are added from the same **Manage ID documents** screen via **Add New Document**. --- # Plans Source: https://docs.self.xyz/docs/self-enterprise/billing/plans/ Three tiers. Manage yours on the **Settings → Usage & Billing** tab via **Manage plan**. Concrete prices and credit amounts are shown there; the shape below is what each tier unlocks. ![Plans](/docs/docs-assets/enterprise-plans.avif) ## Free The default plan, for evaluation and small projects. - **Credits**: a one-time signup grant that **never expires and never renews**. It's granted once per person, to your default organization when your account is created. - **Workspaces**: Pre-KYC, Age Verification, Proof of Human, Sovereign. - Test and live environments, plus webhooks. ## Starter The self-serve paid tier, subscribe from **Manage plan**. - **Billing**: a fixed fee, billed **monthly or annually** (you choose at checkout). - **Credits**: a monthly allotment that **resets each billing cycle**. - **Workspaces**: the same four as Free. What Starter buys you is the renewing credit allotment, not extra workspaces. ## Enterprise For volume and regulated environments. Arranged with sales, there's no self-serve checkout. - **Credits**: custom grants. - **Everything in Starter**, plus Custom Workspaces (including [Custom Config](/docs/self-enterprise/workspaces/custom-config/)), Mobile SDK, SLA and uptime guarantees, Agentic ID, Self Perks, and Audit Tooling (coming soon). :::cta{title="Need Enterprise?" href="mailto:sales@self.xyz" button="Talk to sales"} Custom credit grants, Custom Config, Mobile SDK, Agentic ID, and SLA-backed uptime. ::: ## Switching plans Upgrade, change tier, or cancel from **Settings → Usage & Billing** ([the tab in detail](/docs/self-enterprise/dashboard/billing/#actions)). Checkout runs through Stripe, and your cycle's credit allotment is set when the subscription invoice is paid. ## What you don't pay for Test traffic, webhook deliveries (bundled into the per-verification cost), and sessions that end `expired` or `error`. Which results bill and which don't is covered in [Credits and usage](/docs/self-enterprise/billing/credits-and-usage/#what-youre-invoiced-for). ## Related - [Credits and usage](/docs/self-enterprise/billing/credits-and-usage/): how the per-verification cost works. - [Dashboard: Billing](/docs/self-enterprise/dashboard/billing/). --- # Credits and usage Source: https://docs.self.xyz/docs/self-enterprise/billing/credits-and-usage/ ## Credits Usage is measured in **credits**, an abstract unit. How many you have depends on your plan: - **Free**: a one-time grant that never renews. - **Starter**: a monthly allotment that resets each billing cycle. - **Enterprise**: custom grants. See [Plans](/docs/self-enterprise/billing/plans/). ## Per-verification cost Each verification costs a fixed number of credits, decided by the workspace. The cost is locked in when the session is created (so an in-flight session keeps the price it started with), but the credit is **charged when the verification completes**, not at creation: | Workspace | Credit cost | | ---------------- | ----------- | | Pre-KYC | 25 | | Age Verification | 10 | | Proof of Human | 10 | | Sovereign | 10 | | Custom Config | 25 | The dashboard shows the cost when you configure a workspace. To estimate spend, multiply your expected verifications by the workspace's cost. For [on-chain flows](/docs/self-enterprise/flows/verification-modes/), the charge lands once the transaction's **result is known on-chain**: a confirmed mint (`valid`) or a contract rejection (`invalid`). An attempt that fails for infrastructure reasons is never charged. ## What you're invoiced for Your invoice (the metered usage Self bills) counts verifications that produced a result, not sessions the user abandons: | Session result | On your invoice? | | ----------------------------------- | ---------------- | | `valid` | Yes | | `invalid` (a rule failed) | Yes | | `error` (technical failure) | No | | `expired` (never started/completed) | No | :::note If an on-chain session expires while its mint transaction is still confirming, the late confirmation flips it from `expired` to `valid`, and it's charged once, like any other `valid` result. ::: ## Insufficient credits If your balance can't cover a session's cost, `sessions.create(...)` is rejected with HTTP `402` (the SDK throws `SelfApiError` with `statusCode: 402`, and `details` carries `balance`, `required`, and `planTier`). The session is not created. To add capacity, upgrade your plan from **Manage plan** (or talk to sales about Enterprise). Watch the credits meter on the **Settings → Usage & Billing** tab so this doesn't surprise you. ## Test environment Test verifications (sessions created with an `sk_test_` key) never consume credits. Use test freely. :::note Session **creation** still checks your balance in both environments. If your balance is fully exhausted, creating new test sessions can be rejected with `402` until you top up. ::: ## Related - [Plans](/docs/self-enterprise/billing/plans/): what each tier includes. - [Dashboard: Billing](/docs/self-enterprise/dashboard/billing/): the Usage & Billing tab. --- # From the open-source SDK Source: https://docs.self.xyz/docs/self-enterprise/migration/from-self-pass-sdk/ If you've integrated the open-source [Self Pass](/docs/self-pass/) frontend SDK (`@selfxyz/qrcode`) and backend verifier (`@selfxyz/core`), here's how it maps to Self Enterprise. ## Why migrate The open-source SDK requires you to: - Stand up an off-chain verifier yourself (or run it in-process and pay the cold-start cost). - Manage a `ConfigStore` for predicate configurations. - Build your own webhook delivery, retry, and signature scheme. - Write your own audit log. - Operate it all with no SLA. Enterprise replaces all of that with a managed service. Your frontend integration stays largely identical; the backend collapses. ## Concept mapping | Open-source self-pass | Self Enterprise | | ------------------------------------------ | --------------------------------------------------------------------------------------------------------- | | `SelfAppBuilder` (frontend) | Same idea, but the `endpoint` becomes our hosted verifier, you don't host it. | | `SelfBackendVerifier` | Replaced by our managed verifier. You don't run this anymore. | | `ConfigStore` (your `IConfigStorage` impl) | Replaced by the dashboard's [flow configuration](/docs/self-enterprise/dashboard/configure-a-workspace/). | | Predicate config object | A **flow** in the dashboard. `flowId` replaces inline config. | | Per-app verifier secrets | A Bearer API key (`sk_live_…`). | | Custom webhook code | A webhook endpoint you register, verified via `SelfWebhooks.verify(...)`. | ## Step-by-step ### 1. Map your predicate config to a flow Today (open-source): ```ts import { AllIds, DefaultConfigStore, SelfBackendVerifier } from '@selfxyz/core'; const verifier = new SelfBackendVerifier( 'my-app', // scope 'https://myapp.com/verify', // endpoint false, // mock passports? AllIds, // accepted document types new DefaultConfigStore({ minimumAge: 18, excludedCountries: ['IRN'], ofac: true }), 'uuid', // user-identifier type ); ``` With Enterprise: 1. In the dashboard, [create a flow](/docs/self-enterprise/dashboard/configure-a-workspace/). 2. Configure the same rules: minimum age 18, exclude `IRN`, OFAC on. 3. Deploy it. Copy the `flowId` from the **Test** or **Live** tab. ### 2. Replace `SelfBackendVerifier` with `SelfClient` Today: ```ts import { SelfBackendVerifier } from '@selfxyz/core'; const verifier = new SelfBackendVerifier(/* scope, endpoint, ... */); app.post('/verify', async (req, res) => { const result = await verifier.verify( req.body.attestationId, req.body.proof, req.body.publicSignals, req.body.userContextData, ); // ...act on result }); ``` With Enterprise: ```ts import { SelfClient } from '@selfxyz/enterprise-sdk'; const self = new SelfClient({ apiKey: process.env.SELF_API_KEY! }); // When the user wants to verify (e.g. app/api/start-verification/route.ts): export async function POST(req: Request) { const { userId } = await req.json(); const session = await self.sessions.create({ flowId: process.env.SELF_FLOW_ID!, externalUuid: userId, }); return Response.json({ verificationUrl: session.verificationUrl }); } // You no longer need a /verify endpoint, we deliver the result via webhook. ``` ### 3. Replace inline verification with a webhook handler Today your `/verify` endpoint runs the Groth16 verifier and acts on the result inline. With Enterprise, register a webhook endpoint in the dashboard (it receives all events) and handle `verification.completed`: ```ts // app/api/webhooks/self/route.ts import { SelfWebhooks } from '@selfxyz/enterprise-sdk'; export async function POST(req: Request) { const event = SelfWebhooks.verify( await req.text(), Object.fromEntries(req.headers), process.env.SELF_WEBHOOK_SECRET!, ); if (event.type === 'verification.completed' && event.status === 'valid') { grantAccess(event.external_uuid, event.proof_attributes); } return new Response('ok', { status: 200 }); } ``` `external_uuid` is what you passed when creating the session (typically your user ID). See [Verify webhooks](/docs/self-enterprise/webhooks/verify-webhooks/). ### 4. Drop your frontend QR component With Enterprise you don't render your own QR. Wherever you used `@selfxyz/qrcode`, call `self.sessions.create(...)` on your backend and **redirect the user to `session.verificationUrl`** instead; Self's hosted page renders the QR and handles deeplinks. Then remove `@selfxyz/qrcode`. ### 5. Delete the old code paths - Remove `@selfxyz/core` and `@selfxyz/qrcode`. - Remove your `ConfigStore` implementation. - Remove your `/verify` route, Enterprise delivers via webhook. - Remove any code that loaded verifier circuit files at boot. ## What stays the same - The **Self mobile app** is unchanged for your users. - The **disclosures** are the same, age, nationality, OFAC, etc. See [Disclosures](/docs/self-enterprise/flows/disclosures/). - The **proof system** is the same under the hood. - Your **frontend** gets simpler: send the user to the hosted `verificationUrl`, no QR rendering required. ## What's better - No verifier infrastructure to run. - [Audit log](/docs/self-enterprise/dashboard/activity-log/) out of the box. - [Webhook delivery](/docs/self-enterprise/webhooks/overview/) with automatic retries. - A [dashboard](/docs/self-enterprise/dashboard/overview/) to change rules without a redeploy. - [Per-flow versioning](/docs/self-enterprise/flows/anatomy/#immutable-once-deployed), audit exactly what a user was verified against last March. ## What's different - You pay per verification. See [Plans](/docs/self-enterprise/billing/plans/). - Your service no longer holds the raw proof, we verify it and hand you the attributes. (You can still read the raw proof from the `verification.completed` event if you truly need it.) ## Rolling out safely 1. Build the Enterprise integration behind a feature flag (`enterprise: false`). 2. Configure a **test**-environment flow + webhook endpoint. 3. QA against [mock passports](/docs/self-enterprise/guides/using-mock-passports/). 4. Move staging to Enterprise. 5. Cut over prod with a percentage rollout (10% → 50% → 100%). 6. Decommission your verifier infra after a week of clean prod traffic. :::cta{title="Need help?" href="mailto:team@self.xyz" button="Email support"} Questions about your migration? Reach out to support, or to your CSM if you're on an Enterprise plan. ::: --- # Document specifications Source: https://docs.self.xyz/docs/self-enterprise/reference/document-specifications/ Technical specifications for the credential types Self Enterprise verifies. These pages explain the cryptographic and protocol-level shape of each document, useful when integrating, auditing, or debugging document-specific behavior. - [Aadhaar](/docs/self-enterprise/reference/document-specifications/aadhaar/): India's national ID, with ZK-friendly signature verification. - [KYC](/docs/self-enterprise/reference/document-specifications/kyc/): partner-issued KYC attestations. For an overview of which documents support which disclosures, see [Supported documents](/docs/self-enterprise/flows/supported-documents/). For country coverage, see [Supported countries](/docs/self-enterprise/reference/supported-countries/). --- # Aadhaar Source: https://docs.self.xyz/docs/self-enterprise/reference/document-specifications/aadhaar/ ## What is Aadhaar? Aadhaar is India’s national digital identity system maintained by UIDAI (Unique Identification Authority of India). - It is a **12-digit unique identity number** issued to residents of India, based on their biometric and demographic data. - Aadhaar serves as a **foundational ID** that can be used to verify identity across financial, telecom, and government services. - Verification is done via UIDAI APIs or Aadhaar-linked services, such as QR codes. :::warning UIDAI does not sign the entire Aadhaar number, only the last 4 digits. As this does not have enough entropy to create a nullifier we also use other fields such as the name, date of birth, and gender. This means Aadhaar-derived uniqueness is weaker than passport-derived uniqueness: a person can change their Aadhaar and be considered a "new" person in the protocol. Although we have chosen fields that are hard to change, edge cases do pop up, such as people changing their names due to marriage, divorce, or other legal corrections. If sybil resistance is critical to your use case, contact us before relying on Aadhaar alone. ::: ## Registering with Aadhaar on Self Self allows users to prove identity using Aadhaar in a **privacy-preserving** way. Registration uses the **unmasked e-Aadhaar PDF**, which you can obtain in two ways: 1. #### Using the Aadhaar app 1. #### Download the official **Aadhaar app** from UIDAI ([Android](https://play.google.com/store/apps/details?id=in.gov.uidai.pehchaan)/[iOS](https://apps.apple.com/in/app/aadhaar/id6744029871)). 2. #### Sign in and tap **Download Aadhaar**.
The Download Aadhaar action in the UIDAI Aadhaar app

Download Aadhaar

3. #### Choose **Unmasked Aadhaar** and download the PDF.
The Masked / Unmasked Aadhaar selector with Unmasked Aadhaar selected

Select "Unmasked Aadhaar"

4. #### Upload that PDF in the Self app to register. 1. #### Self processes the PDF to derive a unique **nullifier** (an identifier) that allows users to prove uniqueness without sharing their Aadhaar number directly. 2. #### By using the UIDAI website 1. #### Go to the [UIDAI website](https://myaadhaar.uidai.gov.in/genricDownloadAadhaar/en) 2. #### Enter your Aadhaar number, and verify yourself with an OTP to get a PDF. Do not select the masked Aadhaar option, as the download must be unmasked. 3. #### Upload that PDF in the Self app to register. :::caution Only the **unmasked** e-Aadhaar is accepted for verification. Uploading a QR code image is no longer supported, so upload the PDF itself. The older **mAadhaar** app is a separate app that is deprecated and no longer usable. Use the links above. ::: :::note The password to the PDF in both cases is the first four letters of your name in caps following by your birth year. ::: ## Working with attributes in Aadhaar Since Aadhaar uses a different format than passports, keep in mind that some of the disclosed attribute formats can be different. ### `GenericDiscloseOutputV2`
Field TypeFormatting / Value
forbiddenCountriesListPacked
uint256[4]Packed bytes of all countries into an array 4 uint256 numbers.
issuingState
stringVaries**
name
string[]First value contains the entire name (and it may vary)
idNumber
stringLast 4 letters of the aadhaar number.
nationality
string'IND'
dateOfBirth
stringDD-MM-YYYY
gender
string'M' | 'T' | 'F'
expiryDate
string'UNAVAILBLE'
olderThan
uint2560 - 99
ofac
bool[3]Each value is true if ofac check is enabled.
### `GenericDiscloseOutput`
FieldTypeFormatting / Value
forbiddenCountriesListPacked
string[]Packed bytes list of forbidden countries
issuingState
stringVaries
name
stringFull name. Can vary as well
idNumber
stringLast 4 digits of aadhaar
nationality
string'IND'
dateOfBirth
stringYYYYMMDD
gender
string'M' | 'T' | 'F'
expiryDate
string'UNAVAILABLE'
minimumAge
string'00'-'99'
ofac
bool[]True if in ofac
:::note \*\*Varies refers to a string being all caps / lowercase / first letter in caps and the rest in lowercase. ::: --- # KYC Source: https://docs.self.xyz/docs/self-enterprise/reference/document-specifications/kyc/ Self supports a KYC document type for identity verification. ## Overview KYC is represented in verification payloads as attestation ID `4`. ## User flow 1. User selects `kyc` as document type in the Self app. 2. The app starts the partner KYC flow. 3. After completion, verification is finalized asynchronously (websocket/push). 4. The app stores the resulting KYC attestation and can generate disclose proofs. ## What you receive When verification completes against a flow that allows KYC, the `verification.completed` webhook fires with `proof_attributes` describing the disclosed predicates exactly as it would for any other document type. There's no per-flow document picker: a KYC attestation is accepted whenever the flow's **Security level** is **Standard** (Hi-security flows require a chip-backed document). See [Supported documents](/docs/self-enterprise/flows/supported-documents/) for the capability matrix. ## Notes - KYC follows the same proof and verification path as other document types. - Standard disclosure fields and config checks (age, OFAC, country restrictions) apply. --- # Self Map countries list Source: https://docs.self.xyz/docs/self-enterprise/reference/supported-countries/ ## Supported Countries Self supports electronic passport and identity document verification across multiple countries worldwide. Our coverage includes both Document Signer Certificates (DSC) and Country Signing Certificate Authority (CSCA) support. :::cta{title="Interactive coverage map" href="https://map.self.xyz/" button="Open the map"} For a visual representation of our global coverage, explore the interactive map. ::: ### Country Support Status We currently support document verification for the following countries: #### Full Support (Both DSC and CSCA) These countries have both Document Signer Certificates and Country Signing Certificate Authority support: | Country | Code | Region | | ----------------------- | ---- | --------------- | | 🇦🇪 United Arab Emirates | AE | Middle East | | 🇦🇷 Argentina | AR | South America | | 🇦🇹 Austria | AT | Europe | | 🇦🇺 Australia | AU | Oceania | | 🇧🇬 Bulgaria | BG | Europe | | 🇧🇭 Bahrain | BH | Middle East | | 🇧🇯 Benin | BJ | Africa | | 🇧🇷 Brazil | BR | South America | | 🇧🇿 Belize | BZ | Central America | | 🇨🇦 Canada | CA | North America | | 🇨🇭 Switzerland | CH | Europe | | 🇨🇲 Cameroon | CM | Africa | | 🇨🇳 China | CN | Asia | | 🇨🇴 Colombia | CO | South America | | 🇨🇿 Czech Republic | CZ | Europe | | 🇩🇪 Germany | DE | Europe | | 🇪🇸 Spain | ES | Europe | | 🇪🇺 European Union | EU | Europe | | 🇫🇮 Finland | FI | Europe | | 🇫🇷 France | FR | Europe | | 🇬🇧 United Kingdom | GB | Europe | | 🇭🇺 Hungary | HU | Europe | | 🇮🇩 Indonesia | ID | Asia | | 🇮🇪 Ireland | IE | Europe | | 🇮🇶 Iraq | IQ | Middle East | | 🇮🇷 Iran | IR | Middle East | | 🇮🇸 Iceland | IS | Europe | | 🇮🇹 Italy | IT | Europe | | 🇯🇵 Japan | JP | Asia | | 🇰🇷 South Korea | KR | Asia | | 🇱🇺 Luxembourg | LU | Europe | | 🇲🇦 Morocco | MA | Africa | | 🇲🇩 Moldova | MD | Europe | | 🇲🇳 Mongolia | MN | Asia | | 🇲🇽 Mexico | MX | North America | | 🇲🇾 Malaysia | MY | Asia | | 🇳🇬 Nigeria | NG | Africa | | 🇳🇴 Norway | NO | Europe | | 🇳🇵 Nepal | NP | Asia | | 🇳🇿 New Zealand | NZ | Oceania | | 🇵🇦 Panama | PA | Central America | | 🇶🇦 Qatar | QA | Middle East | | 🇷🇴 Romania | RO | Europe | | 🇷🇼 Rwanda | RW | Africa | | 🇸🇪 Sweden | SE | Europe | | 🇸🇬 Singapore | SG | Asia | | 🇸🇰 Slovakia | SK | Europe | | 🇹🇭 Thailand | TH | Asia | | 🇹🇲 Turkmenistan | TM | Asia | | 🇹🇿 Tanzania | TZ | Africa | | 🇺🇦 Ukraine | UA | Europe | | 🇺🇸 United States | US | North America | | 🇺🇿 Uzbekistan | UZ | Asia | | 🇻🇳 Vietnam | VN | Asia | #### CSCA Support Only These countries have Country Signing Certificate Authority support: | Country | Code | Region | | ----------------------------------- | ---- | --------------- | | 🇦🇩 Andorra | AD | Europe | | 🇦🇬 Antigua and Barbuda | AG | Caribbean | | 🇦🇱 Albania | AL | Europe | | 🇦🇲 Armenia | AM | Asia | | 🇦🇿 Azerbaijan | AZ | Asia | | 🇧🇦 Bosnia and Herzegovina | BA | Europe | | 🇧🇧 Barbados | BB | Caribbean | | 🇧🇩 Bangladesh | BD | Asia | | 🇧🇪 Belgium | BE | Europe | | 🇧🇸 Bahamas | BS | Caribbean | | 🇧🇼 Botswana | BW | Africa | | 🇧🇾 Belarus | BY | Europe | | 🇨🇮 Côte d'Ivoire | CI | Africa | | 🇨🇱 Chile | CL | South America | | 🇨🇷 Costa Rica | CR | Central America | | 🇨🇾 Cyprus | CY | Europe | | 🇩🇰 Denmark | DK | Europe | | 🇩🇲 Dominica | DM | Caribbean | | 🇩🇿 Algeria | DZ | Africa | | 🇪🇨 Ecuador | EC | South America | | 🇪🇪 Estonia | EE | Europe | | 🇬🇪 Georgia | GE | Asia | | 🇬🇭 Ghana | GH | Africa | | 🇬🇲 Gambia | GM | Africa | | 🇬🇷 Greece | GR | Europe | | 🇭🇷 Croatia | HR | Europe | | 🇮🇱 Israel | IL | Middle East | | 🇮🇳 India | IN | Asia | | 🇯🇲 Jamaica | JM | Caribbean | | 🇰🇪 Kenya | KE | Africa | | 🇰🇳 Saint Kitts and Nevis | KN | Caribbean | | 🇰🇵 North Korea | KP | Asia | | 🇽🇰 Kosovo | KS | Europe | | 🇰🇼 Kuwait | KW | Middle East | | 🇰🇿 Kazakhstan | KZ | Asia | | 🇱🇧 Lebanon | LB | Middle East | | 🇱🇮 Liechtenstein | LI | Europe | | 🇱🇹 Lithuania | LT | Europe | | 🇱🇻 Latvia | LV | Europe | | 🇲🇨 Monaco | MC | Europe | | 🇲🇪 Montenegro | ME | Europe | | 🇲🇰 North Macedonia | MK | Europe | | 🇲🇹 Malta | MT | Europe | | 🇲🇻 Maldives | MV | Asia | | 🇳🇱 Netherlands | NL | Europe | | 🇴🇲 Oman | OM | Middle East | | 🇵🇪 Peru | PE | South America | | 🇵🇭 Philippines | PH | Asia | | 🇵🇱 Poland | PL | Europe | | 🇵🇸 Palestine | PS | Middle East | | 🇵🇹 Portugal | PT | Europe | | 🇵🇾 Paraguay | PY | South America | | 🇷🇸 Serbia | RS | Europe | | 🇷🇺 Russia | RU | Asia/Europe | | 🇸🇦 Saudi Arabia | SA | Middle East | | 🇸🇨 Seychelles | SC | Africa | | 🇸🇮 Slovenia | SI | Europe | | 🇸🇲 San Marino | SM | Europe | | 🇸🇳 Senegal | SN | Africa | | 🇸🇾 Syria | SY | Middle East | | 🇹🇯 Tajikistan | TJ | Asia | | 🇹🇱 Timor-Leste | TL | Asia | | 🇹🇷 Turkey | TR | Asia/Europe | | 🇹🇼 Taiwan | TW | Asia | | 🇺🇬 Uganda | UG | Africa | | 🇺🇾 Uruguay | UY | South America | | 🇻🇦 Vatican City | VA | Europe | | 🇻🇨 Saint Vincent and the Grenadines | VC | Caribbean | | 🇻🇪 Venezuela | VE | South America | | 🇿🇼 Zimbabwe | ZW | Africa | #### Special Territories and Organizations - **UN** - United Nations Laissez-Passer - **ZZ** - Unknown/Test Documents ### Technical Details #### What is DSC (Document Signer Certificate)? Document Signer Certificates are used to digitally sign the data stored in electronic passports and identity documents. They ensure the authenticity and integrity of the document data. #### What is CSCA (Country Signing Certificate Authority)? The Country Signing Certificate Authority is the root certificate authority for a country's electronic document infrastructure. It issues certificates to Document Signers. #### Supported Cryptographic Standards Our system supports various cryptographic algorithms including: - **Signature Algorithms**: RSA, RSAPSS, ECDSA - **Hash Algorithms**: SHA-1, SHA-256, SHA-384, SHA-512 - **Key Lengths**: 1024-6144 bits for RSA, 224-521 bits for ECDSA ### Data Sources This information is compiled from our certificate registry: - [DSC Registry Data](https://raw.githubusercontent.com/selfxyz/self/52dba2742b4c37a957eb5ab8ebee83fdccdcf187/registry/outputs/map_dsc.json) - [CSCA Registry Data](https://raw.githubusercontent.com/selfxyz/self/52dba2742b4c37a957eb5ab8ebee83fdccdcf187/registry/outputs/map_csca.json) ### Updates This list is regularly updated as new countries adopt electronic document standards and as we expand our certificate registry. For the most current information, please check our [Interactive Coverage Map](https://map.self.xyz/). --- --- # Troubleshooting Source: https://docs.self.xyz/docs/self-enterprise/reference/troubleshooting/ Things that commonly go wrong, and how to unstick them. Errors from the SDK arrive as `SelfApiError` (`err.statusCode`, `err.code`); the sections below are keyed by those. If none of these apply, email team@self.xyz with your org ID (from the dashboard URL) and, for a webhook issue, the `verification_id` from the event payload. ## Authentication ### `401 unauthenticated` The API key is missing, malformed, or revoked. - Confirm the `apiKey` you pass to `SelfClient` is set and is the full value (it starts with `sk_test_` or `sk_live_`). - If it works locally but 401s in deploy, you're probably reading the wrong env var. If it worked before and suddenly 401s, the key was likely revoked. - The full key is shown once on creation. If you've lost it, generate a new one under **Developer → API keys** and revoke the old one. ### `403 forbidden` The key is recognized but blocked at the key layer (for example a disabled key). Generate a fresh key under **Developer → API keys** and retire the old one. Note that a `flowId` belonging to another organization returns `404 not_found`, not `403`: lookups are scoped to the key's org, so a foreign ID is indistinguishable from a nonexistent one. And there's no test-vs-live mismatch to debug at the flow level, the environment comes from the key itself (`sk_test_` vs `sk_live_`), and a flow is the same ID on both tabs. ## API requests ### `400 validation_failed` The request body didn't match the schema. The `details.issues` array tells you what. ```json { "error": { "code": "validation_failed", "details": { "issues": [ { "path": ["externalUuid"], "message": "String must contain at least 1 character(s)" } ] } } } ``` Fix the field at `path`. Don't retry on `400`, the request will keep failing. ### `404 not_found` The `flowId` (or session ID) is wrong, points at a config that was never deployed, or the flow is archived. A flow that exists but has no deployed version returns `409 conflict` instead. - Open the flow in the dashboard. If you don't see a "Deployed" badge, click **Deploy** (otherwise you'll get `409 conflict`). - If the flow was deleted, recreate it; the ID is gone. For an [on-chain flow](/docs/self-enterprise/flows/onchain-verification/), a `409 conflict` with `details.discriminator: 'no_deployment'` means the flow's contract isn't deployed for that environment yet. Deploys finish in about a minute; check the status on the flow's Test or Live tab and retry a failed deploy from there. ### `402`, insufficient credits Your org's credit balance is too low to cover this session's cost, so the [credit gate](/docs/self-enterprise/billing/credits-and-usage/#insufficient-credits) blocked it. (The error envelope carries `code: "unauthenticated"` with HTTP `402`, branch on the status, not the code.) - Check your balance on the **Settings → Usage & Billing** tab. - On Free, the signup grant never renews. Once it's spent, upgrade to Starter. On Starter, the allotment resets each billing cycle; upgrade your plan (or talk to sales about Enterprise) for more capacity. - Watch the credits meter on that tab so this doesn't surprise you. ### `429 rate_limited` Per-API-key rate limit exceeded. Honor the `Retry-After` header. - If your traffic legitimately exceeds the limit, contact sales@self.xyz about higher limits or move to Enterprise. - The SDK doesn't retry, your code should back off and retry on `429`. ### `5xx` errors Transient. Retry with exponential backoff. If it's persistent (more than 30 seconds), check [status.self.xyz](https://status.self.xyz) and contact support with the request ID. ## Webhooks ### Webhook handler never receives events - Confirm the endpoint is registered under **Developer → Webhooks** for the **same environment** as the key creating sessions (test vs live). - Confirm your endpoint is reachable from the public internet (not behind a VPN, no firewall blocking POST). - If using a tunnel (ngrok, Cloudflare Tunnel), confirm the tunnel is still up. - The delivered event is `verification.completed`; branch on `event.type` so a future event type doesn't surprise your handler. ### Webhook deliveries fail with `400` Your handler is rejecting the delivery. Most likely: - **Signature verification failed.** Check that: - You're using the _raw_ body, not a JSON-parsed object. - The signing secret matches the dashboard's `whsec_...` for this endpoint. - You're not rewriting headers between proxy and handler. - **Body schema mismatch.** Update to the latest SDK version, we may have added an event type your version doesn't know. ### Webhook deliveries fail with `5xx` Your handler is erroring before completing. Find the matching invocation in your logs by the `verification_id` from the event payload. Self retries `5xx`, so once you fix the handler the next retry should land. ### Duplicate events Expected. We deliver at-least-once. See [Best practices](/docs/self-enterprise/webhooks/best-practices/#1-make-handlers-idempotent) for dedup patterns. ## SDK ### `Cannot find module '@selfxyz/enterprise-sdk'` The package is ESM-only and requires Node 20+. - Confirm Node version (`node -v`). - Confirm your `package.json` has `"type": "module"`, or you're using `.mjs` files, or your build system supports ESM. - TypeScript: `"module": "NodeNext"` or `"ESNext"` in `tsconfig.json`. ### Webhook verification throws `WebhookVerificationError` The body or headers were modified between Self and your handler. The usual culprit is JSON-parsing the body before verification: the raw bytes we signed are gone. In a Next.js route handler, read the body with `await req.text()` and never call `req.json()` before `verify(...)`. For the full list (proxy rewrites, wrong secret, clock skew), see [Verify webhooks](/docs/self-enterprise/webhooks/verify-webhooks/#common-failure-modes). ### Types don't narrow on `event.type` ```ts if (event.type === 'verification.completed') { event.status; // TS error? } ``` - Confirm you're importing `WebhookEvent` from `@selfxyz/enterprise-sdk` (which is the discriminated union), not constructing it yourself. - If you're using older TypeScript (<4.4), narrowing on discriminated unions may need explicit type guards. ## Mock passports In a **test** environment (sessions created with an `sk_test_` key) you verify with a mock passport from the Self app instead of a real document. See [Using mock passports](/docs/self-enterprise/guides/using-mock-passports/) for the setup. Mock credentials are cryptographically distinct from real ones and never verify against a live flow. ## When all else fails Email team@self.xyz with: - Your Email. - The `err.code` and `err.message` from the `SelfApiError`, or the `verification_id` from the event for a webhook issue. - What you expected vs. what happened, with approximate timestamps. The more of that you include, the faster we can trace it. ## Related - [SDK error handling](/docs/self-enterprise/sdk/error-handling/). - [Webhook best practices](/docs/self-enterprise/webhooks/best-practices/). - [Test vs. live](/docs/self-enterprise/flows/test-vs-live/). --- # Overview Source: https://docs.self.xyz/docs/self-connect/overview/ ## What is Self Connect Self Connect is an open-source protocol that maps off-chain personal identifiers (such as phone numbers, Twitter handles, email addresses, etc.) to on-chain blockchain addresses. It enables a convenient and interoperable user experience by allowing users to discover and transact with each other using familiar identifiers instead of complex hexadecimal addresses. Self Connect uses a federated model, meaning that anyone has the power to be an issuer of attestation mappings. Issuers have the freedom to decide how to verify that users actually have ownership of their identifiers. After verification, issuers register the mapping as an attestation to the on-chain smart contract registry. ## Use Cases ### 1. Payments to Phone Numbers Send cryptocurrency directly to a friend's phone number without needing to know their wallet address. **Example:** A Kaala wallet user can send funds to a Libera wallet user using only their phone number. ### 2. Social Discovery Find someone's blockchain account based on their social media handle or other identifiers. **Example:** Discover a user's wallet address by their Twitter handle to send them tokens or NFTs. ### 3. Cross-Wallet Interoperability Enable seamless transactions across different wallet applications by mapping identifiers to addresses. **Example:** Users registered in different wallet ecosystems can find and transact with each other using shared identifiers. ### 4. MiniPay Integration MiniPay leverages Self Connect to enable phone number-based transactions, making cryptocurrency payments as simple as traditional mobile money transfers. ### 5. Social Applications Build social applications where users can connect using familiar identifiers instead of wallet addresses. ### 6. Contact List Integration Import phone contacts and automatically discover which contacts have cryptocurrency wallets. ## Why Self Connect ### Privacy Preservation Identifiers are obfuscated before being stored on-chain, so users' sensitive information remains private. How this works, and why no single party can reverse it, is covered in [ODIS](/docs/self-connect/odis/). ### Open and Decentralized * **Permissionless:** Anyone can become an issuer * **Federated:** No central authority controls attestations * **Open Source:** Full transparency and community-driven development * **Composable:** Applications can build on existing attestations ### Cost-Effective Self Connect is economical for issuers: * **10 cUSD of ODIS quota = 10,000 user attestations** * Lookup operations require no gas (only ODIS quota) * Registration can be paid by either issuer or user ### Flexible Verification Issuers have complete control over their verification methods: * SMS verification for phone numbers * OAuth for social accounts * Email verification * Custom verification logic ### Interoperability Standard identifier prefixes enable cross-application compatibility: * Attestations from one issuer can be used by multiple applications * Users don't need to re-verify for each application * Ecosystem-wide identifier mapping ## Architecture Overview Self Connect consists of three main components: 1. **[FederatedAttestations Smart Contract](/docs/self-connect/protocol-architecture/)**: the on-chain registry where attestation mappings are stored 2. **[ODIS](/docs/self-connect/odis/)** (Oblivious Decentralized Identifier Service): a decentralized service that provides privacy-preserving identifier obfuscation 3. **[Issuer Infrastructure](/docs/self-connect/becoming-an-issuer/)**: applications or services that verify identifier ownership and register attestations on-chain ## Getting Started To integrate Self Connect: 1. **As an Application:** Choose trusted issuers and [lookup attestations](/docs/self-connect/registration-and-lookup/) 2. **As an Issuer:** [Set up verification infrastructure](/docs/self-connect/becoming-an-issuer/) and register attestations 3. **As a User:** Get verified by issuers to map your identifiers to your address Continue with [Key Concepts](/docs/self-connect/key-concepts/), or jump straight into the [Developer Guide](/docs/self-connect/getting-started/). --- # Key Concepts Source: https://docs.self.xyz/docs/self-connect/key-concepts/ ## Plaintext Identifiers A `plaintextIdentifier` is any string of text that a user can use to identify another user. This makes it easier to represent an EVM-based address in a human-readable format. **Examples:** * Phone number: `+12345678901` * Twitter handle: `@alice` * Email address: `alice@example.com` * GitHub username: `alicecodes` ## Obfuscated Identifiers An `obfuscatedIdentifier` is the identifier used on-chain, to which the account address is mapped. It preserves user privacy by not revealing the underlying plaintext identifier. The obfuscated identifier is obtained by hashing the plaintext identifier, identifier prefix, and pepper using the following schema: ``` sha3(sha3({prefix}://{plaintextIdentifier})__{pepper}) ``` This ensures that even if someone monitors on-chain attestation events, they cannot determine which phone number or social handle belongs to which address without access to the pepper. The full derivation is described in [Identifier Obfuscation](/docs/self-connect/identifier-obfuscation/). ## Identifier Prefix Identifier prefixes are used to differentiate users having the same plaintext identifier for different purposes and to enable composability across applications. **Example:** Consider Alice having the same username on both Twitter and GitHub: `alicecodes` * Twitter identifier: `twit://alicecodes` * GitHub identifier: `github://alicecodes` By using prefixes, dApps can differentiate between verification methods. If dApps follow a standard prefix convention, the corresponding obfuscated identifiers will be consistent, making it easier to lookup identifiers verified by different issuers. **Standard Prefixes:** These are the prefix values defined by the SDK's `IdentifierPrefix` enum. Note that the enum key and its value differ, so `IdentifierPrefix.PHONE_NUMBER` resolves to `tel`. * Phone numbers: `tel` (`IdentifierPrefix.PHONE_NUMBER`) * Twitter: `twit` (`IdentifierPrefix.TWITTER`) * GitHub: `github` (`IdentifierPrefix.GITHUB`) * Email: `mailto` (`IdentifierPrefix.EMAIL`) ## Pepper A `pepper` is a unique secret obtained by taking the first 13 characters of the SHA256 hash of the unblinded signature from [ODIS](/docs/self-connect/odis/) (Oblivious Decentralized Identifier Service). The pepper is crucial for privacy preservation because: * No single party can compute it unilaterally * It prevents rainbow table attacks * It's unique per identifier ## Unblinded Signature The unblinded signature is obtained by unblinding the signature returned by ODIS, which is the combined output comprised of signatures from multiple ODIS signers. ## Issuers An **issuer** is an entity willing to take on the responsibility of verifying a user's ownership of an identifier. Issuers: * Perform verification (e.g., SMS verification for phone numbers, OAuth for social accounts) * Register attestations on-chain * Are trusted by applications that rely on their attestations * Can be anyone - Self Connect is open and permissionless Addresses of active issuers are listed in [Reference & Resources](/docs/self-connect/reference/#active-issuers). ## Attestations Attestations are on-chain mappings between an obfuscated identifier and a blockchain address. Each record stores the obfuscated identifier, the account address, the issuer address, and the verification timestamp. Each attestation is associated with the issuer that registered it. When looking up attestations, applications decide which issuers to trust. ## Federated Model Self Connect uses a federated attestation model where: * Multiple issuers can exist independently * Each issuer maintains their own verification standards * Applications choose which issuers to trust * No single point of failure or control --- # Protocol Architecture Source: https://docs.self.xyz/docs/self-connect/protocol-architecture/ Self Connect uses a federated architecture where multiple independent issuers can register attestations that map obfuscated identifiers to blockchain addresses. ## Core Components ``` ┌─────────────────┐ │ User/Client │ └────────┬────────┘ │ ├──────────────┐ │ │ v v ┌─────────────┐ ┌─────────────┐ │ ODIS │ │ Issuer │ │ (Privacy) │ │(Verification)│ └──────┬──────┘ └──────┬──────┘ │ │ └────────┬───────┘ │ v ┌──────────────────────┐ │ FederatedAttestations│ │ Smart Contract │ └──────────────────────┘ ``` ## FederatedAttestations Smart Contract The `FederatedAttestations` contract is the on-chain registry for all attestation mappings. It stores attestations in a structure that allows: * **Registration:** Issuers register mappings between obfuscated identifiers and addresses * **Lookup:** Anyone can query attestations for a given obfuscated identifier * **Multi-Issuer Support:** Attestations are organized by issuer, allowing trust-based filtering * **Batch Operations:** Multiple attestations can be queried simultaneously **Key Functions:** ```solidity // Register an attestation as an issuer function registerAttestationAsIssuer( bytes32 identifier, address account, uint64 issuedOn ) external // Lookup attestations for an identifier across multiple issuers function lookupAttestations( bytes32 identifier, address[] calldata trustedIssuers ) external view returns ( uint256[] memory countsPerIssuer, address[] memory accounts, address[] memory signers, uint64[] memory issuedOns, uint64[] memory publishedOns ) ``` Deployed contract addresses for mainnet and Alfajores are listed in [Reference & Resources](/docs/self-connect/reference/#contract-addresses). ## Issuer Ecosystem Issuers are independent entities that: 1. **Verify** user ownership of identifiers through their chosen method 2. **Request** obfuscated identifiers from ODIS 3. **Register** attestations to the FederatedAttestations contract 4. **Maintain** ODIS quota for continued operations Applications choose which issuers to trust based on: * Verification rigor * Reputation * Use case alignment * Uptime and reliability --- # ODIS Source: https://docs.self.xyz/docs/self-connect/odis/ ODIS is the privacy-preserving component of Self Connect that ensures identifiers cannot be reverse-engineered from on-chain data. ## What is ODIS ODIS implements a rate-limited Oblivious Pseudorandom Function (OPRF) that allows users to compute a limited number of hashes without letting the service see the data being hashed. **Key Properties:** * **Oblivious:** ODIS operators cannot see the plaintext identifiers * **Decentralized:** No single party can compute the pepper alone * **Rate-Limited:** Prevents rainbow table attacks through quota system * **Deterministic:** Same input always produces same output ## Privacy Guarantees ODIS provides strong privacy guarantees through its design: **1. Blinding Process** When a client queries ODIS: ``` 1. Client blinds the identifier locally using a secret one-time key 2. Blinded value is sent to ODIS operators 3. Operators compute the OPRF on the hidden input 4. Operators return the blinded result 5. Client unblinds the result to get the final pepper ``` This process ensures that: * ODIS operators never see the plaintext identifier * Even if all operators are compromised, user privacy is maintained * No targeted censorship is possible **2. Threshold Cryptography** ODIS uses a (k, m) threshold signature scheme: * **m:** Total number of ODIS operators * **k:** Minimum number of signatures required **Security Properties:** * If fewer than k operators are compromised: Attackers cannot compute unauthorized peppers * If at least k operators are honest: Service remains available for legitimate users **Example Configuration:** * 7 operators (m=7) * 5 required signatures (k=5) * Security: Need 5 compromised operators to break privacy * Availability: Can tolerate 2 operators being offline **3. Distributed Key Generation (DKG)** Before deployment, ODIS operators participated in a DKG ceremony to generate a shared secret, split across all operators. Each operator holds a key share that can be used to sign responses. When enough signatures (≥k) are combined, they produce the unique, deterministic pepper for that identifier. ## Quota System and Rate Limiting ODIS implements a quota system to prevent rainbow table attacks while allowing legitimate usage. **Quota Factors** Quota is based on: 1. **Payment:** Pay for quota using cUSD * 10 cUSD = 10,000 queries * \~0.001 cUSD per query 2. **Account-based Limits:** Rate limits per account to prevent abuse **How Quota Works** Issuers check their remaining quota against ODIS and pay for more in cUSD as needed. The code for both is in [ODIS Quota Management](/docs/self-connect/becoming-an-issuer/#odis-quota-management). **Rate Limiting Strategy** The quota system makes it prohibitively expensive to: * Scrape large quantities of identifiers * Build rainbow tables * Perform mass surveillance While still allowing: * Normal user flows * Legitimate application usage * Reasonable issuer operations ## Key Rotation If an operator's key is leaked or compromised, ODIS can perform key rotation: 1. New DKG ceremony with at least k old keys participating 2. New keys generated for all operators (including new ones) 3. Old keys destroyed after successful rotation 4. Public verification key remains unchanged This allows: * Adding new operators * Removing compromised operators * Changing threshold values (k, m) * Maintaining service continuity --- # Identifier Obfuscation Source: https://docs.self.xyz/docs/self-connect/identifier-obfuscation/ The obfuscation process transforms a plaintext identifier into a privacy-preserving on-chain identifier. ## Step-by-Step Process **1. Format the Identifier** Combine the identifier prefix with the plaintext identifier: ``` formatted = "{prefix}://{plaintextIdentifier}" ``` **Example:** ``` Phone: "tel://+12345678901" Twitter: "twit://@alice" ``` **2. Hash the Formatted Identifier** ``` hashedIdentifier = sha3(formatted) ``` **3. Blind the Hash** The client generates a random blinding factor and blinds the hash: ``` blindedHash = blind(hashedIdentifier, blindingFactor) ``` This step ensures ODIS cannot see the actual identifier. **4. Query ODIS** Send the blinded hash to ODIS operators. **5. ODIS Processing** Each ODIS operator: 1. Receives the blinded hash 2. Computes a partial signature using their key share 3. Returns the blinded partial signature **6. Combine Signatures** When k signatures are received: 1. Signatures are combined into a full signature 2. This is the "blinded pepper signature" **7. Unblind the Signature** The client unblinds the signature: ``` unblinedSignature = unblind(blindedPepperSignature, blindingFactor) ``` **8. Generate the Pepper** Extract the pepper from the unblinded signature: ``` pepper = first13Chars(sha256(unblindedSignature)) ``` **9. Create Obfuscated Identifier** Combine the hashed identifier with the pepper: ``` obfuscatedIdentifier = sha3(hashedIdentifier + "__" + pepper) ``` **Final Formula:** ``` obfuscatedIdentifier = sha3(sha3("{prefix}://{plaintext}") + "__" + pepper) ``` In practice, the SDK wraps this entire process in a single `OdisUtils.Identifier.getObfuscatedIdentifier` call. See [SDK Integration with Viem](/docs/self-connect/sdk-integration/). ## Verification Results from ODIS can be verified against the service's public key, which is shared with users through the client library. --- # Registration & Lookup Source: https://docs.self.xyz/docs/self-connect/registration-and-lookup/ ## Registration Flow The process of creating an attestation mapping: ``` ┌──────┐ ┌────────┐ ┌──────┐ ┌──────────────┐ │ User │ │ Issuer │ │ ODIS │ │ Blockchain │ └───┬──┘ └───┬────┘ └───┬──┘ └──────┬───────┘ │ │ │ │ │ 1. Request Verification│ │ │ ├──────────────────────>│ │ │ │ │ │ │ │ 2. Verify Ownership │ │ │ │ (SMS, OAuth, etc.) │ │ │ │<─────────────────────>│ │ │ │ │ │ │ │ │ 3. Query for Pepper │ │ │ ├──────────────────────>│ │ │ │ │ │ │ │ 4. Return Pepper │ │ │ │<──────────────────────┤ │ │ │ │ │ │ │ 5. Register Attestation │ │ ├───────────────────────────────────────────────>│ │ │ │ │ │ │ 6. Attestation Registered │ │ │<───────────────────────────────────────────────┤ │ │ │ │ │ 7. Confirmation │ │ │ │<──────────────────────┤ │ │ ``` **Detailed Steps:** 1. **User Requests Verification** * User provides their identifier and wallet address to an issuer * User indicates they want to create an attestation 2. **Issuer Verifies Ownership** * Phone: Send SMS with verification code * Twitter: OAuth flow * Email: Verification link * Custom: Any verification method the issuer chooses 3. **Issuer Queries ODIS** * Issuer sends blinded identifier to ODIS * Consumes issuer's ODIS quota * Receives pepper for obfuscation 4. **Issuer Computes Obfuscated Identifier** * Combines hashed identifier with pepper * Creates the final obfuscated identifier 5. **Register Attestation On-Chain** * Issuer calls `registerAttestationAsIssuer` on the FederatedAttestations contract with the obfuscated identifier, the user's account address, and the verification timestamp 6. **Gas Payment Options** * **Issuer Pays:** Issuer executes transaction with their gas * **User Pays:** Issuer signs attestation, user submits transaction ## Lookup Flow The process of finding addresses from identifiers: ``` ┌─────────────┐ ┌──────┐ ┌──────────────┐ │ Application │ │ ODIS │ │ Blockchain │ └──────┬──────┘ └───┬──┘ └──────┬───────┘ │ │ │ │ 1. Query for Pepper │ │ ├────────────────────>│ │ │ │ │ │ 2. Return Pepper │ │ │<────────────────────┤ │ │ │ │ │ 3. Lookup Attestations │ ├─────────────────────────────────────────>│ │ │ │ │ 4. Return Addresses │ │ │<─────────────────────────────────────────┤ ``` **Detailed Steps:** 1. **Get Obfuscated Identifier:** query ODIS via `getObfuscatedIdentifier` 2. **Query FederatedAttestations Contract:** call `lookupAttestations` with the obfuscated identifier and the list of trusted issuers 3. **Process Results:** the contract returns the number of attestations per issuer (`countsPerIssuer`) plus the matching accounts, signers, and issued/published timestamps Working code for both flows is in [SDK Integration with Viem](/docs/self-connect/sdk-integration/). ## Multi-Issuer Lookup `lookupAttestations` accepts multiple trusted issuers in a single call. The returned arrays are ordered by the input issuer list: `countsPerIssuer[i]` tells you how many of the returned accounts belong to `trustedIssuers[i]`. ## Trust Model Applications must decide which issuers to trust: **Single Issuer Trust:** * Trust only attestations from your own issuer * Maximum control over verification quality * Limited to your own user base **Multiple Issuer Trust:** * Trust attestations from multiple issuers * Broader coverage and interoperability * Must evaluate each issuer's verification quality **Consensus-Based Trust:** * Require attestations from multiple issuers * Higher confidence in verification * Reduced coverage (fewer users will have multiple attestations) --- # Security Considerations Source: https://docs.self.xyz/docs/self-connect/security-considerations/ ## Privacy Assumptions Self Connect's privacy model assumes: 1. **ODIS Operators:** At least k operators remain honest 2. **Rate Limiting:** Quota system prevents mass scraping 3. **Blinding:** Client-side blinding is implemented correctly 4. **No Collusion:** Attackers don't control k or more ODIS operators **If Assumptions Hold:** * Identifiers cannot be reverse-engineered from blockchain data * Rainbow table attacks are prohibitively expensive * User privacy is preserved **If Assumptions Fail:** * If k operators are compromised: Peppers for any identifier can be computed * If rate limiting is bypassed: Rainbow tables become feasible ## Sybil Resistance Self Connect provides Sybil resistance through: 1. **Verification Requirements:** Users must prove ownership of identifiers 2. **Issuer Quality:** Applications choose issuers with strong verification 3. **Costly Registration:** ODIS quota costs make mass fake registrations expensive 4. **Unique Identifiers:** Phone numbers and verified social accounts are limited per person **Limitations:** * Relies on issuer verification quality * Some identifiers (email) are easier to create in bulk * Applications must choose issuers carefully ## Comparison: ASv1 vs. Self Connect | Aspect | ASv1 | Self Connect | | ------------------------- | --------------------------------- | ---------------------------- | | **Verification** | 3 randomly selected validators | Issuer (flexible method) | | **Trust Model** | Single root: Validator collective | Multiple roots: Each issuer | | **Verification Quality** | Standardized across network | Varies by issuer | | **Flexibility** | Phone numbers only | Any identifier type | | **Censorship Resistance** | Validator majority | Multi-issuer selection | | **Scalability** | Limited by validator overhead | Scales with issuer ecosystem | ## Best Practices 1. **Choose Trusted Issuers** * Evaluate verification methods * Check issuer reputation * Monitor issuer behavior 2. **Implement Rate Limiting** * Limit lookup frequency per user * Monitor for abnormal query patterns 3. **Validate Results** * Check timestamps for recency * Verify issuer addresses * Handle multiple attestations appropriately 4. **Secure Key Management** * Protect issuer private keys * Use hardware security modules for production * Implement key rotation procedures 5. **Monitor ODIS Quota** * Set up alerts for low quota * Implement automatic top-ups * Track quota usage patterns --- # Getting Started Source: https://docs.self.xyz/docs/self-connect/getting-started/ ## Installation and Setup Install the required packages for Self Connect integration: ```bash npm install @celo/identity @celo/abis viem ``` ## Required Packages | Package | Purpose | | ---------------- | --------------------------------------------------- | | `@celo/identity` | ODIS integration and identifier utilities | | `@celo/abis` | Contract ABIs for FederatedAttestations | | `viem` | Modern Ethereum library for blockchain interactions | ## Why Viem? Self Connect uses **Viem** as the recommended library for blockchain interactions because: * **Modern & Type-Safe**: Built with TypeScript for excellent type inference * **Lightweight**: Smaller bundle size compared to legacy libraries * **Modular**: Import only what you need * **Better DX**: Cleaner API and better error messages * **Active Maintenance**: Well-maintained with regular updates * **EIP-1193 Support**: Native support for wallet providers > **Note:** ContractKit is deprecated and no longer recommended for new projects. ## Network Configuration **Mainnet:** ```typescript import { celo } from "viem/chains"; const RPC_URL = "https://forno.celo.org"; const ODIS_CONTEXT = OdisContextName.MAINNET; ``` **Alfajores Testnet:** ```typescript import { celoAlfajores } from "viem/chains"; const RPC_URL = "https://alfajores-forno.celo-testnet.org"; const ODIS_CONTEXT = OdisContextName.ALFAJORES; ``` --- # Becoming an Issuer Source: https://docs.self.xyz/docs/self-connect/becoming-an-issuer/ ## Issuer Setup An issuer needs: 1. A funded wallet account 2. ODIS quota for identifier obfuscation 3. Verification infrastructure 4. Optional: Data Encryption Key (DEK) for enhanced authentication ## Step 1: Set Up Issuer Account ```typescript import { createWalletClient, http, parseEther } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { celoAlfajores } from "viem/chains"; // Issuer private key - KEEP SECURE const ISSUER_PRIVATE_KEY = process.env.ISSUER_PRIVATE_KEY; const account = privateKeyToAccount(ISSUER_PRIVATE_KEY); // Create Viem client const walletClient = createWalletClient({ account, transport: http(), chain: celoAlfajores }); const issuerAddress = account.address; console.log("Issuer Address:", issuerAddress); ``` ## Step 2: Authentication Methods Self Connect supports multiple authentication methods for ODIS: **Wallet Key Authentication** ```typescript import { OdisUtils } from "@celo/identity"; import { AuthSigner } from "@celo/identity/lib/odis/query"; const authSigner: AuthSigner = { authenticationMethod: OdisUtils.Query.AuthenticationMethod.WALLET_KEY, sign191: ({ message, account }) => walletClient.signMessage({ message, account }) }; ``` **Encryption Key (DEK) Authentication** ```typescript const authSigner: AuthSigner = { authenticationMethod: OdisUtils.Query.AuthenticationMethod.ENCRYPTION_KEY, rawKey: process.env.DEK_PRIVATE_KEY }; ``` ## Step 3: Configure ODIS Service Context ```typescript import { OdisContextName } from "@celo/identity/lib/odis/query"; const serviceContext = OdisUtils.Query.getServiceContext( OdisContextName.ALFAJORES // or OdisContextName.MAINNET ); console.log("ODIS Endpoint:", serviceContext.odisUrl); console.log("ODIS Public Key:", serviceContext.odisPubKey); ``` ## ODIS Quota Management **Check Current Quota** ```typescript const { remainingQuota } = await OdisUtils.Quota.getPnpQuotaStatus( issuerAddress, authSigner, serviceContext ); console.log("Remaining ODIS Quota:", remainingQuota); ``` **Purchase Quota** ```typescript import { getContract } from "viem"; import { stableTokenABI, odisPaymentsABI } from "@celo/abis"; // Contract addresses (Alfajores testnet) const STABLE_TOKEN_ADDRESS = "0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1"; // cUSD const ODIS_PAYMENTS_ADDRESS = "0x645170cdB6B5c1bc80847bb728dBa56C50a20a49"; // Amount to pay (0.01 cUSD = 10 queries) const ONE_CENT_CUSD = parseEther("0.01"); // Approve ODIS Payments to spend cUSD const stableToken = getContract({ address: STABLE_TOKEN_ADDRESS, abi: stableTokenABI, client: walletClient }); const approveHash = await stableToken.write.approve([ ODIS_PAYMENTS_ADDRESS, ONE_CENT_CUSD ]); await walletClient.waitForTransactionReceipt({ hash: approveHash }); // Pay for quota const odisPayments = getContract({ address: ODIS_PAYMENTS_ADDRESS, abi: odisPaymentsABI, client: walletClient }); const paymentHash = await odisPayments.write.payInCUSD([ issuerAddress, ONE_CENT_CUSD ]); await walletClient.waitForTransactionReceipt({ hash: paymentHash }); console.log("ODIS quota purchased successfully"); ``` ## Verification Responsibilities As an issuer, you must verify user ownership of identifiers. Implementation depends on identifier type: **Phone Number Verification** ```typescript // Example using Twilio import twilio from "twilio"; const client = twilio(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN); async function verifyPhoneNumber(phoneNumber: string): Promise { // Send verification code await client.verify.v2 .services(TWILIO_VERIFY_SERVICE_SID) .verifications.create({ to: phoneNumber, channel: "sms" }); // User enters code (from your UI) const verificationCode = await getUserInput(); // Check verification const verification = await client.verify.v2 .services(TWILIO_VERIFY_SERVICE_SID) .verificationChecks.create({ to: phoneNumber, code: verificationCode }); return verification.status === "approved"; } ``` **Twitter Verification** ```typescript // Example using Twitter OAuth async function verifyTwitterHandle(handle: string, userAddress: string): Promise { // Implement OAuth flow const oauth = await initiateTwitterOAuth(userAddress); // User authenticates with Twitter const twitterUser = await completeOAuthFlow(oauth); // Verify handle matches return twitterUser.username === handle; } ``` **Email Verification** ```typescript // Example verification flow async function verifyEmail(email: string, userAddress: string): Promise { // Generate verification token const token = generateSecureToken(); // Store token with expiry await storeVerificationToken(email, userAddress, token); // Send verification email await sendEmail(email, { subject: "Verify your email", body: `Click here to verify: ${BASE_URL}/verify/${token}` }); // User clicks link, returns true if token is valid return await checkTokenVerified(token); } ``` --- # SDK Integration with Viem Source: https://docs.self.xyz/docs/self-connect/sdk-integration/ ## Complete Registration Example ```typescript import { createWalletClient, createPublicClient, http, parseEther, type Address, type Hex } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { celoAlfajores } from "viem/chains"; import { OdisUtils } from "@celo/identity"; import { OdisContextName } from "@celo/identity/lib/odis/query"; import type { AuthSigner } from "@celo/identity/lib/odis/query"; import { getContract } from "viem"; import { federatedAttestationsABI, odisPaymentsABI, stableTokenABI } from "@celo/abis"; // Configuration const ISSUER_PRIVATE_KEY = process.env.ISSUER_PRIVATE_KEY as Hex; const FEDERATED_ATTESTATIONS_ADDRESS = "0x70F9314aF173c246669cFb0EEe79F9Cfd9C34ee3" as Address; const ODIS_PAYMENTS_ADDRESS = "0x645170cdB6B5c1bc80847bb728dBa56C50a20a49" as Address; const STABLE_TOKEN_ADDRESS = "0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1" as Address; // Setup const account = privateKeyToAccount(ISSUER_PRIVATE_KEY); const walletClient = createWalletClient({ account, transport: http(), chain: celoAlfajores }); const publicClient = createPublicClient({ transport: http(), chain: celoAlfajores }); const issuerAddress = account.address; // User information (provided by user after verification) const userPlaintextIdentifier = "+12345678910"; const userAccountAddress = "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb" as Address; const attestationVerifiedTime = BigInt(Math.floor(Date.now() / 1000)); async function registerAttestation() { // 1. Setup authentication const authSigner: AuthSigner = { authenticationMethod: OdisUtils.Query.AuthenticationMethod.WALLET_KEY, sign191: ({ message, account }) => walletClient.signMessage({ message, account }) }; const serviceContext = OdisUtils.Query.getServiceContext( OdisContextName.ALFAJORES ); // 2. Check and top up ODIS quota if needed const { remainingQuota } = await OdisUtils.Quota.getPnpQuotaStatus( issuerAddress, authSigner, serviceContext ); console.log("Remaining quota:", remainingQuota); if (remainingQuota < 1) { console.log("Purchasing ODIS quota..."); // Get contract instances const stableToken = getContract({ address: STABLE_TOKEN_ADDRESS, abi: stableTokenABI, client: { public: publicClient, wallet: walletClient } }); const odisPayments = getContract({ address: ODIS_PAYMENTS_ADDRESS, abi: odisPaymentsABI, client: { public: publicClient, wallet: walletClient } }); const ONE_CENT_CUSD = parseEther("0.01"); // Approve ODIS Payments to spend cUSD const approveHash = await stableToken.write.approve([ ODIS_PAYMENTS_ADDRESS, ONE_CENT_CUSD ]); await publicClient.waitForTransactionReceipt({ hash: approveHash }); // Pay for quota const paymentHash = await odisPayments.write.payInCUSD([ issuerAddress, ONE_CENT_CUSD ]); await publicClient.waitForTransactionReceipt({ hash: paymentHash }); console.log("ODIS quota purchased successfully"); } // 3. Get obfuscated identifier from ODIS console.log("Getting obfuscated identifier..."); const { obfuscatedIdentifier } = await OdisUtils.Identifier.getObfuscatedIdentifier( userPlaintextIdentifier, OdisUtils.Identifier.IdentifierPrefix.PHONE_NUMBER, issuerAddress, authSigner, serviceContext ); console.log("Obfuscated Identifier:", obfuscatedIdentifier); // 4. Register attestation on-chain console.log("Registering attestation..."); const federatedAttestations = getContract({ address: FEDERATED_ATTESTATIONS_ADDRESS, abi: federatedAttestationsABI, client: { public: publicClient, wallet: walletClient } }); const hash = await federatedAttestations.write.registerAttestationAsIssuer([ obfuscatedIdentifier as Hex, userAccountAddress, attestationVerifiedTime ]); const receipt = await publicClient.waitForTransactionReceipt({ hash }); console.log("Attestation registered!"); console.log("Transaction:", receipt.transactionHash); return { obfuscatedIdentifier, transactionHash: receipt.transactionHash }; } // Execute registerAttestation().catch(console.error); ``` ## Complete Lookup Example ```typescript import { createPublicClient, http, type Address, type Hex } from "viem"; import { celoAlfajores } from "viem/chains"; import { OdisUtils } from "@celo/identity"; import { OdisContextName } from "@celo/identity/lib/odis/query"; import { getContract } from "viem"; import { federatedAttestationsABI } from "@celo/abis"; const FEDERATED_ATTESTATIONS_ADDRESS = "0x70F9314aF173c246669cFb0EEe79F9Cfd9C34ee3" as Address; const publicClient = createPublicClient({ transport: http(), chain: celoAlfajores }); async function lookupIdentifier( plaintextIdentifier: string, identifierType: string, trustedIssuers: Address[] ): Promise { // 1. Setup authentication for lookup // For read-only operations, use a zero address const lookupAddress = "0x0000000000000000000000000000000000000000" as Address; const authSigner = { authenticationMethod: OdisUtils.Query.AuthenticationMethod.WALLET_KEY, sign191: async () => "0x" as Hex }; const serviceContext = OdisUtils.Query.getServiceContext( OdisContextName.ALFAJORES ); // 2. Get obfuscated identifier const { obfuscatedIdentifier } = await OdisUtils.Identifier.getObfuscatedIdentifier( plaintextIdentifier, identifierType, lookupAddress, authSigner, serviceContext ); console.log("Looking up:", obfuscatedIdentifier); // 3. Query FederatedAttestations const federatedAttestations = getContract({ address: FEDERATED_ATTESTATIONS_ADDRESS, abi: federatedAttestationsABI, client: publicClient }); const attestations = await federatedAttestations.read.lookupAttestations([ obfuscatedIdentifier as Hex, trustedIssuers ]); const [countsPerIssuer, accounts, signers, issuedOns, publishedOns] = attestations; // 4. Process results console.log("Found attestations:"); let accountIndex = 0; for (let i = 0; i < trustedIssuers.length; i++) { const count = Number(countsPerIssuer[i]); console.log(`\nIssuer: ${trustedIssuers[i]}`); console.log(`Attestation count: ${count}`); for (let j = 0; j < count; j++) { console.log(` Account: ${accounts[accountIndex]}`); console.log(` Signer: ${signers[accountIndex]}`); console.log(` Issued: ${new Date(Number(issuedOns[accountIndex]) * 1000).toISOString()}`); console.log(` Published: ${new Date(Number(publishedOns[accountIndex]) * 1000).toISOString()}`); accountIndex++; } } return accounts as Address[]; } // Example usage const trustedIssuers: Address[] = [ "0x6549aF2688e07907C1b821cA44d6d65872737f05", // Kaala "0x388612590F8cC6577F19c9b61811475Aa432CB44" // Libera ]; lookupIdentifier( "+12345678910", OdisUtils.Identifier.IdentifierPrefix.PHONE_NUMBER, trustedIssuers ).catch(console.error); ``` ## Viem Best Practices **Use Type-Safe Contract Interactions** ```typescript import { getContract, type Address } from "viem"; import { federatedAttestationsABI } from "@celo/abis"; // Type-safe contract instance const contract = getContract({ address: FEDERATED_ATTESTATIONS_ADDRESS, abi: federatedAttestationsABI, client: { public: publicClient, wallet: walletClient } }); // TypeScript knows the exact function signatures const hash = await contract.write.registerAttestationAsIssuer([ obfuscatedIdentifier as `0x${string}`, userAddress as `0x${string}`, timestamp ]); ``` **Handle Hex Types Properly** Viem uses strict `Hex` types for type safety: ```typescript import type { Hex, Address } from "viem"; // Correct const privateKey: Hex = process.env.PRIVATE_KEY as Hex; const address: Address = "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb" as Address; // Type assertion for obfuscated identifiers const obfuscatedIdentifier: Hex = result.obfuscatedIdentifier as Hex; ``` **Use Proper Error Handling** ```typescript import { ContractFunctionExecutionError, TransactionExecutionError } from "viem"; try { const hash = await contract.write.registerAttestationAsIssuer([...]); } catch (error) { if (error instanceof ContractFunctionExecutionError) { console.error("Contract error:", error.shortMessage); } else if (error instanceof TransactionExecutionError) { console.error("Transaction failed:", error.message); } else { console.error("Unknown error:", error); } } ``` **Optimize with Public Actions** For read-only operations, use public client directly: ```typescript import { createPublicClient, http } from "viem"; import { celo } from "viem/chains"; const publicClient = createPublicClient({ chain: celo, transport: http() }); // No wallet needed for reads const attestations = await publicClient.readContract({ address: FEDERATED_ATTESTATIONS_ADDRESS, abi: federatedAttestationsABI, functionName: "lookupAttestations", args: [obfuscatedIdentifier, trustedIssuers] }); ``` ## Web/Browser Browser environments require careful handling of wallet connections: ```typescript // Check for wallet if (window.ethereum) { const accounts = await window.ethereum.request({ method: "eth_requestAccounts" }); // Create client with injected provider const walletClient = createWalletClient({ account: accounts[0], transport: custom(window.ethereum), chain: celoAlfajores }); } ``` **Next.js Example:** ```typescript // pages/api/register-attestation.ts import type { NextApiRequest, NextApiResponse } from "next"; import { OdisUtils } from "@celo/identity"; export default async function handler( req: NextApiRequest, res: NextApiResponse ) { const { phoneNumber, userAddress } = req.body; try { // Verify phone number (your verification logic) const isVerified = await verifyPhoneNumber(phoneNumber); if (!isVerified) { return res.status(400).json({ error: "Verification failed" }); } // Register attestation const result = await registerAttestation(phoneNumber, userAddress); res.status(200).json(result); } catch (error) { res.status(500).json({ error: error.message }); } } ``` ## Custom Identifier Types Create custom identifier types for your use case: ```typescript // Define custom prefix const CUSTOM_PREFIX = "custom-app"; async function registerCustomIdentifier( customId: string, userAddress: string ) { const { obfuscatedIdentifier } = await OdisUtils.Identifier.getObfuscatedIdentifier( customId, CUSTOM_PREFIX, // Custom prefix issuerAddress, authSigner, serviceContext ); // Register as usual await registerAttestation(obfuscatedIdentifier, userAddress); } ``` **Best Practices for Custom Identifiers:** * Use descriptive prefixes (e.g., `myapp://` not `ma://`) * Document your prefix for ecosystem adoption * Consider standardization if widely applicable * Ensure identifiers are unique and verifiable --- # Reference & Resources Source: https://docs.self.xyz/docs/self-connect/reference/ ## Contract Addresses **Mainnet (Celo)** | Contract | Address | | --------------------- | -------------------------------------------- | | FederatedAttestations | `0x0aD5b1d0C25ecF6266Dd951403723B2687d6aff2` | | OdisPayments | `0xAE6B29f31B96e61DdDc792f45fDa4e4F0356D0CB` | | StableToken (cUSD) | `0x765DE816845861e75A25fCA122bb6898B8B1282a` | **Alfajores Testnet** | Contract | Address | | --------------------- | -------------------------------------------- | | FederatedAttestations | `0x70F9314aF173c246669cFb0EEe79F9Cfd9C34ee3` | | OdisPayments | `0x645170cdB6B5c1bc80847bb728dBa56C50a20a49` | | StableToken (cUSD) | `0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1` | ## Active Issuers | Issuer Name | Address | | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | Kaala | `0x6549aF2688e07907C1b821cA44d6d65872737f05` (mainnet) | | Libera |

0x388612590F8cC6577F19c9b61811475Aa432CB44 (mainnet)
0xe3475047EF9F9231CD6fAe02B3cBc5148E8eB2c8 (alfajores)

| ## Example Repositories | Example | Description | Link | | -------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | **Phone Number (Next.js)** | Web app demonstrating phone verification with Viem | [emisianto](https://github.com/celo-org/emisianto) | | **Template** | Next.js template for Self Connect | [socialconnect-template](https://github.com/celo-org/socialconnect-template) | | **React Native** | Mobile app demo | [SocialConnect-ReactNative-Demo](https://github.com/celo-org/SocialConnect-ReactNative-Demo) | | **Twitter (Client-side)** | Twitter handle verification | [SocialConnect-Twitter](https://github.com/celo-org/SocialConnect-Twitter) | | **Twitter (Server-side)** | Server-side Twitter verification | [SocialConnect-Twitter-Server-Side](https://github.com/celo-org/SocialConnect-Twitter-Server-Side) | | **MiniPay Nexus** | MiniPay integration template | [nexus](https://github.com/celo-org/nexus) | --- # Troubleshooting & FAQ Source: https://docs.self.xyz/docs/self-connect/troubleshooting/ ## Troubleshooting ### ODIS Quota Issues **Problem:** "Insufficient quota" error **Solution:** Check your remaining quota with `getPnpQuotaStatus` and purchase more if needed. See [ODIS Quota Management](/docs/self-connect/becoming-an-issuer/#odis-quota-management). ### Rate Limiting **Problem:** Too many requests to ODIS **Solution:** * Implement request queuing * Cache obfuscated identifiers * Use batch operations when possible * Monitor quota usage ### Transaction Failures **Problem:** "Transaction reverted" or gas estimation failed **Solution:** ```typescript // Ensure sufficient gas const hash = await contract.write.registerAttestationAsIssuer( [identifier, address, timestamp], { gas: 200000n // Explicit gas limit } ); // Check if attestation already exists const existing = await contract.read.lookupAttestations([ identifier, [issuerAddress] ]); if (existing.accounts.length > 0) { console.log("Attestation already exists"); } ``` ### Network Issues **Problem:** RPC connection failures **Solution:** ```typescript // Use fallback RPCs const transport = fallback([ http("https://forno.celo.org"), http("https://rpc.ankr.com/celo"), http("https://1rpc.io/celo") ]); const client = createPublicClient({ chain: celo, transport }); ``` ## FAQ **Q: Do I need to pay gas for lookups?** A: No, lookups are read-only operations that don't require gas. You only need ODIS quota. **Q: Can users register themselves?** A: Users can submit the registration transaction if the issuer provides a signed attestation, but the issuer must still verify ownership and provide the obfuscated identifier. **Q: How much does it cost to register a user?** A: With 10 cUSD of ODIS quota, you can register 10,000 users. Gas costs for on-chain registration are typically <0.01 cUSD per transaction. **Q: Can I trust attestations from any issuer?** A: No, you should only trust issuers whose verification standards you trust. Each issuer is responsible for their own verification quality. **Q: What happens if an issuer goes offline?** A: Existing attestations remain on-chain and accessible. Users can register with other issuers for redundancy. **Q: Can I verify multiple identifier types for the same user?** A: Yes, you can register multiple attestations (phone + Twitter + email) for the same address, each with its own prefix. **Q: Is E.164 format required for phone numbers?** A: Yes, the SDK's `getObfuscatedIdentifier` function only accepts E.164 formatted phone numbers (e.g., +12345678901). **Q: How do I map attestation results to issuers?** A: The return arrays are ordered by the `trustedIssuers` input array. See [Multi-Issuer Lookup](/docs/self-connect/registration-and-lookup/#multi-issuer-lookup). **Q: Can I use Self Connect on other EVM chains?** A: Self Connect is currently designed for Celo. The contracts and ODIS infrastructure are Celo-specific. **Q: Why use Viem instead of Web3.js or Ethers.js?** A: Viem offers better TypeScript support, smaller bundles, and active maintenance. See [Why Viem?](/docs/self-connect/getting-started/#why-viem). **Q: Do I need to use different RPC endpoints for mainnet vs testnet?** A: Yes. Use the `celo` or `celoAlfajores` chain from `viem/chains`; see [Network Configuration](/docs/self-connect/getting-started/#network-configuration). --- # Overview Source: https://docs.self.xyz/docs/agent-id/overview/ Self Agent ID is an on-chain identity registry that binds AI agent identities to Self Protocol human proofs. Each agent receives a **soulbound ERC-721 NFT** backed by a ZK passport verification, enabling trustless proof-of-human for autonomous agents. The system implements the [ERC-8004 Proof-of-Human extension](https://eips.ethereum.org/EIPS/eip-8004) and provides SDK implementations in TypeScript, Python, and Rust with identical feature parity. :::note There is no consumer website. You use Agent ID through the SDK, the on-chain registry, the REST/A2A API at `https://agent-api.self.xyz`, or the `self-agent` CLI. See [Register an Agent with the SDK](/docs/agent-id/register-an-agent/) to register, sign, and verify an agent. ::: ## Who This Is For | Audience | What You Do | | ------------------------ | ---------------------------------------------------------------------------- | | **Agent builders** | Register an agent identity, sign outbound requests with `SelfAgent` | | **Service/API teams** | Verify inbound agent signatures with `SelfAgentVerifier` middleware | | **Protocol/infra teams** | Gate smart contracts, query on-chain state, compose with registry interfaces | ## How It Works 1. **Human scans passport** with the Self app (ZK proof generated locally on phone) 2. **Hub V2 verifies the proof** on-chain and calls back to the registry 3. **Registry mints a soulbound NFT** binding the agent key to a unique human nullifier 4. **Agent signs requests** using the SDK; services verify signatures against the on-chain registry :::note No personal data ever leaves the user's device. Only a ZK proof and nullifier are submitted on-chain. ::: ## Key Properties - **Trustless**: On-chain verification with no central authority - **Private**: ZK proofs reveal nothing about the human's identity - **Composable**: A single registry call integrates into any EVM contract, backend, or agent framework - **Sybil-resistant**: Each human maps to a unique nullifier, preventing unlimited agent registration - **Time-bounded**: Proofs expire (default: 1 year or document expiry), requiring periodic re-authentication - **ERC-8004 compliant**: Implements the standard Identity Registry interface with a Proof-of-Human extension ## Networks | Network | Chain ID | Registry Address | Passport Required | | ---------------------- | ---------- | -------------------------------------------- | --------------------------- | | Celo Mainnet (default) | `42220` | `0xaC3DF9ABf80d0F5c020C06B04Cced27763355944` | Real passport via Self app | | Celo Sepolia (testnet) | `11142220` | `0x043DaCac8b0771DD5b444bCC88f2f8BBDBEdd379` | Mock documents via Self app | :::warning **Mainnet is the default.** Registration requires a real passport scanned with the Self app. Testnet also requires the Self app, but you can generate mock documents within the app instead of using a real passport. Testnet is for development and testing only. ::: :::warning Celo Sepolia chain ID is **11142220**, not 44787 (deprecated Alfajores). ::: ## Where It Lives - API (REST + A2A): `https://agent-api.self.xyz` - Contracts: on Celo (addresses above) - SDKs: `@selfxyz/agent-sdk` (TypeScript), `selfxyz-agent-sdk` (Python), `self-agent-sdk` (Rust) - Source: [github.com/selfxyz/self-agent-id](https://github.com/selfxyz/self-agent-id) ## A2A Protocol Agents can interact programmatically via the A2A JSON-RPC endpoint at `https://agent-api.self.xyz/api/a2a`. This supports registration, verification, lookup, deregistration, and proof freshness checks — all through structured intents or natural language. Send `{ "intent": "help" }` to get a full list of capabilities and a decision guide for choosing the right registration mode. ## Architecture The system consists of three on-chain registries: - **SelfAgentRegistry** — Core identity registry (ERC-8004 + Proof-of-Human extension, soulbound ERC-721) - **SelfReputationRegistry** — ERC-8004 Reputation Registry for aggregated feedback with document-type weighted signals - **SelfValidationRegistry** — ERC-8004 Validation Registry for on-demand third-party validation Plus SDKs in TypeScript, Python, and Rust with identical feature parity for signing requests, verifying agents, and interacting with the registry. ## Next Steps - [Registration Modes](/docs/agent-id/registration-modes/) — Choose how to register your agent - [SDK Integration](/docs/agent-id/sdk-integration/) — Sign and verify agent requests - [Smart Contracts](/docs/agent-id/smart-contracts/) — On-chain gating, queries, and the full ERC-8004 interface - [A2A Protocol & REST API](/docs/agent-id/rest-api/) — Programmatic agent interaction via JSON-RPC --- # Register an Agent with the SDK Source: https://docs.self.xyz/docs/agent-id/register-an-agent/ Register an agent end to end from your own app: render the passport-scan QR with the SDK, the Self mobile app submits the proof on-chain, and the registry mints the agent. Signing and verification are on-chain too. This page shows the full loop with the SDK: register, sign, verify. It needs no hosted service at all. If you prefer, you can also register through the [CLI](/docs/agent-id/cli/) or the [REST/A2A API](/docs/agent-id/rest-api/) at `https://agent-api.self.xyz`, which return the QR for you to display. ## What you need - `npm install @selfxyz/agent-sdk @selfxyz/qrcode ethers` - The Self mobile app on a phone. On testnet you can generate mock documents in the app, so no real passport is needed. - A Celo RPC. Public defaults: `https://forno.celo.org` (mainnet), `https://forno.celo-sepolia.celo-testnet.org` (testnet). | Network | Chain ID | Registry | | ---------------------- | ---------- | -------------------------------------------- | | Celo Mainnet | `42220` | `0xaC3DF9ABf80d0F5c020C06B04Cced27763355944` | | Celo Sepolia (testnet) | `11142220` | `0x043DaCac8b0771DD5b444bCC88f2f8BBDBEdd379` | ## 1. Register an agent Render the QR in your own frontend. The proof goes from the Self app to the on-chain Hub, which mints the agent NFT into the registry. No backend is involved. ```tsx import { useEffect, useState } from 'react'; import { SelfAppBuilder, SelfQRcodeWrapper } from '@selfxyz/qrcode'; import { signRegistrationChallenge, buildAdvancedRegisterUserDataAscii } from '@selfxyz/agent-sdk'; import { Wallet } from 'ethers'; const REGISTRY = '0xaC3DF9ABf80d0F5c020C06B04Cced27763355944'; // Celo mainnet export function RegisterAgent({ humanAddress }: { humanAddress: string }) { const [selfApp, setSelfApp] = useState(null); const [agentKey] = useState(() => Wallet.createRandom()); useEffect(() => { (async () => { // The agent key signs a challenge proving it controls the key const sig = await signRegistrationChallenge(agentKey.privateKey, { humanIdentifier: humanAddress, chainId: 42220, registryAddress: REGISTRY, nonce: 0, }); // Encode the registration payload (config index is derived from disclosures) const disclosures = { minimumAge: 18, ofac: true }; const userDefinedData = buildAdvancedRegisterUserDataAscii({ agentAddress: agentKey.address, signature: sig, disclosures, }); // Build the Self verification request; the registry is the on-chain endpoint setSelfApp( new SelfAppBuilder({ version: 2, appName: 'My Agent', scope: 'self-agent-id', endpoint: REGISTRY, endpointType: 'celo', // 'staging_celo' for testnet userId: humanAddress, userIdType: 'hex', userDefinedData, disclosures, }).build(), ); })(); }, [humanAddress]); return selfApp ? ( { // Minted. Save agentKey.privateKey securely — it is the agent's key. }} onError={() => console.error('verification failed')} /> ) : (

Loading…

); } ``` The user scans the QR with the Self app. On `onSuccess` the agent NFT is minted. **Save the agent private key.** This example uses `linked` mode; for other modes (wallet-free, Ed25519, etc.) see [Registration Modes](/docs/agent-id/registration-modes/). :::note For a headless flow, use `getUniversalLink(selfApp)` from `@selfxyz/qrcode` to produce the deep link instead of the React component, and read the new agent from the registry with `getAgentId` / `isVerifiedAgent`. ::: ## 2. Publish the agent's identity document (optional but recommended) Agents minted this way start with a blank `agentURI`. Publish a JSON document so services and indexers can discover the agent, then point the agent at it. ```typescript import { generateRegistrationJSON } from '@selfxyz/agent-sdk'; const doc = generateRegistrationJSON({ name: 'My Agent', description: 'A human-backed AI agent', image: 'https://my-agent.example.com/avatar.png', services: [{ name: 'A2A', endpoint: 'https://my-agent.example.com/a2a', version: '1.0' }], }); // Host doc over HTTPS, then call registry.setAgentURI(agentId, url) ``` ## 3. Sign requests as the agent ```typescript import { SelfAgent } from '@selfxyz/agent-sdk'; const agent = new SelfAgent({ privateKey: process.env.AGENT_PRIVATE_KEY!, registryAddress: '0xaC3DF9ABf80d0F5c020C06B04Cced27763355944', rpcUrl: 'https://forno.celo.org', }); const res = await agent.fetch('https://some-service.example.com/api/protected', { method: 'POST', body: JSON.stringify({ hello: 'world' }), }); ``` ## 4. Verify the agent in a service The verifier recovers the signer and checks the registry on-chain. No API, no shared secret. ```typescript import { SelfAgentVerifier } from '@selfxyz/agent-sdk'; const verifier = new SelfAgentVerifier({ registryAddress: '0xaC3DF9ABf80d0F5c020C06B04Cced27763355944', rpcUrl: 'https://forno.celo.org', }); app.use('/api', verifier.auth()); // rejects requests from unverified agents ``` That is the whole product: register on-chain, sign, verify on-chain. Nothing here depends on a hosted Self website. ## Next steps - [Registration Modes](/docs/agent-id/registration-modes/) — pick the key type and ownership model - [SDK Integration](/docs/agent-id/sdk-integration/) — signing and verification in TypeScript, Python, and Rust - [Verification Patterns](/docs/agent-id/verification-patterns/) — `sameHuman`, proof freshness, policy controls - [Smart Contracts](/docs/agent-id/smart-contracts/) — gate a contract on verified agents --- # Registration Modes Source: https://docs.self.xyz/docs/agent-id/registration-modes/ Self Agent ID supports six registration modes. All produce the same on-chain result — a soulbound NFT with ZK proof-of-human — but they differ in key type, wallet requirements, and user experience. Two modes (privy and smartwallet) are UI wrappers that use the same underlying contract flows. ## Networks | Network | Chain ID | Default? | Passport Type | Use Case | |---------|----------|----------|---------------|----------| | **Celo Mainnet** | 42220 | Yes | Real passports via Self app | Production deployments | | **Celo Sepolia** | 11142220 | No | Mock documents via Self app | Testing and development | :::warning Mainnet is the default network. Real passport verification requires the Self mobile app and a physical passport with NFC. Testnet also requires the Self app, but you can generate mock documents within the app instead of using a real passport. Testnet should only be used for development and testing. ::: ## Quick Decision Guide ``` ┌─────────────────────┐ │ What key type does │ │ your agent use? │ └────────┬────────────┘ │ ┌────────────┴────────────┐ │ │ Ed25519 ECDSA │ │ ┌────────┴────────┐ ┌────────┴─────────────┐ │ Need human │ │ How should the │ │ wallet binding? │ │ human authenticate? │ └───┬─────────┬───┘ └──┬──────┬──────┬─────┘ │ │ │ │ │ No Yes Wallet Social Passkey │ │ │ login ▼ ▼ │ │ │ ed25519 ed25519- ┌───┴───┐ ▼ ▼ linked │ │ privy smartwallet Yes No │ │ ▼ ▼ linked wallet-free ``` ## Mode Comparison | Mode | Wallet Required? | NFT Owner | Agent Key Type | Best For | |------|-----------------|-----------|----------------|----------| | **linked** | Yes (registration only) | Human's wallet | Independent ECDSA keypair | Autonomous agents with human oversight | | **wallet-free** | No | Agent's address | Independent ECDSA keypair | Non-crypto users, embedded agents | | **ed25519** | No | Derived from pubkey | Ed25519 keypair | OpenClaw/Eliza/IronClaw agents | | **ed25519-linked** | Yes (registration only) | Human's wallet | Ed25519 keypair | Ed25519 agents with human wallet binding | | **privy** | No (social login) | Human's wallet | Independent ECDSA keypair | OAuth users (Google, Twitter, etc.) | | **smartwallet** | No (passkey) | Agent's address | Independent ECDSA keypair | Passkey UX, gasless management | ## Linked The agent generates its own dedicated ECDSA keypair. The human's wallet is used only during registration to establish ownership. **Flow:** 1. Connect browser wallet 2. A fresh agent keypair is generated in the browser 3. Agent signs a challenge proving key ownership (ECDSA) 4. Scan passport with Self app — contract verifies both ZK proof and agent signature in one step 5. Human's wallet key is never exposed to agent software **Use when:** You're building autonomous agents that need their own identity, or you want key rotation without re-registering the human. :::note Linked mode is the recommended default for most agent deployments. It separates the human's wallet from the agent's operational key while maintaining a verifiable ownership link. ::: ## Wallet-Free No crypto wallet required. The agent generates an ECDSA keypair and owns its own NFT. **Flow:** 1. Agent keypair generated in browser 2. Agent signs a challenge proving key ownership 3. Scan passport with Self app — NFT is minted to the agent's own address 4. Deregister anytime by scanning passport again (ZK proof links to your unique identity) **Use when:** The user has no crypto wallet and just needs a quick registration with their passport, or you're embedding agent identity into a service where wallet management is undesirable. :::note In wallet-free mode, the user must save the agent's private key securely. If lost, they can deregister by scanning their passport and create a new agent. ::: ## Ed25519 For agents that use Ed25519 keys natively (common in AI agent frameworks). No wallet required — the NFT owner is derived from the Ed25519 public key. **Flow:** 1. Agent provides its Ed25519 public key 2. Agent signs a challenge with its Ed25519 private key 3. Scan passport with Self app — NFT is minted to an address derived from the Ed25519 pubkey 4. Deregister anytime by scanning passport again **Use when:** Your agent framework uses Ed25519 keys (OpenClaw, Eliza, IronClaw) and you don't need a separate human wallet binding. :::note The NFT owner address is deterministically derived from the Ed25519 public key. The agent can prove ownership of the NFT by signing with the same Ed25519 key. ::: ## Ed25519-Linked Combines Ed25519 agent keys with a human wallet for ownership. The human's wallet holds the NFT while the agent operates with Ed25519 keys. **Flow:** 1. Connect browser wallet 2. Agent provides its Ed25519 public key 3. Agent signs a challenge with its Ed25519 private key 4. Scan passport with Self app — NFT is minted to the human's wallet, linked to the Ed25519 agent key 5. Human retains custody of the NFT while the agent operates independently **Use when:** Your agent uses Ed25519 keys but you want the NFT ownership tied to a human wallet for oversight and management. ## Privy A UI wrapper over the linked/wallet-free flow that uses Privy for social login authentication. Users sign in with OAuth providers instead of connecting a wallet. **Flow:** 1. User authenticates via social login (Google, Twitter, email, etc.) 2. Privy creates an embedded wallet behind the scenes 3. Agent keypair is generated 4. Agent signs challenge; registration proceeds as linked mode 5. Scan passport with Self app **Use when:** Your users prefer social login over wallet connections, or you want a familiar OAuth-style onboarding experience. :::warning Privy is a UI convenience layer — the underlying contract interaction is identical to linked or wallet-free mode. The Privy-managed wallet holds the NFT on behalf of the user. ::: ## Smart Wallet A UI wrapper that uses a passkey (Face ID / fingerprint) to create a smart account. No MetaMask, no seed phrase. **Flow:** 1. Create passkey via biometrics (creates a ZeroDev Kernel smart account) 2. Agent keypair is generated 3. Agent signs challenge; smart wallet is set as guardian 4. Scan passport with Self app 5. Revoke agent anytime with biometrics (gaslessly on Celo mainnet) **Use when:** You want the simplest user experience with no seed phrases, no browser extensions, and gasless management via Pimlico. :::note On Celo Sepolia (testnet), the smart wallet is counterfactual only — it deploys on first mainnet management action. ::: ## Verification Configs All modes support 6 verification configurations, combining age thresholds with OFAC sanctions screening: | Config Index | Age Requirement | OFAC Screening | |-------------|-----------------|----------------| | 0 | None | Off | | 1 | 18+ | Off | | 2 | 21+ | Off | | 3 | None | On | | 4 | 18+ | On | | 5 | 21+ | On | The config is selected via the second character of `userDefinedData` during registration. ## ZK-Attested Credentials During registration, users can optionally disclose verified claims that are stored on-chain as ZK-attested credentials: - Nationality (ISO 3166 country code) - Full name - Date of birth - Gender - Issuing state - OFAC sanctions clearance - Age verification (18+ or 21+) All disclosures are optional. Raw passport data never leaves the user's device. --- # SDK Integration Source: https://docs.self.xyz/docs/agent-id/sdk-integration/ Self Agent ID provides three SDKs with identical feature parity: | Language | Package | Install | | ---------- | -------------------- | -------------------------------- | | TypeScript | `@selfxyz/agent-sdk` | `npm install @selfxyz/agent-sdk` | | Python | `selfxyz-agent-sdk` | `pip install selfxyz-agent-sdk` | | Rust | `self-agent-sdk` | `cargo add self-agent-sdk` | ## Agent-Side: Signing Requests Use `SelfAgent` to sign outbound API requests. The SDK attaches three headers to every request: - `x-self-agent-address` — the agent's Ethereum address - `x-self-agent-signature` — ECDSA signature of `keccak256(timestamp + METHOD + path + bodyHash)` - `x-self-agent-timestamp` — Unix timestamp (seconds) ::::tabs :::tab{label="TypeScript"} ```typescript import { SelfAgent } from "@selfxyz/agent-sdk"; const agent = new SelfAgent({ privateKey: process.env.AGENT_PRIVATE_KEY!, registryAddress: "0xaC3DF9ABf80d0F5c020C06B04Cced27763355944", rpcUrl: "https://forno.celo.org", }); // Signed fetch — headers are attached automatically const res = await agent.fetch("https://api.example.com/protected", { method: "POST", body: JSON.stringify({ action: "hello" }), }); ``` ::: :::tab{label="Python"} ```python from self_agent_sdk import SelfAgent agent = SelfAgent( private_key=os.environ["AGENT_PRIVATE_KEY"], registry_address="0xaC3DF9ABf80d0F5c020C06B04Cced27763355944", rpc_url="https://forno.celo.org", ) # Signed request res = agent.fetch("https://api.example.com/protected", method="POST", json={"action": "hello"}) ``` ::: :::tab{label="Rust"} ```rust use self_agent_sdk::SelfAgent; let agent = SelfAgent::new( &std::env::var("AGENT_PRIVATE_KEY")?, "0xaC3DF9ABf80d0F5c020C06B04Cced27763355944", "https://forno.celo.org", )?; let res = agent.fetch("https://api.example.com/protected") .method("POST") .json(&serde_json::json!({"action": "hello"})) .send() .await?; ``` ::: :::: ## Service-Side: Verifying Requests Use `SelfAgentVerifier` to verify incoming agent requests. The verifier recovers the signer address from the ECDSA signature, derives the agent key, and checks `isVerifiedAgent()` on-chain. ### Builder Pattern ::::tabs :::tab{label="TypeScript"} ```typescript import { SelfAgentVerifier } from "@selfxyz/agent-sdk"; const verifier = SelfAgentVerifier.create() .requireAge(18) .requireOFAC() .sybilLimit(1) .build(); // Express middleware app.use("/api", verifier.auth()); ``` ::: :::tab{label="Python"} ```python from self_agent_sdk import SelfAgentVerifier verifier = (SelfAgentVerifier.create() .require_age(18) .require_ofac() .sybil_limit(1) .build()) # Flask middleware @app.before_request def verify_agent(): result = verifier.verify_request(request) if not result.valid: return {"error": result.error}, 401 ``` ::: :::tab{label="Rust"} ```rust use self_agent_sdk::SelfAgentVerifier; let verifier = SelfAgentVerifier::builder() .require_age(18) .require_ofac() .sybil_limit(1) .build()?; // Axum middleware (requires `axum` feature) let app = Router::new() .route("/api/protected", post(handler)) .layer(verifier.into_layer()); ``` ::: :::: ### Direct Verification ```typescript const result = await verifier.verify({ signature: req.headers["x-self-agent-signature"], timestamp: req.headers["x-self-agent-timestamp"], method: req.method, url: req.url, body: req.body, }); if (result.valid) { console.log(`Verified agent: ${result.agentAddress}, ID #${result.agentId}`); console.log(`Credentials:`, result.credentials); } ``` ## Security Defaults `SelfAgentVerifier` defaults are strict: | Setting | Default | Description | | --------------------- | ----------- | ------------------------------------------------------------ | | `requireSelfProvider` | `true` | Only accept proofs from Self Protocol's provider | | `sybilLimit` | `1` | One agent per human (sybil resistance); `0` disables the cap | | Replay protection | Enabled | Signature nonce + timestamp freshness | | Timestamp window | 300 seconds | Reject requests older than 5 minutes | :::warning Setting `requireSelfProvider: false` accepts verification from any approved proof provider, not only Self Protocol. ::: ## Agent Status & Info ```typescript // Check registration status const registered = await agent.isRegistered(); // Get full agent info const info = await agent.getInfo(); // { agentId, address, agentKey, isVerified, proofProvider, proofExpiresAt } ``` ## Proof Expiry Agent proofs have a validity window. The SDK exposes expiry information and handles stale proofs: ```typescript const info = await agent.getInfo(); const expiresAt = info.proofExpiresAt; // Unix timestamp (seconds) // SDK verifiers automatically reject expired proofs // 30-day warning threshold for upcoming expiry ``` :::warning When a proof expires, the agent's `isProofFresh()` returns `false` on-chain. There is no on-chain refresh mechanism — the agent must deregister and re-register with a fresh proof (scanning their passport again). The soulbound NFT is NOT burned — it remains as a historical record. ::: ## A2A Agent Cards Build and store standardized agent identity cards: ```typescript import { buildAgentCard } from "@selfxyz/agent-sdk"; const card = await buildAgentCard(agent, { name: "My Agent", description: "A human-verified AI assistant", }); // Store on-chain via updateAgentMetadata() ``` Cards follow the A2A format and are queryable via: - `GET /api/cards/{chainId}/{agentId}` - `GET /.well-known/a2a/{agentId}?chain={chainId}` - `POST /api/a2a` — A2A JSON-RPC endpoint for agent-to-agent communication --- # Verification Patterns Source: https://docs.self.xyz/docs/agent-id/verification-patterns/ Self Agent ID supports four verification patterns, each suited to different integration scenarios. ## Pattern 1: Agent-to-Service The most common pattern. An agent signs HTTP requests; a service verifies the signature and checks the on-chain registry. ``` Agent Service │ │ │── POST /api (signed) ────────▶│ │ headers: │ │ x-self-agent-address │── ecrecover signer │ x-self-agent-signature │── derive agentKey │ x-self-agent-timestamp │── isVerifiedAgent(key) │ │── check credentials │◀── 200 OK ────────────────────│ ``` **SDK support:** `SelfAgent.fetch()` on agent side, `SelfAgentVerifier.auth()` middleware on service side. ## Pattern 2: Agent-to-Agent (Peer Verification) Two agents verify each other's identity. Both sign their requests, and both verify the other's signature against the registry. ``` Agent A Agent B │ │ │── POST (signed by A) ────────▶│ │ │── verify A's signature │ │── sameHuman(A, B) check │◀── Response (signed by B) ────│ │── verify B's signature │ ``` **Key feature:** `sameHuman(agentIdA, agentIdB)` detects whether two agents share the same human backer without revealing who that human is. ## Pattern 3: Agent-to-Chain (Direct) The agent calls a smart contract directly using `msg.sender`. The contract checks the registry. ```solidity import { AgentGate } from "./AgentGate.sol"; contract MyProtocol is AgentGate { constructor(address registry) AgentGate(registry) {} function protectedAction() external onlyVerifiedAgent { // Only registered, human-backed agents can call this } } ``` **Best for:** Verified Wallet mode where the agent's wallet address IS the `msg.sender`. ## Pattern 4: Agent-to-Chain (Meta-Transaction) The agent signs an EIP-712 typed data message; a relayer submits the transaction on-chain. The contract verifies the EIP-712 signature matches a registered agent. ``` Agent Relayer Contract │ │ │ │── sign EIP-712 ──────── │ │ │ (agentKey, nonce, │── metaVerify(sig) ────▶│ │ deadline) │ │── ecrecover signer │ │ │── isVerifiedAgent() │ │◀── tx confirmed ───────│ │◀── receipt ─────────────│ │ ``` **Best for:** Gasless verification where agents don't hold native tokens. The `AgentDemoVerifier` contract implements this pattern. ## Sybil Resistance Services can enforce their own sybil policies using three approaches: ### Strict (max 1 agent per human) ```typescript const verifier = SelfAgentVerifier.create() .sybilLimit(1) .build(); ``` ### Moderate (allow N agents) ```typescript const verifier = SelfAgentVerifier.create() .sybilLimit(5) .build(); ``` ### Detection Only ```typescript // Allow unlimited, but check relationships const verifier = SelfAgentVerifier.create() .sybilLimit(0) // unlimited .build(); // Then use sameHuman() for analytics const areSame = await registry.sameHuman(agentId1, agentId2); ``` ## Credential-Based Verification Verifiers can require specific ZK-attested credentials: ```typescript const verifier = SelfAgentVerifier.create() .requireAge(18) // Agent's human must be 18+ .requireOFAC() // Agent's human must be OFAC-cleared .requireNationality("US") // Specific nationality .build(); ``` All credential checks are verified against on-chain ZK-attested data — no additional identity check needed. --- # Smart Contracts Source: https://docs.self.xyz/docs/agent-id/smart-contracts/ Self Agent ID deploys 9 contracts on Celo. The core registry implements `IERC8004` and the `IERC8004ProofOfHuman` extension. ## Contract Overview | Contract | Role | |----------|------| | **SelfAgentRegistry** | Core registry — ERC-721 soulbound NFTs, IERC8004 + IERC8004ProofOfHuman, registration modes, credentials, proof expiry, sybil detection (UUPS upgradeable) | | **SelfHumanProofProvider** | Implements `IHumanProofProvider` — metadata wrapper connecting to Self Protocol Hub V2 (verification strength: 100) | | **SelfReputationRegistry** | ERC-8004 Reputation Registry — aggregated feedback with document-type weighted signals (UUPS upgradeable) | | **SelfValidationRegistry** | ERC-8004 Validation Registry — on-demand validation requests/responses from third parties (UUPS upgradeable) | | **AgentDemoVerifier** | EIP-712 meta-transaction verifier for gasless on-chain agent verification | | **AgentGate** | Abstract contract with `onlyVerifiedAgent` modifier for gating access | | **LocalRegistryHarness** | Local mock for testing without Hub V2 dependency | ## Deployed Addresses ### Celo Mainnet (42220) | Contract | Address | |----------|---------| | SelfAgentRegistry (proxy) | [`0xaC3DF9ABf80d0F5c020C06B04Cced27763355944`](https://celoscan.io/address/0xaC3DF9ABf80d0F5c020C06B04Cced27763355944) | | SelfHumanProofProvider | [`0x4b036aFD959B457A208F676cf44Ea3ef73Ea3E3d`](https://celoscan.io/address/0x4b036aFD959B457A208F676cf44Ea3ef73Ea3E3d) | | SelfReputationRegistry (proxy) | [`0x69Da18CF4Ac27121FD99cEB06e38c3DC78F363f4`](https://celoscan.io/address/0x69Da18CF4Ac27121FD99cEB06e38c3DC78F363f4) | | SelfValidationRegistry (proxy) | [`0x71a025e0e338EAbcB45154F8b8CA50b41e7A0577`](https://celoscan.io/address/0x71a025e0e338EAbcB45154F8b8CA50b41e7A0577) | | AgentDemoVerifier | [`0xD8ec054FD869A762bC977AC328385142303c7def`](https://celoscan.io/address/0xD8ec054FD869A762bC977AC328385142303c7def) | | AgentGate | [`0x26e05bF632fb5bACB665ab014240EAC1413dAE35`](https://celoscan.io/address/0x26e05bF632fb5bACB665ab014240EAC1413dAE35) | | Hub V2 | [`0xe57F4773bd9c9d8b6Cd70431117d353298B9f5BF`](https://celoscan.io/address/0xe57F4773bd9c9d8b6Cd70431117d353298B9f5BF) | ### Celo Sepolia (11142220) | Contract | Address | |----------|---------| | SelfAgentRegistry (proxy) | [`0x043DaCac8b0771DD5b444bCC88f2f8BBDBEdd379`](https://celo-sepolia.blockscout.com/address/0x043DaCac8b0771DD5b444bCC88f2f8BBDBEdd379) | | SelfHumanProofProvider | [`0x5E61c3051Bf4115F90AacEAE6212bc419f8aBB6c`](https://celo-sepolia.blockscout.com/address/0x5E61c3051Bf4115F90AacEAE6212bc419f8aBB6c) | | SelfReputationRegistry (proxy) | [`0x3Bb0A898C1C0918763afC22ff624131b8F420CC2`](https://celo-sepolia.blockscout.com/address/0x3Bb0A898C1C0918763afC22ff624131b8F420CC2) | | SelfValidationRegistry (proxy) | [`0x84cA20B8A1559F136dA03913dbe6A7F68B6B240B`](https://celo-sepolia.blockscout.com/address/0x84cA20B8A1559F136dA03913dbe6A7F68B6B240B) | | AgentDemoVerifier | [`0xc31BAe8f2d7FCd19f737876892f05d9bDB294241`](https://celo-sepolia.blockscout.com/address/0xc31BAe8f2d7FCd19f737876892f05d9bDB294241) | | AgentGate | [`0x86Af07e30Aa42367cbcA7f2B1764Be346598bbc2`](https://celo-sepolia.blockscout.com/address/0x86Af07e30Aa42367cbcA7f2B1764Be346598bbc2) | | Hub V2 | [`0x16ECBA51e18a4a7e61fdC417f0d47AFEeDfbed74`](https://celo-sepolia.blockscout.com/address/0x16ECBA51e18a4a7e61fdC417f0d47AFEeDfbed74) | ## ERC-8004 Interfaces The registry implements two standard interfaces: ### IERC8004 (Base Identity Registry) ```solidity // Registration (3 overloads) function register() external returns (uint256 agentId); function register(string calldata agentURI) external returns (uint256 agentId); function register(string calldata agentURI, string[] calldata keys, bytes[] calldata values) external returns (uint256 agentId); // Agent URI function setAgentURI(uint256 agentId, string calldata newURI) external; // Metadata (key-value store) function getMetadata(uint256 agentId, string memory key) external view returns (bytes memory); function setMetadata(uint256 agentId, string calldata key, bytes calldata value) external; // Agent Wallet (EIP-712 signature required from newWallet) function setAgentWallet(uint256 agentId, address newWallet, uint256 deadline, bytes calldata signature) external; function getAgentWallet(uint256 agentId) external view returns (address); function unsetAgentWallet(uint256 agentId) external; ``` ### IERC8004ProofOfHuman (Extension) ```solidity // Register with human proof from approved provider function registerWithHumanProof(string calldata agentURI, address proofProvider, bytes calldata proof, bytes calldata providerData) external returns (uint256 agentId); // Revoke proof (requires re-proving same human) function revokeHumanProof(uint256 agentId, address proofProvider, bytes calldata proof, bytes calldata providerData) external; // Proof queries function hasHumanProof(uint256 agentId) external view returns (bool); function proofExpiresAt(uint256 agentId) external view returns (uint256); function isProofFresh(uint256 agentId) external view returns (bool); function getHumanNullifier(uint256 agentId) external view returns (uint256); function getProofProvider(uint256 agentId) external view returns (address); function isApprovedProvider(address provider) external view returns (bool); // Sybil detection function sameHuman(uint256 agentIdA, uint256 agentIdB) external view returns (bool); function getAgentCountForHuman(uint256 nullifier) external view returns (uint256); ``` ## Key Interfaces ### Reading Agent State ```solidity // Check if an agent is verified bool verified = registry.isVerifiedAgent(agentKey); // Get agent ID from key uint256 agentId = registry.getAgentId(agentKey); // Get proof provider address provider = registry.getProofProvider(agentId); // Get ZK-attested credentials AgentCredentials memory creds = registry.getAgentCredentials(agentId); // Check proof freshness and expiry bool fresh = registry.isProofFresh(agentId); uint256 expiresAt = registry.proofExpiresAt(agentId); ``` ### Sybil Detection ```solidity // Check if two agents share the same human bool same = registry.sameHuman(agentId1, agentId2); // Count agents for a nullifier uint256 count = registry.getAgentCountForHuman(nullifier); ``` ### Metadata & Agent URI ```solidity // Set/get agent URI (ERC-8004) registry.setAgentURI(agentId, "https://example.com/agent.json"); // Set/get metadata key-value pairs registry.setMetadata(agentId, "name", abi.encode("My Agent")); bytes memory value = registry.getMetadata(agentId, "name"); // Set agent wallet (requires EIP-712 signature from newWallet) registry.setAgentWallet(agentId, newWallet, deadline, signature); ``` ### Gating Access Use `AgentGate` as a base contract: ```solidity import { AgentGate } from "./AgentGate.sol"; contract MyProtocol is AgentGate { constructor(address _registry) AgentGate(_registry) {} function protectedAction() external onlyVerifiedAgent { // Only verified agents can call this bytes32 agentKey = bytes32(uint256(uint160(msg.sender))); uint256 agentId = registry.getAgentId(agentKey); // ... your logic } } ``` ### EIP-712 Meta-Transactions The `AgentDemoVerifier` contract accepts EIP-712 signed messages for gasless verification: ```solidity // Domain: { name: "AgentDemoVerifier", version: "1", chainId, verifyingContract } // Type: MetaVerify(bytes32 agentKey, uint256 nonce, uint256 deadline) function metaVerify( bytes32 agentKey, uint256 nonce, uint256 deadline, bytes calldata signature ) external; ``` ## Proof Expiry Each agent's human proof has a validity window. After expiry, `isProofFresh()` returns `false` and the agent must re-authenticate. ```solidity // Check if proof is still valid bool fresh = registry.isProofFresh(agentId); // Get the expiry timestamp uint256 expiresAt = registry.proofExpiresAt(agentId); ``` :::note Expired proofs do **not** burn the soulbound NFT. The NFT remains as a historical record, but `hasHumanProof()` returns `true` while `isProofFresh()` returns `false`. The agent must re-authenticate to restore fresh status. ::: The expiry is calculated as the minimum of: - Document expiry date (from passport) - Registration timestamp + `maxProofAge` (default: 1 year) ## Reputation Registry The `SelfReputationRegistry` is a standalone ERC-8004 Reputation Registry scoped to the `SelfAgentRegistry`. It stores aggregated feedback from clients with document-type weighted signals. ```solidity // Give feedback on an agent reputationRegistry.giveFeedback(agentId, value, decimals, tag1, tag2, endpoint, uri, hash); // Agent responds to feedback reputationRegistry.appendResponse(agentId, clientAddress, feedbackIndex, uri, hash); // Get aggregated summary (uint256 count, int256 sum, uint8 decimals) = reputationRegistry.getSummary(agentId, clients, tag1, tag2); ``` **Document-type weights** (auto-assigned at registration): | Document | Weight | Tag | |----------|--------|-----| | E-Passport (NFC) | 100 | `passport-nfc` | | EU ID Card (NFC) | 100 | `id-card-nfc` | | Aadhaar | 80 | `aadhaar` | | KYC | 50 | `kyc` | ## Validation Registry The `SelfValidationRegistry` is a standalone ERC-8004 Validation Registry for on-demand validation requests. ```solidity // Request validation from a third party bytes32 requestHash = validationRegistry.requestValidation(agentId, validator, requestURI); // Validator responds validationRegistry.respondValidation(requestHash, response, responseURI, tag); // Get validation summary (uint256 count, uint256 avgResponse) = validationRegistry.getSummary(agentId, validators, tag); ``` ## Deregistration Three paths to deregister an agent (all burn the soulbound NFT): 1. **Hub V2 callback** — `_deregisterAgent()` triggered by a new deregistration proof 2. **Self-deregister** — `selfDeregister(agentId)` called by the NFT owner 3. **Guardian revoke** — `guardianRevoke(agentId)` called by the agent's guardian All converge on `_revokeAgent()` which clears all state (credentials, nullifier, provider, guardian, metadata) and burns the NFT. ## Governance The registry uses role-based governance with two multisig wallets: | Role | Capability | |------|-----------| | `SECURITY_ROLE` | Approve proof providers, set verification configs, set max proof age | | `OPERATIONS_ROLE` | Grant roles, link reputation/validation registries | ## Integration with Self Protocol The `SelfHumanProofProvider` connects to Self Protocol's [Identity Verification Hub V2](/docs/self-pass/contracts/basic-integration/). See the [Working with userDefinedData](/docs/self-pass/contracts/working-with-userdefineddata/) guide for details on how the registration data is encoded. All core registries (SelfAgentRegistry, SelfReputationRegistry, SelfValidationRegistry) are deployed as UUPS proxies with ERC-7201 namespaced storage, enabling safe upgrades without storage collision. --- # ERC-8004 & Proof-of-Human Source: https://docs.self.xyz/docs/agent-id/erc-8004/ Self Agent ID is a reference implementation of [ERC-8004](https://eips.ethereum.org/EIPS/eip-8004) plus a proposed optional **Proof-of-Human** extension. This page explains the standard, the extension interfaces, and how Self maps onto all three ERC-8004 registry types. ## What is ERC-8004? ERC-8004 is a proposed standard for on-chain AI agent registries. It defines a minimal interface for registering agents, assigning them unique IDs (as NFTs), and looking up agent ownership. Think of it as a universal, composable identity layer for agents. The base standard is intentionally minimal. It does not specify how agents are verified or who operates them. That is where optional extensions come in, similar to how ERC-721 defines optional Metadata and Enumerable extensions within the same EIP. ```solidity /// @title IERC8004 - Agent Registry (Base Standard) interface IERC8004 { function registerAgent(bytes32 agentPubKey) external returns (uint256); function getAgentId(bytes32 agentPubKey) external view returns (uint256); function ownerOf(uint256 agentId) external view returns (address); } ``` Self's `SelfAgentRegistry` implements the fuller base surface that the EIP draft describes: `register()` overloads, `setAgentURI`, key-value metadata, and agent-wallet binding. See [Smart Contracts](/docs/agent-id/smart-contracts/) for the deployed interface. ## The problem the extension solves ERC-8004 registers agents, but it does not answer the critical question: **is this agent operated by a real, unique human?** Without proof-of-human, anyone can register unlimited agents, enabling sybil attacks, bot farms, and impersonation. Protocols that gate access to "verified agents" have no standard way to check humanity, so every project builds its own solution and the ecosystem fragments. ## Design principles The proposed `IERC8004ProofOfHuman` extension is additive: it inherits from `IERC8004` and adds only proof-of-human registration, revocation, and query functions. Three principles guide it: - **Provider-agnostic.** Any ZK identity system (Self Protocol, World ID, Humanity Protocol) can implement `IHumanProofProvider`. The registry does not care how humanity is proven. - **Sybil-resistant.** Each human produces a unique, scoped nullifier. The registry tracks agent counts per nullifier, and services choose their own limits (1, N, or unlimited). - **Privacy-preserving.** Only a nullifier is stored on-chain. No name, no passport number, no biometrics. ZK proofs verify humanity without revealing identity. ## The extension interface ```solidity /// @title IERC8004ProofOfHuman /// @notice Optional extension to ERC-8004 adding proof-of-human verification interface IERC8004ProofOfHuman is IERC8004 { // ── Registration ────────────────────────────── function registerWithHumanProof( string calldata agentURI, address proofProvider, bytes calldata proof, bytes calldata providerData ) external returns (uint256 agentId); function revokeHumanProof( uint256 agentId, address proofProvider, bytes calldata proof, bytes calldata providerData ) external; // ── Verification (read by any service/contract) ─ function hasHumanProof(uint256 agentId) external view returns (bool); function proofExpiresAt(uint256 agentId) external view returns (uint256); function isProofFresh(uint256 agentId) external view returns (bool); function getHumanNullifier(uint256 agentId) external view returns (uint256); function getProofProvider(uint256 agentId) external view returns (address); function isApprovedProvider(address provider) external view returns (bool); // ── Sybil detection ─────────────────────────── function getAgentCountForHuman(uint256 nullifier) external view returns (uint256); function sameHuman(uint256 a, uint256 b) external view returns (bool); } ``` ### The pluggable provider interface ```solidity /// @title IHumanProofProvider /// @notice Pluggable identity backend for proof-of-human interface IHumanProofProvider { /// @notice Verify a ZK proof and return (success, nullifier). /// @dev The nullifier is deterministic: same human + same scope /// always produces the same nullifier. function verifyHumanProof( bytes calldata proof, bytes calldata data ) external returns (bool verified, uint256 nullifier); /// @notice Human-readable provider name (e.g. "Self Protocol"). function providerName() external view returns (string memory); /// @notice Verification strength score (0-100). function verificationStrength() external view returns (uint8); } ``` Self's `SelfHumanProofProvider` implements this, wrapping Self Protocol's [Identity Verification Hub V2](/docs/self-pass/contracts/basic-integration/). ## Key concepts **Nullifier** — a scoped, opaque identifier unique per `(human, service)` pair, derived by the proof provider from the document. Two agents with the same nullifier belong to the same human. The nullifier is stored on-chain; the underlying document data is not. Nullifiers across different services are unlinkable. **Proof provider** — an approved contract that verifies proofs and reports a nullifier and strength. The registry keeps an allowlist (`isApprovedProvider`). Verifiers should require Self's provider unless they intentionally accept others. **maxAgentsPerHuman** — a registry-level cap on how many agents one human can hold at once (default 1). Prevents a single human from evading reputation decay by rotating agents. **proofExpiresAt** — a first-class expiry timestamp, `min(document expiry, registration time + maxProofAge)` (`maxProofAge` defaults to ~1 year). No oracle is needed: the expiry is set at registration and checked on-chain by `isProofFresh()`. `hasHumanProof()` returns true for any agent that ever proved humanity, including expired ones. `isProofFresh()` distinguishes "still valid" from "needs renewal," so callers can give different UX for each. ## Verification strength `verificationStrength()` returns a 0-100 score describing how strongly the proof binds a human identity. The SDK maps the score to a label with `getProviderLabel(strength)`: | Score | SDK label | | ----- | ---------- | | ≥ 100 | `passport` | | ≥ 80 | `kyc` | | ≥ 60 | `govt_id` | | ≥ 40 | `liveness` | | < 40 | `unknown` | Self Protocol's provider currently reports **100** (`passport`), reflecting NFC passport / ID verification with biometric match. The `/api/reputation` and `/api/verify-status` endpoints return this score and label. This is distinct from the Reputation Registry's document-type weights (see below). ## All three ERC-8004 registry types ERC-8004 defines three registry roles. Self Agent ID implements all three on-chain: - **Identity — `SelfAgentRegistry`.** Agent NFT minting, human proof storage, ZK-attested credentials, and metadata (A2A Agent Cards as `agentURI`). - **Reputation — `SelfReputationRegistry`.** Aggregated feedback with document-type weighted signals (E-Passport/EU ID Card 100, Aadhaar 80, KYC 50). - **Validation — `SelfValidationRegistry`.** On-demand validation requests and responses from third parties, with a configurable freshness threshold. See [Smart Contracts](/docs/agent-id/smart-contracts/) for the feedback, validation, and gating APIs. ## Agent documents are also A2A Agent Cards The ERC-8004 registration document served at `agentURI` is designed to be optionally valid as an A2A Agent Card. A document that also contains `url`, `version`, `provider`, `capabilities`, and `securitySchemes` is simultaneously a valid A2A Agent Card, with no separate registration step. See [Agent Registration JSON & Agent Cards](/docs/agent-id/agent-registration-json/). ## Sybil resistance properties 1. Registration reverts when a human proof is required (default) and none is supplied. 2. A nullifier can control at most `maxAgentsPerHuman` agents at once. 3. Revoking an agent frees the nullifier slot, so the human can re-register. 4. Expired proofs cause `isProofFresh()` to return false without any transaction. ## Reference implementation The reference implementation uses the UUPS upgradeable proxy pattern, but implementers can choose any deployment strategy. The interfaces are proxy-agnostic. | Contract | Celo Mainnet (42220) | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------- | | SelfAgentRegistry | [`0xaC3DF9ABf80d0F5c020C06B04Cced27763355944`](https://celoscan.io/address/0xaC3DF9ABf80d0F5c020C06B04Cced27763355944) | | SelfReputationRegistry | [`0x69Da18CF4Ac27121FD99cEB06e38c3DC78F363f4`](https://celoscan.io/address/0x69Da18CF4Ac27121FD99cEB06e38c3DC78F363f4) | | SelfValidationRegistry | [`0x71a025e0e338EAbcB45154F8b8CA50b41e7A0577`](https://celoscan.io/address/0x71a025e0e338EAbcB45154F8b8CA50b41e7A0577) | Celo Sepolia testnet addresses are listed in [Smart Contracts](/docs/agent-id/smart-contracts/#celo-sepolia-11142220). Source: [github.com/selfxyz/self-agent-id](https://github.com/selfxyz/self-agent-id). ## EIP proposal status The Self Protocol team is preparing a PR to the ERC-8004 EIP proposing proof-of-human as an optional extension section, with the full interface specification, rationale, security considerations, and a link to this reference implementation. The draft targets ethereum-magicians discussion with deployed contract addresses before formal submission. ## Next steps - [Smart Contracts](/docs/agent-id/smart-contracts/) — deployed addresses and the full on-chain API - [Agent Registration JSON & Agent Cards](/docs/agent-id/agent-registration-json/) — the identity document format - [Verification Patterns](/docs/agent-id/verification-patterns/) — how services and contracts consume these checks --- # Agent Registration JSON & Agent Cards Source: https://docs.self.xyz/docs/agent-id/agent-registration-json/ Every agent registered with `SelfAgentRegistry` can publish a JSON document at its `agentURI`. This file is the agent's identity document: it is how services, indexers, and [8004scan](https://8004scan.xyz) discover and verify the agent's capabilities. ## How it relates to A2A The document at `agentURI` is an ERC-8004 registration file. When the A2A fields (`version`, `url`, `provider`, `capabilities`, `securitySchemes`) are included, the **same** document is simultaneously a valid A2A Agent Card. There is no second document and no separate A2A registration step. The two specs read non-overlapping fields and ignore what they do not recognize. ``` ERC-8004 reads: type, name, description, image, services[], active, registrations[], supportedTrust[] A2A reads: name, description, url, version, provider, capabilities, securitySchemes, skills[], defaultInputModes, defaultOutputModes ``` ## Minimal ERC-8004 format Enough for the on-chain registry and 8004scan: ```json { "type": "https://eips.ethereum.org/EIPS/eip-8004#registration-v1", "name": "My Agent", "description": "What this agent does", "image": "https://example.com/avatar.png", "services": [ { "name": "A2A", "endpoint": "https://my-agent.example.com/a2a", "version": "1.0" } ] } ``` ## Combined ERC-8004 + A2A format Adding the A2A fields makes this single document a valid A2A Agent Card too: ```json { "type": "https://eips.ethereum.org/EIPS/eip-8004#registration-v1", "name": "My Agent", "description": "A human-backed AI agent verified via Self Protocol", "image": "https://my-agent.example.com/avatar.png", "version": "0.1.0", "url": "https://my-agent.example.com/a2a", "provider": { "name": "Acme Corp", "url": "https://acme.example.com" }, "capabilities": { "streaming": false, "pushNotifications": false }, "securitySchemes": [ { "type": "bearer", "description": "API key in Authorization header" } ], "active": true, "services": [ { "name": "A2A", "endpoint": "https://my-agent.example.com/a2a", "version": "1.0" }, { "name": "MCP", "endpoint": "https://my-agent.example.com/mcp", "version": "1.0" } ], "registrations": [ { "agentId": 42, "agentRegistry": "eip155:42220:0xaC3DF9ABf80d0F5c020C06B04Cced27763355944" } ], "supportedTrust": ["reputation"] } ``` ## Field reference | Field | Required | Protocol | Notes | | --------------------- | ----------------- | -------------- | -------------------------------------------------------------------------------------------------------- | | `type` | YES | ERC-8004 | Must be `"https://eips.ethereum.org/EIPS/eip-8004#registration-v1"` | | `name` | YES | ERC-8004 + A2A | Human-readable agent name | | `description` | YES | ERC-8004 + A2A | What the agent does | | `image` | YES | ERC-8004 | Avatar URL | | `services` | YES | ERC-8004 | At least one service endpoint | | `services[].name` | YES | ERC-8004 | One of: `web`, `A2A`, `MCP`, `OASF`, `ENS`, `DID`, `email` | | `services[].endpoint` | YES | ERC-8004 | Service URI | | `services[].version` | YES (for A2A/MCP) | ERC-8004 | Required by A2A protocol, e.g. `"1.0"` | | `active` | NO | ERC-8004 | Set `false` when the agent is inactive (e.g. proof expired) | | `registrations` | NO | ERC-8004 | Cross-chain registry refs using CAIP-10: `eip155::
` | | `supportedTrust` | NO | ERC-8004 | `reputation`, `crypto-economic`, `tee-attestation` | | `version` | NO\* | A2A | Agent **software** version (e.g. `"0.1.0"`). \*Required for an A2A Agent Card | | `url` | NO\* | A2A | A2A primary endpoint. MUST equal `services[name="A2A"].endpoint`. \*Required for an A2A Agent Card | | `provider` | NO\* | A2A | Publisher identity `{ name, url?, email? }`. \*Required for an A2A Agent Card | | `capabilities` | NO\* | A2A | `{ streaming: bool, pushNotifications: bool }`. \*Required for an A2A Agent Card | | `securitySchemes` | NO\* | A2A | Auth methods. `{ type: "bearer" \| "apiKey" \| "oauth2" \| "none" }[]`. \*Required for an A2A Agent Card | | `defaultInputModes` | NO | A2A | MIME types the agent accepts, e.g. `["text/plain"]` | | `defaultOutputModes` | NO | A2A | MIME types the agent produces | | `skills` | NO | A2A | `[{ name, description? }]` — specific tasks the agent offers | ## Constraints when combining the two - `url` (the A2A primary endpoint) and `services[name="A2A"].endpoint` **MUST** point to the same address. A2A clients read `url`; ERC-8004 indexers read `services`. - `version` = agent **software** version (e.g. `"0.1.0"`); `services[].version` = **protocol** version (e.g. `"1.0"`). These are distinct fields. - The path `/.well-known/agent-card.json` may serve this same document for A2A well-known discovery — no duplication required. ## Agents minted via Self Protocol start with a blank agentURI :::warning Agents registered through the **Hub V2 Self Protocol flow** (the `verifySelfProof` callback that mints the NFT) are created with a **blank `agentURI`**. The on-chain `Registered` event carries an empty string for the URI. ::: You **must** call `setAgentURI()` after registration: ```solidity registry.setAgentURI(agentId, "https://my-agent.example.com/.well-known/agent-card.json"); ``` Until `setAgentURI()` is called: - 8004scan indexes the agent with no identity document - A2A clients cannot discover your service endpoints - The `active` and `services` fields are invisible to the ecosystem This does not apply to agents registered via the `register(agentURI)` or `registerWithHumanProof(agentURI, ...)` overloads, which accept a URI directly. ## On-chain metadata When registered via `SelfAgentRegistry`, agents can carry key-value metadata set with `setMetadata(agentId, key, value)`: | Key | Description | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `agentWallet` | Reserved. Set via `setAgentWallet()` — a payment address separate from the NFT owner, with an EIP-712 signature from the new wallet. | Any custom key is allowed except the reserved `agentWallet`. ## SDK helper The TypeScript SDK builds valid documents with `generateRegistrationJSON`: ```typescript import { generateRegistrationJSON } from "@selfxyz/agent-sdk"; // Minimal ERC-8004 only const minimalDoc = generateRegistrationJSON({ name: "My Agent", description: "What it does", image: "https://...", services: [{ name: "A2A", endpoint: "https://...", version: "1.0" }], }); // Combined ERC-8004 + A2A (single document, both protocols) const fullDoc = generateRegistrationJSON({ name: "My Agent", description: "What it does", image: "https://...", services: [ { name: "A2A", endpoint: "https://my-agent.example.com/a2a", version: "1.0" }, ], a2a: { version: "0.1.0", url: "https://my-agent.example.com/a2a", provider: { name: "Acme Corp" }, capabilities: { streaming: false, pushNotifications: false }, securitySchemes: [{ type: "bearer" }], }, }); ``` ## Hosting requirements The JSON file must be: 1. Accessible over HTTPS at the `agentURI` provided during registration 2. Served with `Content-Type: application/json` 3. Updated whenever the agent's services or status change (update the document, then call `setAgentURI()` if the URL itself moved) ## Validation Use the 8004scan validator at [8004scan.xyz](https://8004scan.xyz) to confirm your registration JSON is correctly formatted before registering. ## Where the API serves cards from The API reads the on-chain `agentMetadata` and serves the stored card as HTTP JSON, so you do not have to host anything yourself if you set the card on-chain: - `GET /api/cards/{chainId}/{agentId}` — the Agent Card JSON - `GET /.well-known/a2a/{agentId}?chain={chainId}` — redirects to the card resolver See [REST API](/docs/agent-id/rest-api/) and [SDK Integration](/docs/agent-id/sdk-integration/#a2a-agent-cards). --- # REST API Source: https://docs.self.xyz/docs/agent-id/rest-api/ Self Agent ID exposes REST endpoints for registration workflows, agent queries, and A2A discovery. The base URL is `https://agent-api.self.xyz`. There is no consumer web app, but the full API stays available at this host. Prefix the paths below with the base URL. The full OpenAPI 3.1 spec is available at [`openapi.yaml`](https://github.com/selfxyz/self-agent-id/blob/main/openapi.yaml) in the main repository — import it into Postman or use it to generate clients. :::note Most query endpoints are thin wrappers over registry contract reads, so you can also reproduce them directly with the SDK or a plain RPC call. See [Smart Contracts](/docs/agent-id/smart-contracts/). To register without calling the API at all, render the QR in your own frontend with the SDK — see [Register an Agent with the SDK](/docs/agent-id/register-an-agent/). ::: ## Public Query Endpoints These endpoints require no authentication. ### Get Agent Info ``` GET /api/agent/info/{chainId}/{agentId} ``` Returns agent registration details, verification status, and credentials. **Example response:** ```json { "agentId": 5, "chainId": 11142220, "agentAddress": "0x83fa4380903fecb801F4e123835664973001ff00", "isVerified": true, "proofProvider": "0x5E61c3051Bf4115F90AacEAE6212bc419f8aBB6c", "verificationStrength": 100, "strengthLabel": "passport", "credentials": { "nationality": "GBR", "olderThan": 18, "ofac": [false, false, false] }, "registeredAt": 1740000000, "network": "testnet" } ``` ### List Agents by Human Address ``` GET /api/agent/agents/{chainId}/{address} ``` Returns all agent IDs registered by a specific human wallet address. **Example response:** ```json { "agents": [5, 12, 37], "chainId": 11142220, "humanAddress": "0xabc..." } ``` ### Verify Agent Proof-of-Human ``` GET /api/agent/verify/{chainId}/{agentId} ``` Checks whether an agent has valid proof-of-human verification, the proof provider address, verification strength, and Sybil metrics. `isSelfProvider` is `true` when the proof was issued by Self Protocol's provider. `agentCountForHuman` is how many agents share this human's nullifier. **Example response:** ```json { "agentId": 5, "chainId": 11142220, "isVerified": true, "proofProvider": "0x5E61c3051Bf4115F90AacEAE6212bc419f8aBB6c", "isSelfProvider": true, "verificationStrength": 100, "strengthLabel": "passport", "humanNullifier": "12345678901234567890", "agentCountForHuman": 1 } ``` When an agent has no proof, the response is `{ "agentId", "chainId", "isVerified": false, "proofProvider": "0x000…000", "isSelfProvider": false, "verificationStrength": 0, "strengthLabel": "None", "humanNullifier": "0", "agentCountForHuman": 0 }`. ### Get Agent Card ``` GET /api/cards/{chainId}/{agentId} ``` Returns the A2A-compatible agent identity card in JSON format. ### Get Reputation Score ``` GET /api/reputation/{chainId}/{agentId} ``` Returns the agent's verification strength score from the proof provider. **Example response:** ```json { "score": 100, "hasProof": true, "providerName": "Self Protocol", "proofType": "passport" } ``` The `score` is the provider's `verificationStrength()` (0-100). `proofType` is the SDK label for that score: `passport` (≥100), `kyc` (≥80), `govt_id` (≥60), `liveness` (≥40), else `unknown`. Self Protocol's provider currently returns `100` / `passport`. ### Get Verification Status ``` GET /api/verify-status/{chainId}/{agentId} ``` Returns real-time proof status and freshness. **Example response:** ```json { "verified": true, "proofType": "passport", "registeredAtBlock": "12345678", "providerAddress": "0x5E61c3051Bf4115F90AacEAE6212bc419f8aBB6c" } ``` When the agent has no proof, the response is simply `{ "verified": false }`. ### A2A Discovery ``` GET /.well-known/a2a/{agentId}?chain={chainId} ``` Redirects to the agent card resolver. Compatible with A2A agent discovery protocols. ### A2A Protocol (JSON-RPC) ``` POST /api/a2a ``` Agent-to-Agent JSON-RPC 2.0 endpoint for programmatic interaction. Agents can register, verify, look up, deregister, and check proof freshness through natural language or structured intents. **Example request:** ```json { "jsonrpc": "2.0", "id": 1, "method": "message/send", "params": { "message": { "role": "user", "parts": [{ "type": "data", "data": { "intent": "register" } }] } } } ``` Supported intents: `register`, `status`, `lookup`, `verify`, `deregister`, `freshness`, `help`. All intents default to mainnet. Add `network: "testnet"` or `chainId: 11142220` for Celo Sepolia (mock documents via the Self app — no real passport needed). ### Service Discovery ``` GET /.well-known/self-agent-id.json ``` Returns the service discovery document with API base URL, supported networks, registration modes, and capabilities. **Example response:** ```json { "name": "Self Agent ID", "version": "1.0", "apiBase": "https://agent-api.self.xyz/api/agent", "networks": ["mainnet", "testnet"], "registrationModes": ["linked", "wallet-free", "ed25519", "ed25519-linked", "privy", "smartwallet"], "capabilities": ["register", "deregister", "verify", "credentials", "agent-card", "a2a"], "sessionTtlMs": 1800000 } ``` ### List Agents by Nullifier ``` GET /api/agent/agents-by-nullifier/{chainId}/{nullifier} ``` Returns all agent IDs associated with a given human nullifier. Useful for Sybil detection — check how many agents a single human has registered. **Example response:** ```json { "nullifier": "123456789...", "agents": [5, 12], "chainId": 42220 } ``` ### Health Check ``` GET /api/health ``` Returns service health status. No authentication required. ## Identify Endpoints Used to discover existing agents for a human without registering a new one. The human scans their passport and the system returns their nullifier, which can be used to look up all associated agents. ### Create Identify Session ``` POST /api/agent/identify ``` Creates a passport scan session for agent discovery. Returns a QR code and deep link for the Self app. **Request body:** ```json { "network": "mainnet" } ``` ### Poll Identify Status ``` GET /api/agent/identify/status?token={sessionToken} ``` Polls for identification completion. Returns the human's nullifier and agent count once the passport scan is complete. ## Proof Refresh Endpoints Used to refresh an existing agent's proof without re-registering. The human re-scans their passport to update the proof on-chain. ### Create Refresh Session ``` POST /api/agent/refresh ``` Creates a proof refresh session for an existing agent. **Request body:** ```json { "agentId": 42, "network": "mainnet", "disclosures": { "minimumAge": 18, "ofac": true } } ``` ### Poll Refresh Status ``` GET /api/agent/refresh/status?token={sessionToken} ``` Polls for refresh completion. Returns updated verification details once the proof is refreshed on-chain. ## Registration Endpoints Used by the dApp and CLI during registration flows. Sessions use encrypted tokens with a 30-minute TTL. ### Create Registration Session ``` POST /api/agent/register ``` Creates a new registration session. Returns session token, QR code data, deep link, and the generated agent address. **Request body:** ```json { "mode": "linked", "network": "mainnet", "humanAddress": "0xYourWalletAddress", "disclosures": { "minimumAge": 18, "ofac": true, "nationality": false, "name": false } } ``` Modes: `linked`, `wallet-free`, `ed25519`, `ed25519-linked`, `privy`, `smartwallet`. Networks: `mainnet` (default, real passports via Self app), `testnet` (mock documents via Self app, no real passport needed). **Example response:** ```json { "sessionToken": "enc_...", "deepLink": "selfapp://verify?scope=...", "qrData": "selfapp://verify?scope=...", "agentAddress": "0x83fa...ff00", "mode": "linked", "network": "mainnet" } ``` ### Poll Registration Status ``` GET /api/agent/register/status?token={sessionToken} ``` Polls for registration completion. Returns stage: `qr-ready`, `proof-received`, `completed`, or `failed`. **Example response:** ```json { "stage": "completed", "agentId": 42, "agentAddress": "0x83fa...ff00", "txHash": "0xabc...", "sessionToken": "enc_..." } ``` ### Get QR Code ``` GET /api/agent/register/qr?token={sessionToken} ``` Returns the QR code payload and deep link for the current session. ### Self App Callback ``` POST /api/agent/register/callback?token={sessionToken} ``` Webhook endpoint called by the Self app after the user scans the QR and submits a passport proof. ### Export Agent Private Key ``` GET /api/agent/register/export?token={sessionToken} ``` After registration completes, export the agent's private key. Only available for `linked`, `wallet-free`, `ed25519`, and `ed25519-linked` modes. **Example response:** ```json { "privateKey": "0xdeadbeef...", "agentAddress": "0x83fa...ff00", "agentId": 42 } ``` ## Deregistration Endpoints ### Create Deregistration Session ``` POST /api/agent/deregister ``` Creates a deregistration session for an existing agent. The human must re-prove identity to burn the agent NFT. **Request body:** ```json { "network": "testnet", "agentAddress": "0xAgentAddress", "disclosures": { "minimumAge": 18, "ofac": true } } ``` ### Poll Deregistration Status ``` GET /api/agent/deregister/status?token={sessionToken} ``` Returns current deregistration stage. Once completed, the agent NFT has been burned. ### Deregistration Callback ``` POST /api/agent/deregister/callback?token={sessionToken} ``` Webhook endpoint for the Self app after the user confirms deregistration. ## Demo Endpoints Used by the live demo page for testing agent verification patterns. All demo endpoints require `x-self-agent-*` signed headers (produced by `SelfAgent.signRequest()`). ### Verify Agent (Service Pattern) ``` POST /api/demo/verify ``` Accepts a signed agent request, verifies signature and on-chain status, returns agent info and credentials. ### Agent-to-Agent ``` POST /api/demo/agent-to-agent ``` Demo agent endpoint that verifies the caller, checks `sameHuman()`, and signs its response. Response headers include the demo agent's own `x-self-agent-*` signature for mutual verification. ### Chain Verify (Meta-Transaction) ``` POST /api/demo/chain-verify ``` Relayer endpoint that submits EIP-712 meta-transactions to the `AgentDemoVerifier` contract. Rate limited to 3 per hour per human nullifier. **Request body:** ```json { "agentKey": "0x...", "nonce": "0", "deadline": 1740001800, "eip712Signature": "0x...", "networkId": "celo-sepolia" } ``` ### Chain Verify Ed25519 (Meta-Transaction) ``` POST /api/demo/chain-verify-ed25519 ``` Same as chain-verify but for agents registered with Ed25519 keys. Relays EIP-712 meta-transactions signed with Ed25519. ### Census (Aggregated Stats) ``` POST /api/demo/census — contribute credentials GET /api/demo/census — read aggregate stats ``` Demonstrates agent-gated data contribution and reading. ### AI Chat ``` POST /api/demo/chat ``` AI agent chat endpoint backed by a LangChain agent. Signed requests are verified on-chain; unsigned requests are treated as anonymous. **Request body:** ```json { "query": "What is Self Agent ID?", "session_id": "user-123" } ``` ## AA Proxy Endpoints Account-abstraction proxy for the smart-wallet registration mode. These endpoints proxy JSON-RPC calls to a Pimlico bundler/paymaster, shielding the API key from the client. Origin-restricted and rate-limited. ### Issue AA Proxy Token ``` POST /api/aa/token?chainId={chainId} ``` Issues a short-lived token required by the bundler and paymaster endpoints. Rate limited to 30 requests per minute per IP. **Example response:** ```json { "token": "eyJhbGciOi...", "expiresAt": 1740001800000 } ``` ### Bundler Proxy ``` POST /api/aa/bundler?chainId={chainId} ``` Proxies ERC-4337 bundler JSON-RPC calls. Requires the `x-aa-proxy-token` header from the token endpoint. **Allowed methods:** `eth_chainId`, `eth_supportedEntryPoints`, `eth_estimateUserOperationGas`, `eth_sendUserOperation`, `eth_getUserOperationReceipt`, `eth_getUserOperationByHash`, `eth_gasPrice`, `eth_maxPriorityFeePerGas`. **Request body (JSON-RPC 2.0):** ```json { "jsonrpc": "2.0", "method": "eth_sendUserOperation", "params": [...], "id": 1 } ``` ### Paymaster Proxy ``` POST /api/aa/paymaster?chainId={chainId} ``` Proxies ERC-4337 paymaster JSON-RPC calls. Requires the `x-aa-proxy-token` header. **Allowed methods:** `pm_sponsorUserOperation`, `pm_getPaymasterData`, `pm_getPaymasterStubData`, `eth_chainId`. ## Authentication - **Public endpoints** (info, agents, verify, cards, reputation, verify-status, discovery): No authentication required - **Demo endpoints**: Require `x-self-agent-*` headers (signed via SDK) - **Registration endpoints**: Session-based with encrypted tokens, used by dApp/CLI - **AA Proxy endpoints**: Origin-restricted + `x-aa-proxy-token` header (from `/api/aa/token`) ## Chain ID Parameter All endpoints that accept a `chainId` parameter support: - `42220` — Celo Mainnet - `11142220` — Celo Sepolia Testnet :::warning Use chain ID `11142220` for Celo Sepolia, not `44787` (deprecated Alfajores). ::: ## Error Codes All errors return `{ "error": "message" }` with the appropriate HTTP status. | Code | Meaning | | ---- | ------------------------------------------------------------------ | | 400 | Bad request — invalid parameters, missing fields, or wrong mode | | 401 | Invalid or tampered session token / missing auth headers | | 403 | Origin check failed or agent verification failed | | 404 | Agent not found on-chain | | 409 | Operation not available at current session stage / replay detected | | 410 | Session expired (30-minute TTL) | | 413 | Request too large (AA proxy, 200 KB limit) | | 429 | Rate limit exceeded | | 500 | Server error — RPC failure or configuration issue | | 503 | Service not configured (bundler, paymaster, or LangChain) | --- # CLI Source: https://docs.self.xyz/docs/agent-id/cli/ Self Agent ID includes a cross-language CLI for registering and deregistering agents from the terminal. Available in TypeScript, Python, and Rust with identical command surfaces. :::note The CLI talks to the API at `https://agent-api.self.xyz`. Override it with the `SELF_AGENT_API_BASE` environment variable if you run your own deployment. The CLI fetches the scannable QR from the API (`GET /api/qr/{sessionToken}`), so no consumer web app is needed. ::: ## Install ::::tabs :::tab{label="TypeScript"} ```bash npm install -g @selfxyz/agent-sdk # or use npx: npx @selfxyz/agent-sdk register init ... ``` ::: :::tab{label="Python"} ```bash pip install selfxyz-agent-sdk # Then use: self-agent register init ... ``` ::: :::tab{label="Rust"} ```bash cargo install self-agent-sdk # Then use: self-agent register init ... ``` ::: :::: ## Registration Flow The CLI uses a **browser handoff** pattern: the terminal creates a session, generates a URL, and the user completes the Self proof in their browser. ### Step 1: Create Session ```bash self-agent register init \ --mode linked \ --human-address 0xYourWalletAddress \ --network mainnet \ --out .self/session.json ``` **Modes:** `linked`, `wallet-free`, `ed25519`, `ed25519-linked`, `smartwallet` :::note The default network is **mainnet**, which requires a real passport scanned via the Self app. Use `--network testnet` for development — testnet also requires the Self app, but you can generate mock documents within the app instead of using a real passport. ::: ### Step 2: Open Browser Handoff ```bash self-agent register open --session .self/session.json ``` Opens the handoff URL in the default browser. The user scans the QR code with the Self app. ### Step 3: Wait for Completion ```bash self-agent register wait --session .self/session.json ``` Polls the registration status until the Hub V2 callback confirms verification. ### Step 4: Check Status ```bash self-agent register status --session .self/session.json ``` Returns the current session state (pending, verified, failed). ### Step 5: Export Credentials ```bash self-agent register export --session .self/session.json ``` Outputs the agent address, agent key (bytes32), agent ID, and private key for use in your agent's environment. ## Deregistration Flow ```bash # Create deregistration session self-agent deregister init \ --mode linked \ --human-address 0xYourWalletAddress \ --network mainnet \ --out .self/session-deregister.json # Open browser for Self proof self-agent deregister open --session .self/session-deregister.json # Wait for completion self-agent deregister wait --session .self/session-deregister.json ``` ## Ed25519 Registration For agents that use Ed25519 keys instead of Ethereum wallets. Two modes are available: ### Standalone Ed25519 Register an agent identified solely by its Ed25519 public key: ```bash self-agent register init \ --mode ed25519 \ --ed25519-pubkey \ --ed25519-signature \ --network mainnet \ --out .self/session.json ``` ### Ed25519 Linked to Human Register an Ed25519 agent linked to a human's Ethereum address: ```bash self-agent register init \ --mode ed25519-linked \ --ed25519-pubkey \ --ed25519-signature \ --human-address 0xYourWalletAddress \ --network mainnet \ --out .self/session.json ``` :::note The `--ed25519-signature` is a hex-encoded signature over the session challenge, proving ownership of the Ed25519 private key. The `--ed25519-pubkey` is the hex-encoded 32-byte public key. ::: After `init`, the remaining steps (`open`, `wait`, `status`, `export`) are identical to the standard registration flow. ## Agent-Guided Flow (Recommended) For automated onboarding, your backend or agent runtime orchestrates the CLI commands and sends the handoff URL to the user: 1. Backend calls `register init` and stores session state 2. Backend calls `register open` and forwards URL to user UI 3. User completes browser proof flow 4. Backend runs `register wait` and records the returned lifecycle state :::note The agent-guided flow is the recommended integration pattern for services that onboard users programmatically. The CLI handles all the complexity of session management and proof verification. ::: ## Canonical challenge domain For every mode except `self-custody`, the agent key signs a challenge proving it controls the key. All SDKs hash the same domain so a session created by one CLI is verifiable by any other: ``` keccak256(abi.encodePacked("self-agent-id:register:", humanIdentifier, chainId, registryAddress, nonce)) ``` The hashing and the `(r, s, v)` signature split must match across TypeScript, Python, and Rust. The same value is reproduced on-chain when the registry verifies the agent signature alongside the Self ZK proof. ## Session schema (v1) The session file (`.self/session.json`) is a structured record, not a flat blob. Top-level keys: | Key | Notes | | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `version` | Schema version (`1`) | | `operation` | `register` or `deregister` | | `sessionId`, `createdAt`, `expiresAt` | Session identity and TTL | | `mode`, `disclosures` | Registration mode and selected disclosures | | `network` | `{ chainId, rpcUrl, registryAddress, endpointType, appUrl, appName, scope }` | | `registration` | `{ humanIdentifier, agentAddress, userDefinedData, challengeHash, signature, smartWalletTemplate? }` (`challengeHash`/`signature` for non-`self-custody` modes) | | `callback` | `{ listenHost: "127.0.0.1", listenPort, path: "/callback", stateToken, used, lastStatus?, lastError? }` | | `state` | `{ stage, updatedAt, lastError?, agentId?, guardianAddress? }` | | `secrets` | `{ agentPrivateKey }` — generated-key modes only (`linked`, `wallet-free`, `smartwallet`) | ### Local session stages The local session file moves through these `state.stage` values: ``` initialized → handoff_opened → callback_received → onchain_verified → onchain_deregistered (deregister flow) → failed | expired ``` :::note These are the **local CLI** stages. They are distinct from the **API** registration stages (`qr-ready`, `proof-received`, `completed`, `failed`) returned by `GET /api/agent/register/status`. The CLI reconciles the API/on-chain state into its own session file. ::: ## Browser handoff & callback contract `register open` encodes the session into a `payload=` parameter for the API's `/cli/register` handoff page. The payload carries: `version`, `operation`, `sessionId`, `stateToken`, `callbackUrl`, `mode`, `chainId`, `registryAddress`, `endpointType`, `appName`, `scope`, `humanIdentifier`, `expectedAgentAddress`, `expiresAt`, and optionally `disclosures`, `userDefinedData`, `smartWalletTemplate`. When the browser flow completes, it POSTs JSON back to the CLI's loopback callback (`http://127.0.0.1:/callback`): `{ sessionId, stateToken, status: "success" | "error", timestamp, operation?, error?, guardianAddress? }`. The CLI rejects callbacks whose `sessionId` / `stateToken` do not match, and rejects replays. ## Security 1. Exporting the agent private key is blocked unless `--unsafe` is passed explicitly. 2. Session and key files use restricted file permissions. Treat them as sensitive local state. 3. The callback listener binds to the loopback host only. 4. Session expiry is enforced before handoff and wait operations. 5. Rotate or delete old session files after a successful registration. ## Refreshing an expired proof Human proofs expire at `min(document expiry, registration time + maxProofAge)` (default `maxProofAge` ≈ 365 days). After expiry, `isProofFresh(agentId)` returns `false`. The CLI surfaces `proofExpiresAt` in `register status` and warns when expiry is within 30 days. There is no in-place CLI refresh command. To refresh, run the full deregister flow then a new register flow, which mints a **new** `agentId` (update any stored references): ```bash self-agent deregister init --mode linked --human-address 0x... --agent-address 0x... --network mainnet --out .self/dereg.json self-agent deregister open --session .self/dereg.json # complete Self proof self-agent deregister wait --session .self/dereg.json self-agent register init --mode linked --human-address 0x... --network mainnet --out .self/refresh.json self-agent register open --session .self/refresh.json # complete Self proof self-agent register wait --session .self/refresh.json ``` :::note The REST API also exposes an in-place [refresh endpoint](/docs/agent-id/rest-api/#proof-refresh-endpoints) (`POST /api/agent/refresh`) that re-proves an existing agent without minting a new ID. The CLI does not wrap it yet. ::: ## Network Flag | Value | Chain | Notes | | ------------------- | ----------------------- | ----------------------- | | `mainnet` (default) | Celo Mainnet (42220) | Real passports required | | `testnet` | Celo Sepolia (11142220) | Mock documents only | --- # Celo Agent Visa Source: https://docs.self.xyz/docs/agent-id/celo-agent-visa/ The Celo Agent Visa is a tiered soulbound NFT system that rewards AI agents for on-chain activity on the Celo blockchain. Agents progress through tiers based on transaction count and stablecoin volume, earning increasingly prestigious visa status. ## Tier System | Tier | Name | Requirements | Manual Review | | ---- | ---------------- | ----------------------------------------------------- | ------------- | | 1 | **Tourist Visa** | Self Agent Registry entry + 1 transaction | No | | 2 | **Work Visa** | Human proof + 1,000 txs OR $5,000 stablecoin volume | No | | 3 | **Citizenship** | Human proof + 10,000 txs OR $15,000 stablecoin volume | Yes | Tier 2 and 3 use **OR** logic for metrics — meeting either the transaction count or volume threshold qualifies the agent. Tier 3 (Citizenship) requires manual review approval from the Celo team. ## How It Works 1. **Register an agent** via [Self Agent ID](/docs/agent-id/overview/) — this is a prerequisite 2. **Transact on Celo** — the scoring service monitors transaction count and stablecoin volume (USDT, USDC, cUSD) 3. **Check eligibility** — visit the visa dashboard or query the API 4. **Claim your visa** — the system mints a soulbound NFT at no gas cost (relayer pays) 5. **Progress through tiers** — as your metrics grow, request upgrades to higher tiers All on-chain operations are gasless for end users. The Self relayer infrastructure covers transaction costs. ## Deployed Contracts | Network | Contract | Address | | ------------ | ------------- | -------------------------------------------- | | Celo Mainnet | CeloAgentVisa | `0xCa97f7586CF9De62B8ca516d7Ee25f6AEae5e109` | | Celo Sepolia | CeloAgentVisa | `0xf049FD6260Fce964B82728A86CF1BbEB8AB3E875` | The contract is a soulbound ERC-721 (non-transferable) using UUPS upgradeable pattern. Visa NFTs are permanently tied to the agent's identity and cannot be traded. ## API Endpoints All visa endpoints are served at `https://agent-api.self.xyz`. The read endpoints are wrappers over `CeloAgentVisa` contract reads, so you can also call the contract directly; the claim and review endpoints use a relayer. ### Get Visa Status ``` GET /api/visa/{chainId}/{agentId} ``` Returns the agent's current tier, metrics, eligibility for each tier, and manual review status. **Example response:** ```json { "agentId": 42, "chainId": 42220, "tier": 1, "tierName": "Tourist Visa", "metrics": { "transactionCount": 1250, "volumeUsd": 6200, "lastUpdated": 1745123456 }, "eligibility": { "1": true, "2": true, "3": false }, "reviewRequestedTier": 0, "manualReviewApproved": false } ``` ### List Agents by Wallet ``` GET /api/visa/agents?wallet={address}&chainId={chainId} ``` Returns all agent IDs owned by a wallet address. ### Claim or Upgrade Visa ``` POST /api/visa/claim ``` Gasless visa mint (for new visas) or tier upgrade. The relayer submits the transaction on behalf of the user. **Request body:** ```json { "chainId": "42220", "agentId": "42", "targetTier": 2, "agentWallet": "0x..." } ``` Returns `422 REVIEW_REQUIRED` if the target tier requires manual review that hasn't been approved yet. Returns `422 NOT_ELIGIBLE` if metrics don't meet thresholds. ### Request Manual Review ``` POST /api/visa/request-review ``` Requests manual review from the Celo team for Tier 2 or 3 upgrades. **Request body:** ```json { "chainId": "42220", "agentId": "42", "targetTier": 2 } ``` ## Reading Visa Status On-Chain ```typescript import { getContract } from "viem"; const visa = getContract({ address: "0xCa97f7586CF9De62B8ca516d7Ee25f6AEae5e109", abi: VISA_ABI, client: publicClient, }); // Get current tier (0 = no visa, 1 = Tourist, 2 = Work, 3 = Citizenship) const tier = await visa.read.getTier([BigInt(agentId)]); // Get cached metrics const metrics = await visa.read.getMetrics([BigInt(agentId)]); // Returns: [transactionCount, volumeUsd, lastUpdated] // Check eligibility for a specific tier const eligible = await visa.read.checkTierEligibility([BigInt(agentId), 2]); ``` ## Metrics & Scoring Metrics are calculated off-chain by a scoring service and pushed on-chain periodically: - **Transaction count** — all outgoing transactions from the agent's wallet (via RPC) - **Stablecoin volume** — USD volume of ERC-20 transfers for supported stablecoins (via TheGraph subgraph) **Supported stablecoins (Celo Mainnet):** | Token | Address | | ----------- | -------------------------------------------- | | USDT | `0x48065fbbE25f71C9282ddf5e1cD6D6A887483D5e` | | USDC | `0xcebA9300f2b948710d2653dD7B07f33A8B32118C` | | cUSD (USDm) | `0x765DE816845861e75A25fCA122bb6898B8B1282a` | ## Claim Flow ``` Agent registered in Self Agent Registry ↓ Agent transacts on Celo (transfers, swaps, contract calls) ↓ Scoring service detects activity and pushes metrics on-chain ↓ Agent checks eligibility via dashboard or API ↓ ┌─ Tier 1 (Tourist): Claim directly — no review needed ├─ Tier 2 (Work): Claim directly once metrics are met └─ Tier 3 (Citizenship): Request review → Celo team approves → Claim upgrade ↓ Soulbound NFT minted/upgraded (gasless) ``` :::note The visa contract uses `agentId` as its primary key, not wallet address. This means if an agent's wallet changes via the Self Agent Registry, the visa follows the agent identity. ::: --- # Troubleshooting Source: https://docs.self.xyz/docs/agent-id/troubleshooting/ ## `userDefinedData` encoding fails **This is the #1 integration mistake.** The Self SDK passes `userDefinedData` as a **UTF-8 string**, not raw bytes. Each position uses the ASCII character — `'0'` (0x30), not `0x00`. ```solidity // WRONG — raw byte value bytes32 data = bytes32(uint256(0)); // CORRECT — ASCII character bytes32 data = bytes32(bytes1(uint8(uint8(bytes1("0"))))); ``` Use `bytes32(bytes1(uint8(x)))` for byte positioning in `bytes32`. :::danger If your contract receives garbled `userDefinedData`, this is almost certainly the cause. ::: ## "Agent not verified" when querying **Possible causes:** 1. **Wrong chain ID** — Are you querying the right network? Mainnet is `42220`, testnet is `11142220`. 2. **Wrong registry address** — Each network has its own registry. See [Smart Contracts](/docs/agent-id/smart-contracts/) for addresses. 3. **Agent not yet registered** — Registration requires the Self app callback to complete. Poll with `self_check_registration` or the REST API status endpoint. 4. **Agent was deregistered** — The soulbound NFT was burned. Re-registration is needed. 5. **Proof expired** — The agent's human proof has passed its validity window. Check `isProofFresh(agentId)` — if it returns `false`, the agent must re-authenticate by scanning their passport again. ## Agent proof expired If `isProofFresh()` returns `false` but `hasHumanProof()` returns `true`: - The proof validity window has elapsed (default: 1 year or document expiry, whichever is sooner) - The soulbound NFT is NOT burned — it remains as a historical record - The agent must re-authenticate via the Self app to restore fresh status - SDK verifiers automatically reject expired proofs with a descriptive error ## Registration callback never arrives **Check these:** 1. **`SELF_ENDPOINT` type mismatch** — Use `celo` for mainnet, `staging_celo` for testnet. If set wrong, the Self app sends the callback to the wrong network. 2. **`.env.local` key must be lowercase** — `NEXT_PUBLIC_SELF_ENDPOINT` (not `NEXT_PUBLIC_Self_Endpoint`). Environment variable names are case-sensitive. 3. **Callback URL not reachable** — If running locally, ensure the callback port is accessible. The CLI binds to `127.0.0.1` by default. 4. **Session expired** — Sessions have a 30-minute TTL. Start a new registration if expired. ## Signature verification fails **Common causes:** 1. **Timestamp drift** — The verifier rejects signatures older than 5 minutes (default). Ensure client and server clocks are in sync. 2. **Body encoding** — The signature covers `keccak256(timestamp + METHOD + path + bodyHash)`. If the body is modified in transit (e.g., by a proxy re-serializing JSON), verification fails. 3. **Path vs full URL** — The SDK signs the URL path (e.g., `/api/data`), not the full URL. Ensure your verifier extracts the path correctly. 4. **Wrong private key** — The signing key must match the registered agent address. ## Wrong chain ID :::warning Celo Sepolia chain ID is **11142220**, not `44787` (deprecated Alfajores). This is a common mistake when migrating from older Celo testnet configurations. ::: | Network | Chain ID | RPC | |---------|----------|-----| | Celo Mainnet | `42220` | `https://forno.celo.org` | | Celo Sepolia | `11142220` | `https://forno.celo-sepolia.celo-testnet.org` | ## Foundry build fails If you see errors about `PUSH0` or unsupported opcodes: ```bash # Hub V2 uses PUSH0 (EVM version cancun) forge build --evm-version cancun forge test --evm-version cancun forge script ... --evm-version cancun ``` Add to `foundry.toml`: ```toml [profile.default] evm_version = "cancun" ``` ## Provider verification untrusted If agents pass `isVerifiedAgent()` but you don't trust the proof source: ```typescript const verifier = SelfAgentVerifier.create() .requireSelfProvider() // Ensures Self Protocol as provider .build(); ``` Without `requireSelfProvider()`, any approved provider's proofs are accepted. Self Protocol's provider is the only one currently deployed, but this guard protects against future third-party providers. ## WebAuthn / passkey errors **Firefox blocks WebAuthn on `http://localhost`**. Use one of: - **Chrome** on `http://localhost` (Chrome allows it) - **HTTPS** with a local cert (e.g., `mkcert`) - **Deployed URL** with HTTPS This only affects smart wallet mode (passkey-based registration). ## Smart wallet not gasless on testnet Smart wallet mode is **counterfactual only** on testnet — the smart account address is computed but not deployed, so gasless transactions aren't available. On **mainnet**, the Pimlico paymaster sponsors gas for registration transactions, enabling full gasless UX. ## Blockscout / Celoscan verification issues - **Blockscout** does not require an API key for contract verification. - **Celoscan** — use the **Sourcify verifier** (not the etherscan verifier). The Celoscan API endpoint is flaky; Sourcify is more reliable. ```bash # Celoscan via Sourcify forge verify-contract
\ --chain 42220 \ --verifier sourcify ``` ## "InvalidIdentityCommitmentRoot" error You're using the wrong network. Real passports only work on **mainnet** (Celo, chain ID 42220). Mock documents only work on **testnet** (Celo Sepolia, chain ID 11142220). Make sure your registration network matches your document type. ## A2A endpoint unreachable If other agents cannot reach your A2A endpoint: 1. **Endpoint not registered** — The `agentURI` metadata must include the A2A endpoint URL. Check that `setAgentURI()` was called after registration. 2. **Firewall / NAT** — Your A2A server must be publicly reachable. If running locally, use a tunnel (e.g., `ngrok`) and register the tunnel URL as your endpoint. 3. **TLS required** — A2A endpoints must be served over HTTPS. Plain HTTP connections are rejected by most agent runtimes. 4. **CORS misconfigured** — If the calling agent is browser-based, your A2A server must return appropriate `Access-Control-Allow-Origin` headers. 5. **Wrong path** — The default A2A path is `/.well-known/agent.json` for the agent card. Ensure your server serves the agent card at that path and the `url` field inside it points to the correct task endpoint. ## MCP "no identity" error If MCP tools that require identity (sign, fetch, get identity) fail: - `SELF_AGENT_PRIVATE_KEY` is not set in your MCP config - Query tools (`self_lookup_agent`, `self_verify_agent`, `self_list_agents_for_human`) work without a key - Set the key in your MCP config `env` block to enable full mode ```json { "mcpServers": { "self-agent-id": { "command": "npx", "args": ["@selfxyz/mcp-server"], "env": { "SELF_AGENT_PRIVATE_KEY": "0x...", "SELF_NETWORK": "mainnet" } } } } ``` --- # Building an Agent Source: https://docs.self.xyz/docs/agent-id/guides/agent-builder/ This guide walks through registering an AI agent with Self Agent ID and making authenticated requests. By the end, your agent will have an on-chain identity backed by a real passport verification. ## 1. Choose Your SDK | Language | Package | Install | | ---------- | -------------------- | -------------------------------- | | TypeScript | `@selfxyz/agent-sdk` | `npm install @selfxyz/agent-sdk` | | Python | `selfxyz-agent-sdk` | `pip install selfxyz-agent-sdk` | | Rust | `self-agent-sdk` | `cargo add self-agent-sdk` | All three SDKs have identical functionality with language-idiomatic naming. ## 2. Register Your Agent Six registration modes — choose based on your use case: | Mode | Best for | Wallet needed? | | ---------------- | -------------------------------- | :------------: | | `wallet-free` | Embedded agents, IoT, CLI-only | No | | `ed25519` | OpenClaw, Eliza, IronClaw agents | No | | `linked` | Autonomous AI agents | Yes (human's) | | `ed25519-linked` | Ed25519 agents with human wallet | Yes (human's) | | `privy` | Social login (Google, Twitter) | No | | `smartwallet` | Consumer-facing, passkey UX | No | ### Via the SDK (simplest) Render the passport-scan QR in your own frontend and read the result from the chain. No hosted service is involved. This is the recommended path. See [Register an Agent with the SDK](/docs/agent-id/register-an-agent/) for the full runnable example (generate the agent key, sign the challenge, build the QR with `@selfxyz/qrcode`, mint on-chain). ### Via CLI ```bash # Install the CLI (comes with the SDK) npm install -g @selfxyz/agent-sdk # Initialize registration self-agent register init \ --mode linked \ --human-address 0xYourWallet \ --network mainnet \ --minimum-age 18 \ --ofac # Print the QR (fetched from the API) to scan with the Self app self-agent register open --session .self/session-*.json # Wait for verification to complete self-agent register wait --session .self/session-*.json # Export the agent private key self-agent register export --session .self/session-*.json --unsafe --print-private-key ``` The CLI talks to `https://agent-api.self.xyz` by default (override with `SELF_AGENT_API_BASE`). ### Via A2A Protocol (for agents) Agents can self-register by sending a JSON-RPC request to the A2A endpoint: ```bash curl -X POST https://agent-api.self.xyz/api/a2a \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "message/send", "params": { "message": { "role": "user", "parts": [{ "type": "data", "data": { "intent": "register" } }] } } }' ``` The endpoint returns a QR code and deep link. A human scans the QR with the Self app to complete verification. Send `{ "intent": "help" }` to see all available modes and a decision guide. ### Via REST API ```bash curl -X POST https://agent-api.self.xyz/api/agent/register \ -H "Content-Type: application/json" \ -d '{ "mode": "linked", "network": "mainnet", "humanAddress": "0xYourWallet", "disclosures": { "minimumAge": 18, "ofac": true } }' ``` Poll `/api/agent/register/status?token=` until `stage: "completed"`. ### Via Smart Wallet (passkeys) Smart-wallet mode uses a passkey to create a ZeroDev Kernel smart account as the guardian, with gasless operations via the Pimlico paymaster on mainnet. Build the passkey step into your own frontend with `@zerodev/sdk` and `@zerodev/passkey-validator`; the agent keypair and challenge are generated the same way as `linked`. The gasless bundler and paymaster are proxied through `https://agent-api.self.xyz/api/aa/*`. See [Registration Modes](/docs/agent-id/registration-modes/#smart-wallet). ## 3. Sign Outbound Requests Every SDK provides `agent.fetch()` which automatically signs requests with three headers: ::::tabs :::tab{label="TypeScript"} ```typescript import { SelfAgent } from "@selfxyz/agent-sdk"; const agent = new SelfAgent({ privateKey: process.env.AGENT_PRIVATE_KEY!, network: "mainnet", }); const res = await agent.fetch("https://api.example.com/data", { method: "POST", body: JSON.stringify({ query: "test" }), }); ``` ::: :::tab{label="Python"} ```python from self_agent_sdk import SelfAgent import os agent = SelfAgent( private_key=os.environ["AGENT_PRIVATE_KEY"], network="mainnet", ) res = agent.fetch("https://api.example.com/data", method="POST", body='{"query": "test"}') ``` ::: :::tab{label="Rust"} ```rust use self_agent_sdk::{SelfAgent, SelfAgentConfig, NetworkName}; let agent = SelfAgent::new(SelfAgentConfig { private_key: std::env::var("AGENT_PRIVATE_KEY").unwrap(), network: Some(NetworkName::Mainnet), registry_address: None, rpc_url: None, }).unwrap(); let res = agent.fetch( "https://api.example.com/data", Some(reqwest::Method::POST), Some(r#"{"query":"test"}"#.to_string()), ).await.unwrap(); ``` ::: :::: The signed headers: | Header | Value | | ------------------------ | -------------------------------------------------------------------- | | `x-self-agent-address` | Agent's Ethereum address | | `x-self-agent-signature` | ECDSA signature of `keccak256(timestamp + METHOD + path + bodyHash)` | | `x-self-agent-timestamp` | Unix timestamp (ms) | ## 4. Check Registration Status ```typescript const registered = await agent.isRegistered(); const info = await agent.getInfo(); // { agentId, isVerified, proofProvider, verificationStrength, ... } ``` ## 5. Read Credentials ```typescript const creds = await agent.getCredentials(); // { nationality, olderThan, ofac, dateOfBirth, gender, issuingState, ... } ``` Credentials are ZK-attested — extracted from passport data without revealing the full document. ## 6. Set an Agent Card (A2A) Agent cards enable discovery in agent-to-agent protocols: ```typescript await agent.setAgentCard({ name: "My Agent", description: "Analyzes market data", url: "https://myagent.example.com", skills: [{ name: "market-analysis", description: "Analyzes crypto markets" }], }); const card = await agent.getAgentCard(); ``` Cards are stored on-chain and readable by any agent or service. ## Gotchas :::warning **`userDefinedData` is UTF-8, not raw bytes.** The Self SDK passes `userDefinedData` as a UTF-8 string. Each byte position uses the ASCII character (`'0'` not `0x00`). This is the #1 integration mistake. ::: :::warning **Mainnet uses real passports. Testnet uses mock documents.** Registration defaults to Celo Mainnet (chain ID 42220) which requires a real passport scan via the Self app. Use `network: "testnet"` (chain ID 11142220) for testing — testnet also requires the Self app, but you can generate mock documents within the app instead of using a real passport. ::: :::note **Key management.** Store your agent private key securely. The CLI exports keys with `--unsafe` for a reason. Use environment variables or a secrets manager in production. ::: ## Next Steps - [Verify agent requests in your API](/docs/agent-id/guides/service-operator/) - [Gate smart contracts by agent ID](/docs/agent-id/guides/contract-developer/) - [SDK README (TypeScript)](https://github.com/selfxyz/self-agent-id/tree/main/typescript-sdk) - [SDK README (Python)](https://github.com/selfxyz/self-agent-id/tree/main/python-sdk) - [SDK README (Rust)](https://github.com/selfxyz/self-agent-id/tree/main/rust-sdk) --- # Verifying Agents (Service Operator) Source: https://docs.self.xyz/docs/agent-id/guides/service-operator/ This guide shows how to add Self Agent ID verification to your API. After setup, only registered, human-backed agents can access your protected endpoints. ## 1. Install the SDK ```bash npm install @selfxyz/agent-sdk # TypeScript pip install selfxyz-agent-sdk # Python cargo add self-agent-sdk # Rust ``` ## 2. Create a Verifier Use the builder pattern to configure verification policy: ::::tabs :::tab{label="TypeScript"} ```typescript import { SelfAgentVerifier } from "@selfxyz/agent-sdk"; const verifier = SelfAgentVerifier.create() .network("mainnet") .requireAge(18) .requireOFAC() .requireSelfProvider() // Ensure Self Protocol proofs (default: true) .sybilLimit(3) // Max 3 agents per human .rateLimit({ perMinute: 10 }) .build(); ``` ::: :::tab{label="Python"} ```python from self_agent_sdk import SelfAgentVerifier verifier = (SelfAgentVerifier.create() .network("mainnet") .require_age(18) .require_ofac() .require_self_provider() .sybil_limit(3) .rate_limit(per_minute=10) .build()) ``` ::: :::tab{label="Rust"} ```rust use self_agent_sdk::SelfAgentVerifier; let verifier = SelfAgentVerifier::create() .network("mainnet") .require_age(18) .require_ofac() .build(); ``` ::: :::: ### Builder Options | Method | Description | Default | | ------------------------------- | ------------------------------------ | ---------------- | | `.network(name)` | `"mainnet"` or `"testnet"` | `"testnet"` | | `.requireAge(n)` | Minimum age (18 or 21) | None | | `.requireOFAC()` | OFAC sanctions screening | Off | | `.requireNationality(...codes)` | Allowed ISO country codes | Any | | `.requireSelfProvider()` | Require Self Protocol proofs | `true` | | `.sybilLimit(n)` | Max agents per human (0 = unlimited) | `1` | | `.rateLimit(config)` | Per-agent rate limiting | None | | `.replayProtection(enabled?)` | Signature replay detection | `true` | | `.includeCredentials()` | Attach credentials to request | `false` | | `.maxAge(ms)` | Max signature age | `300000` (5 min) | | `.cacheTtl(ms)` | On-chain query cache | `60000` (1 min) | ## 3. Add Middleware ::::tabs :::tab{label="Express (TypeScript)"} ```typescript import express from "express"; const app = express(); app.use(express.json()); // Protect all /api routes app.use("/api", verifier.auth()); app.post("/api/data", (req, res) => { // req.agent is populated after verification console.log("Agent:", req.agent.address); console.log("Agent ID:", req.agent.agentId); console.log("Credentials:", req.agent.credentials); res.json({ ok: true }); }); ``` ::: :::tab{label="Flask (Python)"} ```python from flask import Flask, g, jsonify from self_agent_sdk.middleware.flask import require_agent app = Flask(__name__) @app.route("/api/data", methods=["POST"]) @require_agent(verifier) def handle(): print("Agent:", g.agent.agent_address) return jsonify(ok=True) ``` ::: :::tab{label="FastAPI (Python)"} ```python from fastapi import FastAPI, Depends from self_agent_sdk.middleware.fastapi import AgentAuth app = FastAPI() agent_auth = AgentAuth(verifier) @app.post("/api/data") async def handle(agent=Depends(agent_auth)): print("Agent:", agent.agent_address) return {"ok": True} ``` ::: :::tab{label="Axum (Rust)"} ```rust use axum::{Router, routing::post, middleware, Json, Extension}; use self_agent_sdk::{VerifiedAgent, self_agent_auth}; use std::sync::Arc; use tokio::sync::Mutex; let verifier = Arc::new(Mutex::new(verifier)); let app = Router::new() .route("/api/data", post(handle)) .layer(middleware::from_fn_with_state(verifier, self_agent_auth)); async fn handle(Extension(agent): Extension) -> Json { println!("Agent: {:?}", agent.address); Json(serde_json::json!({ "ok": true })) } ``` ::: :::: ## 4. Request Shape After Verification After successful verification, `req.agent` (or equivalent) contains: | Field | Type | Description | | --------------- | --------- | --------------------------------------------------- | | `address` | `string` | Agent's Ethereum address | | `agentId` | `number` | On-chain NFT token ID | | `agentKey` | `string` | Derived `bytes32` key | | `isVerified` | `boolean` | On-chain verification status | | `proofProvider` | `string` | Provider contract address | | `credentials` | `object` | ZK-attested credentials (if `includeCredentials()`) | ## 5. Credential-Based Access Control Use credentials to gate by age, OFAC status, or nationality: ```typescript const verifier = SelfAgentVerifier.create() .requireAge(21) // Must be 21+ .requireOFAC() // Must pass OFAC screening .requireNationality("US", "GB", "DE") // Only these countries .includeCredentials() // Attach credentials to request .build(); app.post("/api/restricted", verifier.auth(), (req, res) => { const { nationality, olderThan, ofac } = req.agent.credentials; // nationality: "US", olderThan: 21, ofac: [true, true, true] }); ``` ## 6. Sybil Resistance Control how many agents one human can use against your API: | Setting | Behavior | | ---------------- | ------------------------------------------------------- | | `.sybilLimit(1)` | Strict — one agent per human (default) | | `.sybilLimit(5)` | Moderate — up to 5 agents per human | | `.sybilLimit(0)` | Detection only — unlimited, but `sameHuman()` available | The verifier checks `getAgentCountForHuman(nullifier)` on-chain. ## 7. Provider Verification :::danger **Security note**: Always use `requireSelfProvider()` (enabled by default) to ensure agent proofs came from Self Protocol. Without this check, a malicious provider could approve agents without real passport verification. ::: The verifier checks that `getProofProvider(agentId)` matches Self Protocol's provider address. ## 8. Replay Protection + Rate Limiting **Replay protection** (enabled by default): - Caches `{signature + timestamp}` hashes (10,000 entries) - Same signature cannot be used twice **Rate limiting** (optional): ```typescript .rateLimit({ perMinute: 10, perHour: 100 }) ``` Per-agent sliding-window rate limits. ## 9. Request Body Fidelity (raw bytes) Signature verification is byte-sensitive: the signature covers a hash of the exact request body. Verify against the raw bytes your HTTP server received, not a re-serialized object. - Prefer raw-body capture (for example, a JSON parser `verify` hook that stashes `req.rawBody`). - Do not mutate, normalize, or re-stringify parsed JSON before verifying. - If you cannot access raw bytes, enforce a single canonical serialization end to end. ```typescript import express from "express"; const app = express(); app.use( express.json({ verify: (req: any, _res, buf) => { req.rawBody = typeof buf === "string" ? buf : buf.toString("utf8"); }, }), ); app.use("/api", verifier.auth()); ``` ## 10. Verification Drills Run these three drills against a registered agent to confirm an integration is wired correctly: | Drill | Action | Expected result | | ----------- | ------------------------------------------------------------------- | ------------------------------------------------------ | | **Tamper** | Sign body `A`, then send body `B` with the same signed headers | Invalid-signature rejection (`403`) | | **Expired** | Send a timestamp older than the configured `maxAge` (default 5 min) | Timestamp-freshness rejection (`403`) | | **Replay** | Submit an identical signed request twice | First accepted, second rejected (replay protection on) | A quick pre-deploy smoke check: one registered-agent success request plus at least two of the failure drills above, with the verifier health endpoint confirmed reachable. ## 11. Error Handling The middleware returns standard HTTP errors: | Status | Meaning | | ------ | -------------------------------------------------------- | | `401` | Missing or invalid signature headers | | `403` | Agent not verified, failed policy check, or rate limited | | `500` | On-chain query failed | Handle these in your client: ```typescript const res = await agent.fetch("https://api.example.com/data", { method: "POST" }); if (res.status === 403) { const error = await res.json(); // { error: "Agent not verified on-chain" } // { error: "Age requirement not met: requires 18, has 0" } // { error: "Rate limit exceeded" } } ``` ## Next Steps - [Build an agent that calls your API](/docs/agent-id/guides/agent-builder/) - [Gate smart contracts by agent ID](/docs/agent-id/guides/contract-developer/) - [Troubleshooting](/docs/agent-id/troubleshooting/) --- # Gating Smart Contracts Source: https://docs.self.xyz/docs/agent-id/guides/contract-developer/ This guide shows how to use the SelfAgentRegistry from Solidity to gate smart contract functions by proof-of-human agent identity. ## 1. Import the Registry Interface ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; interface ISelfAgentRegistry { // Agent verification function isVerifiedAgent(bytes32 agentKey) external view returns (bool); function getAgentId(bytes32 agentKey) external view returns (uint256); function getAgentCredentials(uint256 agentId) external view returns (AgentCredentials memory); // Proof-of-human (IERC8004ProofOfHuman) function hasHumanProof(uint256 agentId) external view returns (bool); function isProofFresh(uint256 agentId) external view returns (bool); function proofExpiresAt(uint256 agentId) external view returns (uint256); function getProofProvider(uint256 agentId) external view returns (address); // Sybil detection function sameHuman(uint256 agentIdA, uint256 agentIdB) external view returns (bool); function getAgentCountForHuman(uint256 nullifier) external view returns (uint256); function getHumanNullifier(uint256 agentId) external view returns (uint256); // ERC-8004 metadata function setAgentURI(uint256 agentId, string calldata newURI) external; function getMetadata(uint256 agentId, string memory key) external view returns (bytes memory); function setMetadata(uint256 agentId, string calldata key, bytes calldata value) external; struct AgentCredentials { string issuingState; string[] name; string idNumber; string nationality; string dateOfBirth; string gender; string expiryDate; uint256 olderThan; bool[3] ofac; } } ``` Or import from the contracts package: ```solidity import {ISelfAgentRegistryReader} from "self-agent-id/contracts/src/interfaces/ISelfAgentRegistryReader.sol"; ``` ## 2. `onlyVerifiedAgent` Modifier The most common pattern — gate a function to verified agents only: ```solidity contract MyContract { ISelfAgentRegistry public immutable registry; constructor(address _registry) { registry = ISelfAgentRegistry(_registry); } modifier onlyVerifiedAgent() { bytes32 agentKey = bytes32(uint256(uint160(msg.sender))); require(registry.isVerifiedAgent(agentKey), "Agent not human-verified"); _; } function protectedAction() external onlyVerifiedAgent { // Only callable by verified agents } } ``` The agent key derivation: `bytes32(uint256(uint160(address)))` — pads the 20-byte address to 32 bytes. ## 3. Query Credentials On-Chain ```solidity function gatedByAge(uint256 minAge) external onlyVerifiedAgent { bytes32 agentKey = bytes32(uint256(uint160(msg.sender))); uint256 agentId = registry.getAgentId(agentKey); ISelfAgentRegistry.AgentCredentials memory creds = registry.getAgentCredentials(agentId); require(creds.olderThan >= minAge, "Age requirement not met"); // creds.nationality, creds.ofac, creds.dateOfBirth, etc. } ``` ## 4. Check Proof Freshness Proofs expire. Always check freshness for security-critical operations: ```solidity function requireFreshProof(bytes32 agentKey) internal view { require(registry.isVerifiedAgent(agentKey), "Agent not verified"); uint256 agentId = registry.getAgentId(agentKey); require(registry.isProofFresh(agentId), "Human proof expired — re-authentication required"); } ``` :::warning `isVerifiedAgent()` checks that the agent has a registered proof, but it does NOT check expiry. For time-sensitive operations, always call `isProofFresh()` as well. ::: ## 5. Sybil Checks Detect or prevent multiple agents from the same human: ```solidity // Check if two agents are the same person bool same = registry.sameHuman(agentIdA, agentIdB); // Count how many agents this human has uint256 nullifier = registry.getHumanNullifier(agentId); uint256 count = registry.getAgentCountForHuman(nullifier); require(count <= maxAllowed, "Too many agents for this human"); ``` ## 6. EIP-712 Meta-Transaction Pattern For gasless agent verification, use a relayer that submits EIP-712 typed data on behalf of the agent: ```solidity // Agent signs off-chain: // domain: { name, version, chainId, verifyingContract } // types: { Verify: [agentKey, nonce, deadline] } // values: { agentKey, nonce, deadline } function metaVerifyAgent( bytes32 agentKey, uint256 nonce, uint256 deadline, bytes calldata signature ) external returns (uint256 agentId) { require(block.timestamp <= deadline, "Expired"); require(nonces[agentKey] == nonce, "Invalid nonce"); // Recover signer from EIP-712 signature bytes32 structHash = keccak256(abi.encode(VERIFY_TYPEHASH, agentKey, nonce, deadline)); bytes32 digest = _hashTypedDataV4(structHash); address signer = ECDSA.recover(digest, signature); // Verify signer matches agent key require(bytes32(uint256(uint160(signer))) == agentKey, "Signer mismatch"); require(registry.isVerifiedAgent(agentKey), "Not verified"); nonces[agentKey]++; agentId = registry.getAgentId(agentKey); } ``` See `AgentDemoVerifier.sol` in the repo for a complete implementation. ## 7. Reference Contracts The repo includes two demo contracts you can use as templates: ### AgentDemoVerifier EIP-712 meta-transaction verifier. Demonstrates gasless verification where a relayer submits on behalf of agents. - Source: [`contracts/src/AgentDemoVerifier.sol`](https://github.com/selfxyz/self-agent-id/blob/main/contracts/src/AgentDemoVerifier.sol) - Key function: `metaVerifyAgent(agentKey, nonce, deadline, signature)` ### AgentGate Access gate requiring age-verified agents. Demonstrates direct `msg.sender` verification with credential checks. - Source: [`contracts/src/AgentGate.sol`](https://github.com/selfxyz/self-agent-id/blob/main/contracts/src/AgentGate.sol) - Key functions: `checkAccess(agentKey)`, `gatedAction(agentKey)` ## 8. Contract Addresses ### Celo Mainnet (Chain ID: 42220) | Contract | Address | |----------|---------| | SelfAgentRegistry (proxy) | `0xaC3DF9ABf80d0F5c020C06B04Cced27763355944` | | SelfHumanProofProvider | `0x4b036aFD959B457A208F676cf44Ea3ef73Ea3E3d` | | AgentDemoVerifier | `0xD8ec054FD869A762bC977AC328385142303c7def` | | AgentGate | `0x26e05bF632fb5bACB665ab014240EAC1413dAE35` | | Hub V2 | `0xe57F4773bd9c9d8b6Cd70431117d353298B9f5BF` | ### Celo Sepolia Testnet (Chain ID: 11142220) | Contract | Address | |----------|---------| | SelfAgentRegistry (proxy) | `0x043DaCac8b0771DD5b444bCC88f2f8BBDBEdd379` | | SelfHumanProofProvider | `0x5E61c3051Bf4115F90AacEAE6212bc419f8aBB6c` | | AgentDemoVerifier | `0xc31BAe8f2d7FCd19f737876892f05d9bDB294241` | | AgentGate | `0x86Af07e30Aa42367cbcA7f2B1764Be346598bbc2` | | Hub V2 | `0x16ECBA51e18a4a7e61fdC417f0d47AFEeDfbed74` | ## 9. Foundry Setup :::warning Hub V2 uses the `PUSH0` opcode. You must use `--evm-version cancun` for all Foundry commands. ::: ```bash forge build --evm-version cancun forge test --evm-version cancun ``` Or add to `foundry.toml`: ```toml [profile.default] evm_version = "cancun" ``` ## Next Steps - [Build an agent to interact with your contract](/docs/agent-id/guides/agent-builder/) - [Verify agents in your API](/docs/agent-id/guides/service-operator/) - [Troubleshooting](/docs/agent-id/troubleshooting/) --- # Using the MCP Server Source: https://docs.self.xyz/docs/agent-id/guides/mcp-user/ The Self Agent ID [MCP server](https://github.com/selfxyz/self-agent-id-mcp) gives AI coding agents access to the on-chain identity registry through the [Model Context Protocol](https://modelcontextprotocol.io/). It works with Claude Code, Cursor, Windsurf, Codex, and any MCP-compatible client. ## What is MCP? The Model Context Protocol (MCP) is an open standard that lets AI assistants access external tools and data. The Self Agent ID MCP server exposes 10 tools for agent registration, verification, authentication, and discovery. ## Configuration Add the server to your MCP client config: ::::tabs :::tab{label="Claude Code"} Add to `~/.claude.json` (global) or `.mcp.json` (per-project): ```json { "mcpServers": { "self-agent-id": { "command": "npx", "args": ["@selfxyz/mcp-server"], "env": { "SELF_NETWORK": "mainnet" } } } } ``` ::: :::tab{label="Cursor / Windsurf"} Add to your MCP configuration file: ```json { "mcpServers": { "self-agent-id": { "command": "npx", "args": ["@selfxyz/mcp-server"], "env": { "SELF_NETWORK": "mainnet", "SELF_AGENT_PRIVATE_KEY": "0x..." } } } } ``` ::: :::: ### Environment Variables | Variable | Required | Default | Description | | ------------------------ | :------: | ---------------------------- | --------------------------------------------------------- | | `SELF_AGENT_PRIVATE_KEY` | No | — | Agent private key (hex). Enables identity and auth tools. | | `SELF_NETWORK` | No | `mainnet` | `mainnet` or `testnet` | | `SELF_RPC_URL` | No | Network default | Custom RPC endpoint | | `SELF_AGENT_API_BASE` | No | `https://agent-api.self.xyz` | API base URL. Override to use your own deployment. | :::note **Mainnet is the default.** Registration on mainnet requires a real passport via the Self app. Use `SELF_NETWORK=testnet` for development — testnet also requires the Self app, but you can generate mock documents within the app instead of using a real passport. ::: ## Query-Only Mode Without `SELF_AGENT_PRIVATE_KEY`, these tools work for looking up and verifying agents: | Tool | Description | | ---------------------------- | ----------------------------------- | | `self_lookup_agent` | Look up an agent by on-chain ID | | `self_verify_agent` | Verify an agent's on-chain identity | | `self_verify_request` | Verify incoming request headers | | `self_list_agents_for_human` | List agents for a human address | | `self_register_agent` | Start a registration flow | | `self_check_registration` | Poll registration status | ### Example prompts ``` Look up Self Agent ID #5 on mainnet. ``` ``` Verify if address 0x83fa4380903fecb801F4e123835664973001ff00 is a registered agent on mainnet. ``` ``` List all agents registered to 0xYourAddress on mainnet. ``` ## Full Mode With `SELF_AGENT_PRIVATE_KEY` set, additional tools become available: | Tool | Description | | -------------------------- | ----------------------------------------- | | `self_get_identity` | Get the current agent's on-chain identity | | `self_deregister_agent` | Revoke the agent's identity | | `self_sign_request` | Generate auth headers for an HTTP request | | `self_authenticated_fetch` | Make a signed HTTP request | ### Example prompts ``` Register me as a Self Agent on mainnet. I'll scan my passport to prove I'm human. ``` ``` Sign an HTTP POST request to https://api.example.com/data with body {"query": "test"}. ``` ``` Make an authenticated request to https://api.example.com/protected. ``` ## Claude Code Plugin (guided workflows) For Claude Code specifically, the repo also ships a **plugin** that adds six skills. Each skill is a self-contained knowledge module (decision trees, code examples, reference docs) that loads automatically when your request matches, giving Claude full protocol context without manual setup. ```bash # Clone the repo, then add the plugin from the local checkout git clone https://github.com/selfxyz/self-agent-id.git claude plugin add ./self-agent-id/plugin ``` | Skill | Covers | | ------------------------ | --------------------------------------------------------------------------------------------- | | `self-agent-id-overview` | Architecture, contracts, trust model, ERC-8004, provider system | | `register-agent` | Registration in every mode (linked, wallet-free, ed25519, ed25519-linked, privy, smartwallet) | | `sign-requests` | ECDSA request signing, the 3-header auth system, signed-fetch patterns | | `verify-agents` | On-chain verification, `SelfAgentVerifier` middleware, reputation, freshness, sybil detection | | `query-credentials` | ZK-attested credentials, A2A agent cards, reputation scores | | `integrate-self-id` | End-to-end integration: agent-side, service-side, on-chain gating, MCP setup | The plugin and the MCP server are complementary: the plugin teaches Claude the protocol, while the MCP server gives it live tools to act on-chain. ## Agents without MCP support Frameworks that cannot run an MCP server (LangChain, AutoGPT, custom runtimes) do not need it. The [REST API](/docs/agent-id/rest-api/) and [A2A JSON-RPC endpoint](/docs/agent-id/rest-api/#a2a-protocol-json-rpc) at `https://agent-api.self.xyz` expose registration, verification, lookup, and deregistration directly over HTTP, so any agent can drive the full lifecycle with plain requests. ## Resources The server exposes two MCP resources: | URI | Description | | ----------------- | ------------------------------------------------ | | `self://networks` | Contract addresses, chain IDs, and RPC URLs | | `self://identity` | Current agent's on-chain identity (requires key) | ## Integration Prompt The server includes a built-in prompt for generating verification middleware: ``` Use the self_integrate_verification prompt with Express to generate verification middleware. ``` This generates framework-specific code using `SelfAgentVerifier` for Express, FastAPI, Flask, or Axum. ## Example Workflows ### Register and verify 1. "Register me as a Self Agent on mainnet" 2. Scan passport when prompted 3. "Check my registration status" 4. "What's my agent identity?" ### Verify a peer 1. "Is agent 0x83fa... registered on mainnet?" 2. "What credentials does Agent ID #5 have?" ### Build a gated API 1. "Use the self_integrate_verification prompt to generate Express middleware" 2. Customize the generated code for your routes ## Next Steps - [MCP Server Repository](https://github.com/selfxyz/self-agent-id-mcp) - [Build an agent](/docs/agent-id/guides/agent-builder/) - [Verify agents in your API](/docs/agent-id/guides/service-operator/) - [Troubleshooting](/docs/agent-id/troubleshooting/) --- # Self Pass (Legacy) Source: https://docs.self.xyz/docs/self-pass/ Privacy-preserving identity verification using zero-knowledge proofs. :::warning[Self Pass is legacy] **Self Pass is now legacy. New integrations should use [Self Enterprise](/docs/self-enterprise/get-started/quickstart/).** Self Pass is the open-source SDK where you render your own QR and run the verifier yourself. Self Enterprise is the managed platform that handles both for you, with a dashboard, webhooks, and support. These docs remain for existing integrations; if you're starting fresh, follow the [Enterprise quickstart](/docs/self-enterprise/get-started/quickstart/). Already on Self Pass? See the [migration guide](/docs/self-enterprise/migration/from-self-pass-sdk/). ::: ## Overview Self Pass enables developers to verify real-world identity attributes without exposing personal data. Users scan their passport, national ID, Aadhaar card, or KYC attestation with the Self app, which generates a zero-knowledge proof on-device. Applications can then verify specific attributes (age, nationality, sanctions status) without ever seeing the underlying document. Which documents are supported, and what each can attest to, is documented once under Self Enterprise: see [Supported documents](/docs/self-enterprise/flows/supported-documents/) and [Supported countries](/docs/self-enterprise/reference/supported-countries/). For the attestation IDs you pass to the verifier, see [AttestationId](/docs/self-pass/backend/selfbackendverifier-api-reference/#attestationid). ## Integration Options | Path | Description | Guide | |------|-------------|-------| | **Frontend SDK** | Display QR codes to request proofs from users | [QRCode SDK](/docs/self-pass/frontend/qrcode-sdk/) | | **Backend SDK** | Verify proofs server-side on a Node.js backend | [Backend Integration](/docs/self-pass/backend/basic-integration/) | | **Smart Contracts** | Verify proofs on-chain in a trustless manner | [Contract Integration](/docs/self-pass/contracts/basic-integration/) | | **Mobile SDK** | Embed Self verification directly in a React Native app | [Mobile SDK](/docs/self-pass/mobile-sdk/getting-started/) | Start with [Key Concepts](/docs/self-pass/key-concepts/) to pick a verification path and environment. ## Resources * [Boilerplate repo](https://github.com/selfxyz/self-integration-boilerplate) — a working end-to-end integration to fork * [Soul Bound Token](https://github.com/selfxyz/self/blob/main/contracts/contracts/example/SelfPassportERC721.sol) — mint SBTs for verified users * [Cross Chain (LayerZero)](https://github.com/selfxyz/self-layerzero-example) — cross-chain verification * [Cross Chain (Hyperlane)](https://github.com/selfxyz/self-integration-boilerplate/tree/hyperlane-example) — cross-chain verification * [Workshop Video (ETHGlobal Buenos Aires)](https://www.loom.com/share/8a6d116a5f66415998a496f06fefdc23) * [Celo Testnet Faucet](https://faucet.celo.org/celo-sepolia) --- # Key Concepts (Legacy) Source: https://docs.self.xyz/docs/self-pass/key-concepts/ :::warning[Self Pass is legacy] **Self Pass is now legacy. New integrations should use [Self Enterprise](/docs/self-enterprise/get-started/quickstart/).** These docs remain for existing integrations. Already on Self Pass? See the [migration guide](/docs/self-enterprise/migration/from-self-pass-sdk/). ::: ## Choose Your Verification Path Every Self Pass integration has two parts: a **frontend** that displays a QR code (or deeplink) for users to scan with the Self app, and a **verification method** that checks the proof. You must choose one verification method: | | Smart Contract Verification | Backend Verification | |---|---|---| | **How it works** | Proof is verified on-chain by the [IdentityVerificationHub](/docs/self-pass/architecture/verification-hub/) | Proof is verified on your Node.js server using `SelfBackendVerifier` | | **Trust model** | Trustless — anyone can verify the result on-chain | Trust assumption — users trust your backend verifies correctly | | **Best for** | DeFi, airdrops, token gates, on-chain access control | Web apps, APIs, off-chain services, rapid iteration | | **Trade-offs** | Gas costs per verification; config changes require redeployment | No gas costs; easier to update; requires a running server | | **Guide** | [Smart Contract Integration](/docs/self-pass/contracts/basic-integration/) | [Backend Integration](/docs/self-pass/backend/basic-integration/) | | **Example** | [Boilerplate repo](https://github.com/selfxyz/self-integration-boilerplate) | [Backend branch](https://github.com/selfxyz/self-integration-boilerplate/tree/backend-verification) | :::note Both paths use the same frontend SDK (`@selfxyz/qrcode`) to display the QR code. The only difference is where verification happens. ::: ## Choose Your Environment | Environment | Documents | Network | `endpointType` | When to use | |---|---|---|---|---| | **Staging** | Mock passports | Celo Sepolia | `staging_celo` (contract) or `staging_https` (backend) | Development and testing | | **Production** | Real passports | Celo Mainnet | `celo` (contract) or `https` (backend) | Live applications | :::warning Mock passports only work with staging endpoints on Celo Sepolia. Real passports only work with production endpoints on Celo Mainnet. See [Using Mock Passports](/docs/self-enterprise/guides/using-mock-passports/) for setup instructions. ::: ## Configuration Values * **`scopeSeed`** — A short string (max 31 ASCII characters) that uniquely identifies your application, e.g. `"my-airdrop-app"`. You pass this into your smart contract constructor as `scopeSeed`, and it gets hashed together with the contract address (using Poseidon) to produce the final `scope` — a uint256 value used in proofs to ensure nullifiers are unique to your app and prevent proof replay. Note: the frontend `SelfAppBuilder` currently names this field `scope`, but you are passing in the `scopeSeed` value. * **`endpointType`** — Determines where the proof is sent and which network is used (see table above). * **`endpoint`** — The destination address. For contract verification, this is your deployed contract address. For backend verification, this is your API URL. ## Packages * `@selfxyz/qrcode`: QR code generation and display components — see [QRCode SDK](/docs/self-pass/frontend/qrcode-sdk/). * `@selfxyz/core`: Core utilities including `getUniversalLink` for deeplinks, and `SelfBackendVerifier` — see [Backend Integration](/docs/self-pass/backend/basic-integration/). * `ethers`: Ethereum utilities for address handling. ## Verification Flow
Overview diagram of the Self verification flow
The QR code component displays the current verification status with an LED indicator and changes its appearance based on the verification state: 1. **QR Code Display**: Component shows QR code for users to scan 2. **User Scans**: User scans with Self app and provides proof 3. **Verification**: 1. Onchain Verification: Your smart contract receives the proof and verifies it on the Self VerificationHub contract. 2. Backend Verification: Your API endpoint receives and verifies the proof 4. **Success Callback**: `onSuccess` callback is triggered when verification completes --- # Disclosures (Legacy) Source: https://docs.self.xyz/docs/self-pass/disclosures/ Disclosures control what information users reveal during identity verification. You configure them in the frontend `disclosures` object, which contains two types of settings: 1. **Verification Requirements** - conditions that must be met (must match backend) 2. **Disclosure Requests** - information users will reveal (frontend only) ## Verification Requirements These settings define verification conditions and must match your backend `verification_config`: ### `minimumAge` Verifies user is at least this age without revealing exact age or date of birth. ```javascript disclosures: { minimumAge: 18, // User must be 18 or older } ``` ### `excludedCountries` Blocks users from specific countries using [ISO 3-letter country codes](https://www.iso.org/obp/ui/#search). ```javascript disclosures: { excludedCountries: ['IRN', 'PRK'], // Block Iran and North Korea } ``` :::note The maximum amount of excluded countries you can specify is 40. Any more than this will cause proofs to fail verification on the Self App. ::: ### `ofac` Enables OFAC (sanctions) checking against official watchlists. ```javascript disclosures: { ofac: true, // Enable sanctions checking } ``` ## Disclosure Requests These settings specify what information users will reveal. Configure only in frontend - backend receives this data automatically. ### Personal Information * **`name`**: User's full name from the verified document * **`nationality`**: User's nationality * **`gender`**: User's gender (M/F) * **`date_of_birth`**: Full date of birth ### Document Information * **`passport_number`**: Document number (use carefully for privacy) * **`expiry_date`**: Document expiry date * **`issuing_state`**: Country that issued the document ## Example Configuration The disclosure configuration in `SelfAppBuilder` controls what verification rules are applied and what data users reveal: ```typescript const app = new SelfAppBuilder({ // ... other config disclosures: { // Verification rules (must match backend/contract config exactly) minimumAge: 18, excludedCountries: ["IRN", "PRK"], ofac: true, // Data disclosures (frontend only — what users reveal) nationality: true, gender: true, name: false, date_of_birth: false, passport_number: false, expiry_date: false, issuing_state: false, }, }).build(); ``` ## Verification Result When verification succeeds, disclosed information is available in `result.discloseOutput`: ```javascript const result = await selfBackendVerifier.verify(/*...*/); if (result.isValidDetails.isValid) { const data = result.discloseOutput; console.log(data.nationality); // "USA" (if requested) console.log(data.gender); // "M" or "F" (if requested) console.log(data.olderThan); // "18" (if minimumAge set) console.log(data.name); // undefined (if not requested) } ``` ## Frontend & Backend Alignment :::warning **Critical**: The verification rules in your frontend `disclosures` object **must exactly match** the configuration in your backend `SelfBackendVerifier` or smart contract. A mismatch — even a stricter value — will cause verification to fail. ::: For example, if the backend requires `minimumAge: 18` but the frontend specifies `minimumAge: 21`, verification will fail. The values must be identical, not just compatible. :::tip **Best practice**: Centralize your disclosure config in a shared constants file and import it into both frontend and backend to avoid drift. ::: :::note Data disclosures (`nationality`, `gender`, `name`, etc.) are **not** enforced by your backend or contracts — they only control what data the user reveals. Only verification rules (`minimumAge`, `excludedCountries`, `ofac`) need to match. ::: ## Privacy Best Practices * **Request only what you need**: Each disclosure reveals personal information * **Avoid sensitive fields**: Be cautious with `passport_number` and `name` * **Consider alternatives**: Use `minimumAge` instead of `date_of_birth` for age verification * **Store carefully**: Implement proper data protection for disclosed information ## Common Use Cases **Age verification only:** ```javascript disclosures: { minimumAge: 18, // No personal data revealed } ``` **Basic identity with nationality:** ```javascript disclosures: { minimumAge: 18, nationality: true, gender: true, } ``` **Complete identity verification:** ```javascript disclosures: { minimumAge: 21, ofac: true, nationality: true, name: true, date_of_birth: true, } ``` --- # Use deeplinking (Legacy) Source: https://docs.self.xyz/docs/self-pass/use-deeplinking/ Deep links allow users to open the Self mobile app directly instead of scanning QR codes. This provides a better mobile experience by eliminating the need to scan codes on the same device. ## When to Use Deep Links * **Mobile web applications**: Users can tap a button to open Self app * **Same-device verification**: Avoid QR code scanning on mobile * **Messaging platforms**: Share verification links via SMS, email, or chat * **Native mobile apps**: Direct integration with mobile applications ## Basic Usage Generate a deep link from your SelfApp configuration: ```javascript import { getUniversalLink } from '@selfxyz/core'; import { SelfAppBuilder } from '@selfxyz/qrcode'; // Create your SelfApp (same as for QR codes) const selfApp = new SelfAppBuilder({ // ... your configuration }).build(); // Generate the deep link const deeplink = getUniversalLink(selfApp); // Use the deep link window.open(deeplink, '_blank'); // Opens Self app ``` ## Returning User to App Automatically After the user's proof is verified, the Self app can automatically redirect them back to your app. Add the `deeplinkCallback` parameter with your app's deep link: ```javascript const selfApp = new SelfAppBuilder({ // ... your configuration deeplinkCallback: 'your-app-deeplink-url', }).build(); ``` The Self app will show: **"Redirecting to \[your-callback-url] in 5...4...3...2...1"** and automatically open your callback URL at the end of the countdown. ## Implementation Examples ### Simple Button ```javascript function OpenSelfButton() { const handleOpenSelf = () => { const deeplink = getUniversalLink(selfApp); window.open(deeplink, '_blank'); }; return ( ); } ``` ### Mobile-First Experience ```javascript function VerificationOptions() { const [deeplink, setDeeplink] = useState(''); useEffect(() => { if (selfApp) { setDeeplink(getUniversalLink(selfApp)); } }, [selfApp]); return (
{/* Show deep link button on mobile */}
{/* Show QR code on desktop */}
); } ``` ## Platform Considerations ### Mobile Browsers * **iOS Safari**: Deep links work reliably * **Android Chrome**: Deep links work reliably * **In-app browsers**: May have limitations ### Desktop Browsers * Deep links will attempt to open the mobile app * If app not installed, may show app store page * Generally better to show QR codes on desktop ## Best Practices * **Provide both options**: Offer both QR codes and deep links * **Mobile-first**: Prioritize deep links on mobile devices * **Clear labeling**: Make it obvious what the button does * **Testing**: Test on various mobile browsers and devices --- # QRCode SDK (Legacy) Source: https://docs.self.xyz/docs/self-pass/frontend/qrcode-sdk/ > **Scope**: How to install, configure, and embed the component. [qrcode-sdk-api-reference.md](/docs/self-pass/frontend/qrcode-sdk-api-reference/) (all props, types) lives on a separate page. ## Installation ::::tabs :::tab{label="npm"} ```bash npm i @selfxyz/qrcode ``` ::: :::tab{label="yarn"} ```bash yarn add @selfxyz/qrcode ``` ::: :::tab{label="pnpm"} ```bash pnpm i @selfxyz/qrcode ``` ::: :::: ## Usage (Desktop) The `SelfQRCode` component is responsible for storing information about your app. This includes data about mock/real document verification mode, the userId you want to store, and the config that should be checked for a given proof. This involves 2 steps: 1. Create a `SelfApp` from the `SelfAppBuilder` that takes in `Partial`. 2. Pass the `SelfApp` to the QRCode component along with `onSuccess` and `onError` functions. ```tsx import { useEffect, useState } from 'react' import { countries, SelfQRcodeWrapper } from '@selfxyz/qrcode' import { SelfAppBuilder } from '@selfxyz/qrcode' export default function Verify() { const [selfApp, setSelfApp] = useState(null) useEffect(() => { const userId = '0xYourUserEthAddress' // or a UUID depending on your setup const app = new SelfAppBuilder({ version: 2, appName: process.env.NEXT_PUBLIC_SELF_APP_NAME || 'Self Docs', scope: process.env.NEXT_PUBLIC_SELF_SCOPE || 'my-app', endpoint: `${process.env.NEXT_PUBLIC_SELF_ENDPOINT}`, logoBase64: 'https://i.postimg.cc/mrmVf9hm/self.png', userId, endpointType: 'staging_celo', userIdType: 'hex', // 'hex' for EVM address or 'uuid' for uuidv4 userDefinedData: 'Hello from the Docs!!', disclosures: { // What you want to verify from the user's identity minimumAge: 18, excludedCountries: [countries.CUBA, countries.IRAN, countries.NORTH_KOREA, countries.RUSSIA], // What you want users to nationality: true, gender: true, }, }).build() setSelfApp(app) }, []) const handleSuccessfulVerification = () => { // Persist the attestation / session result to your backend, then gate content console.log('Verified!') } return (
{selfApp ? ( { console.error('Error: Failed to verify identity') }} /> ) : (

Loading QR Code...

)}
) } ``` :::warning The claims you want to verify MUST exactly match the ones you set in your backend configuration. ::: :::danger If you're using a contract to verify proofs, ensure `endpoint` points to the correct deployed contract address for the selected `endpointType` network. ::: ## Usage (Mobile) If you're developing an app then it's not easy to scan a QR Code. Instead, what you would do is to create a deeplink to the Self app and pass in a `deeplinkCallback` url that the Self app can navigate to once the proof is verified. Here's what you'll need to add: ```tsx import { useEffect, useState } from 'react' import { countries, SelfQRcodeWrapper } from '@selfxyz/qrcode' import { SelfAppBuilder } from '@selfxyz/qrcode' import { getUniversalLink } from '@selfxyz/core' export default function Verify() { const [selfApp, setSelfApp] = useState(null); const [universalLink, setUniversalLink] = useState(""); useEffect(() => { const userId = '0xYourUserEthAddress' // or a UUID depending on your setup const app = new SelfAppBuilder({ ..., //same as previous example deeplinkCallback: "https://your-callback-url.com", }).build() setSelfApp(app); setUniversalLink(getUniversalLink(app)); }, []); const openSelfApp = () => { if (!universalLink) return; window.open(universalLink, "_blank"); } return (
{selfApp ? ( ) : (

Loading QR Code...

)}
) } ``` --- # QRCode SDK - API Reference (Legacy) Source: https://docs.self.xyz/docs/self-pass/frontend/qrcode-sdk-api-reference/ :::note **On Self Enterprise?** You don't use this SDK or render your own QR, see the [Enterprise quickstart](/docs/self-enterprise/get-started/quickstart/). ::: ## Exports - SelfQRCodeWrapper - SelfQRCode - SelfAppBuilder - SelfApp - getUniversalLink ## SelfQRCodeWrapper / SelfQRCode | Input | Type | Default | Description | | -------------- | ---------------------------------------------------------------------- | :------: | -------------------------------------------------------------------------- | | selfApp | [#selfapp](/docs/self-pass/frontend/qrcode-sdk-api-reference/#selfapp) | - | The configured Self app instance. | | onSuccess | () ⇒ void; | - | Callback triggered when verification succeeds. | | onError | data: { error_code?: string; reason?: string }) => void | - | Callback triggered when verification fails. | | type | 'websocket' \| 'deeplink' | websocket | Determines whether to use WebSocket or deep link QR. | | websocketUrl | string | WS_DB_RELAYER | Custom WebSocket relayer URL. | | size | number | 300 | Width and height of the QR Code in pixels. | | darkMode | boolean | false | Toggles light/dark mode for QR code styling. | | showBorder | boolean | true | Show a colored border around the QR code that reflects the current status. | | showStatusText | boolean | true | Show the status banner below the QR code. | ## SelfApp | Property | Type | Required | Description | | ---------------- | ------------------------------------------------------------------------------------------------------ | :------: | ---------------------------------------------------------------------------------------------------------------------------- | | appName | string | | The name of your app | | logoBase64 | string | | Image URL or base64 encoded image | | endpointType | `https` \| `staging_https` \| `celo` \| `staging_celo` | | Required by the Self Protocol to know where the proofs will be verified: Onchain/Offchain and Real Documents/Mock Documents. | | endpoint | string | | Either the EVM Address or the backend URL where the proof must be verified. | | deeplinkCallback | string | | Triggered by the app after proof the proof is verified (or if it fails being generated) | | scope | string | | A unique identifier for you application | | userId | string | | An identifier for the end user. | | userIdType | 'uuid' \| 'hex' | | Type of the user identifier | | disclosures | [#selfappdisclosureconfig](/docs/self-pass/frontend/qrcode-sdk-api-reference/#selfappdisclosureconfig) | | Object containing all disclosures and checks. | | version | 1 \| 2 | | Whether to use Self V1 or SelfV2. Defaults to 2 | | userDefinedData | string | | Any data you want to pass to your endpoint. | ## SelfAppDisclosureConfig | Property | Type | Default | Description | | --------------------- | --------------------- | :------: | --------------------------------------------------------- | | issuing_state | boolean | false | Request the issuing state from the document | | name | boolean | false | Request the full name from the document | | passport_number | boolean | false | Request the document number. | | nationality | boolean | false | Request the user’s nationality. | | date_of_birth | boolean | false | Request the date of birth. | | gender | boolean | false | Request the gender field. | | expiry_date | boolean | false | Request the document expiry date. | | ofac\*\* | boolean | false | Check against OFAC sanction lists. | | excludedCountries\*\* | Country3LetterCode\[] | \[] | Exclude users from specific ISO 3166-1 alpha-3 countries. | | minimumAge\*\* | number | 0 | Require a minimum age (e.g., `18` (upto `99`)).\| | --- # Basic Integration (Legacy) Source: https://docs.self.xyz/docs/self-pass/backend/basic-integration/ The `@selfxyz/core` library provides the essential building blocks to integrate Self’s verification flows into your backend. It contains utilities, types, and helpers for working with scopes, configs, and verification data. Within this library, the `SelfBackendVerifier` class is the main tool you’ll use on the server. It helps you verify that the proofs produced by the frontend/mobile flow are valid against the on‑chain hub and your configured rules. :::note Install `@selfxyz/core@latest` (currently 1.2.0-beta.1). It must be at least 1.1.0-beta.1 — versions prior to this use Celo Alfajores for mock passports and will not verify correctly. ::: ## Creating a verifier instance You typically instantiate a `SelfBackendVerifier` once with your chosen **scope**, **endpoint**, and other settings. ```typescript import { SelfBackendVerifier, DefaultConfigStore, AllIds } from '@selfxyz/core' const selfBackendVerifier = new SelfBackendVerifier( 'docs', // scope string 'https://docs.self.xyz/api/verify', // endpoint (your backend verification API) true, // mockPassport → true = testnet, realPassport → false = mainnet AllIds, // allowed attestation IDs map new DefaultConfigStore({ // config store (see separate docs) minimumAge: 18, excludedCountries: ['USA'], ofac: false, }), 'hex' // user identifier type ); ``` ### Parameters * **scope**: identifier for your application. Must match the frontend scope. * **endpoint**: the URL where your proofs will be verified (including the route). Must match the frontend endpoint. * **mockPassport**: toggle to use testnet/staging vs mainnet hub. * **allowedIds**: map of attestation IDs you want to accept. * **configStorage**: implementation of `IConfigStorage` (for now, use `DefaultConfigStore` or `InMemoryConfigStore` see [configstore.md](/docs/self-pass/backend/configstore/)). * **userIdentifierType**: either `'uuid'` or `'hex'` (depending on how your frontend built the user identifier). ## Verifying a proof Call `.verify()` with the attestation ID, proof, public signals, and user context data. These will be passed to your endpoint from Self's relayers. The verifier will check validity against on‑chain contracts and your config store. ```typescript const result = await selfBackendVerifier.verify( attestationId, // e.g. AttestationId.PASSPORT / BIOMETRIC_ID_CARD / AADHAAR / KYC proof, // zkSNARK proof object pubSignals, // array of public signals from prover userContextData //user context data ); console.log(result) ``` ### Example output ```json { "attestationId": 1, "isValidDetails": { "isValid": true, "isMinimumAgeValid": true, "isOfacValid": false }, "forbiddenCountriesList": ["USA"], "discloseOutput": { "minimumAge": "18", "nationality": "IND", "gender": "M" ... }, "userData": { "userIdentifier": "8e4e6f24-...", "userDefinedData": "..." //what you pass in the qrcode } } ``` ## Using in an API endpoint You’ll usually expose this via a backend route that your Self's relayers call after creating your proof. :::warning Since your API must be reachable to Self's relayers, we recommend you use `ngrok` to tunnel requests to your local API endpoint during development and then set this API in your frontend and backend. ::: ```typescript import express from "express" import bodyParser from "body-parser" import { SelfBackendVerifier } from "@selfxyz/core" import { AllIds } from "./utils/constants.js" import { DefaultConfigStore } from "./store/DefaultConfigStore.js" const app = express() app.use(bodyParser.json()) const selfBackendVerifier = new SelfBackendVerifier( process.env.SELF_SCOPE_SEED, process.env.SELF_ENDPOINT, true, AllIds, new DefaultConfigStore({ minimumAge: 18, excludedCountries: ["USA"], ofac: false, }), "hex" ) app.post("/api/verify", async (req, res) => { try { const { attestationId, proof, publicSignals, userContextData } = req.body if (!proof || !publicSignals || !attestationId || !userContextData) { return res.status(200).json({ status: "error", result: false, reason: "Proof, publicSignals, attestationId and userContextData are required", }) } const result = await selfBackendVerifier.verify( attestationId, proof, publicSignals, userContextData ); const { isValid, isMinimumAgeValid } = result.isValidDetails; if (!isValid || !isMinimumAgeValid) { let reason = "Verification failed" if (!isMinimumAgeValid) reason = "Minimum age verification failed" return res.status(200).json({ status: "error", result: false, reason, }) } return res.status(200).json({ status: "success", result: true, }) } catch (error) { return res.status(200).json({ status: "error", result: false, reason: error instanceof Error ? error.message : "Unknown error", }); } }) app.listen(3000, () => { console.log("Server listening on http://localhost:3000"); }) ``` :::note Notice however that the `status` of the http request is 200 regardless of whether the inputs were correct / wrong and if the proof was verified or not. In general, you will need to follow the format below when creating an API. ::: ## Endpoint API Reference `POST` `/api/verify` Verifies a Self Identity proof. **Headers** | Name | Value | | ------------ | ------------------ | | Content-Type | `application/json` | **Body**
NameTypeDescription
attestationIdnumberThe id of the document being verified.
proof
{
    a: [string, string],
    b: [[string, string],
        [string, string]],
    c: [string, string]
}
The self identity proof.
publicSignalsstring[]The public signals for the proof
userContextDatastringContains information about the user id and app defined user data.
**Response** ::::tabs :::tab{label="200 (success)"} ```json { status: "success", result: true, } ``` ::: :::tab{label="200 (error)"} ```json { status: "error", result: false, reason: "Not enough inputs", } ``` ::: :::: ## Accepting only certain type of documents If you want to only accept certain types of documents (for example if you don't want to use Aadhaar or KYC) then you can create a map with only the attestation IDs you allow. ```typescript import { ATTESTATION_ID, AttestationId } from "@selfxyz/core" const allowedIds = new Map([ [ATTESTATION_ID.PASSPORT, true], [ATTESTATION_ID.BIOMETRIC_ID_CARD, true], // [ATTESTATION_ID.AADHAAR, true], // [ATTESTATION_ID.KYC, true], // KYC ]); ``` --- # ConfigStore (Legacy) Source: https://docs.self.xyz/docs/self-pass/backend/configstore/ The **config storage layer** defines how verification configurations are persisted and retrieved on the backend. These configurations represent the same disclosure/verification rules that the frontend and smart contracts enforce. Keeping them consistent is critical. This `@selfxyz/core` library exposes an interface (`IConfigStorage`) and two default implementations (`DefaultConfigStore`, `InMemoryConfigStore`). ## `IConfigStorage` ```typescript export interface IConfigStorage { getConfig(id: string): Promise setConfig(id: string, config: VerificationConfig): Promise getActionId(userIdentifier: string, data: string): Promise } ``` #### Methods * **`getConfig(id)`** * Returns the `VerificationConfig` associated with a given id. * This id is typically the `configId` referenced by your contract/backend/frontend. * **`setConfig(id, config)`** * Stores a new verification config under the given id. * Returns `true` if an existing config was replaced, `false` if it was newly set. * **`getActionId(userIdentifier, data)`** * Computes or retrieves an action id based on user context data from the frontend. * This action id links user activity with a registered config and is later used by `getConfig(id)` ## DefaultConfigStore A simple implementation that always returns the same config regardless of id. ```typescript export class DefaultConfigStore implements IConfigStorage { constructor(private config: VerificationConfig) {} //other methods } ``` ### Example ```typescript const defaultConfigStore = new DefaultConfigStore({ minimumAge: 18, excludedCountries: ['US', 'CA'], ofac: true, }); ``` ## InMemoryConfigStore A more flexible implementation that can hold multiple configs in memory. ```typescript export class InMemoryConfigStore implements IConfigStorage { private configs: Map = new Map(); private getActionIdFunc: IConfigStorage['getActionId']; constructor(getActionIdFunc: IConfigStorage['getActionId']) { this.getActionIdFunc = getActionIdFunc; } //other methods } ``` ### Example ```typescript const inMemoryHandler = async (userIdentifier: string, userDefinedData: string) => { return userDefinedData === 'high_value' ? 'strict' : 'standard'; }; const inMemory = new InMemoryConfigStore(inMemoryHandler); inMemory.setConfig('strict', { minimumAge: 18, excludedCountries: ['USA', 'CAN'], ofac: true, }); inMemory.setConfig('standard', { minimumAge: 18, excludedCountries: ['USA', 'CAN'], ofac: true, }); ``` **Use cases:** * Backends that need to manage multiple verification configurations at once. * Scenarios where action ids must be computed dynamically per user. * Good for prototyping before connecting to a persistent store (DB, KV, etc.). ## Custom Implementations Here's an example of a `KVConfigStore` that is used in the [playground](https://plaground.self.xyz/). ```typescript import { IConfigStorage, VerificationConfig, } from "@selfxyz/core"; import { Redis } from "@upstash/redis"; export class KVConfigStore implements IConfigStorage { private redis: Redis; constructor(url: string, token: string) { this.redis = new Redis({ url: url, token: token, }); } async getActionId(userIdentifier: string, data: string): Promise { return userIdentifier; } async setConfig(id: string, config: VerificationConfig): Promise { await this.redis.set(id, JSON.stringify(config)); return true; } async getConfig(id: string): Promise { const config = (await this.redis.get(id)) as VerificationConfig; return config; } } ``` ### Integration with Frontend The `userDefinedData` parameter in the frontend's `SelfAppBuilder` is passed to your `getActionId` method: ```javascript // Frontend const selfApp = new SelfAppBuilder({ // ... other config version: 2, userDefinedData: Buffer.from(JSON.stringify({ action: "high_value_transaction", amount: 50000, merchant: "merchant_123" })).toString('hex'), // ... }).build(); // Backend - getActionId receives this data async getActionId(userIdentifier: string, userDefinedData: string): Promise { const data = JSON.parse(Buffer.from(userDefinedData, 'hex').toString()); if (data.action === 'high_value_transaction' && data.amount > 10000) { return 'strict_verification'; } return 'standard_verification'; } ``` ### Summary * `IConfigStorage` defines a contract for working with verification configs. * The library ships with `DefaultConfigStore` (static) and `InMemoryConfigStore` (multi‑config, dynamic). * Backend verifiers (like `SelfBackendVerifier`) depend on `IConfigStorage` to fetch configs and action ids. * You can plug in your own storage backend by implementing the same interface. --- # SelfBackendVerifier - API Reference (Legacy) Source: https://docs.self.xyz/docs/self-pass/backend/selfbackendverifier-api-reference/ :::note **On Self Enterprise?** You don't run this verifier, Self verifies on the hot path and sends you a signed webhook. See the [Enterprise quickstart](/docs/self-enterprise/get-started/quickstart/). ::: A backend verification class that validates zero-knowledge proofs generated by the Self mobile app. ## Constructor ```typescript new SelfBackendVerifier( scope: string, endpoint: string, mockPassport: boolean = false, allowedIds: Map, configStorage: IConfigStorage, userIdentifierType: UserIdType ) ``` ### Parameters | Parameter | Type | Description | | ------------------ | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | scope | `string` | Your application's unique identifier. Must match the scope used in SelfAppBuilder. Max 30 characters. | | endpoint | `string` | Your backend verification endpoint URL. Must be publicly accessible and match your frontend configuration. | | mockPassport | `boolean` | `false` for real documents (mainnet), `true` for testing with mock documents (testnet). Default: `false` | | allowedIds | `Map` | Map of allowed document types. Key: attestation ID, Value: allowed status | | configStorage | `IConfigStorage` | Configuration storage implementation that determines verification requirements. | | userIdentifierType | `UserIdType` |

Type of user identifier: 'uuid' or 'hex' (for blockchain addresses)


| ## Methods Validates zero-knowledge proofs from the Self mobile app. ```typescript async verify( attestationId: AttestationId, proof: VcAndDiscloseProof, pubSignals: BigNumberish[], userContextData: string ): Promise ``` ### Parameters | Parameter | Type | Description | | --------------- | -------------------- | ------------------------------------------------------------------------------- | | attestationId | `AttestationId` | Document type identifier (1 = electronic passport, 2 = EU ID card, 3 = Aadhaar, 4 = KYC) | | proof | `VcAndDiscloseProof` | Zero-knowledge proof object containing cryptographic proof arrays. | | pubSignals | `BigNumberish[]` | Public signals from the zero-knowledge proof | | userContextData | `string` | Hex-encoded string containing user context and configuration data | ## **Return Value** The method returns a `VerificationResult` object with comprehensive verification details: ```typescript { attestationId: AttestationId; // Document type that was verified isValidDetails: { isValid: boolean; // Overall cryptographic proof validity isMinimumAgeValid: boolean; // Age requirement validation, false if minimum age check does not pass isOfacValid: boolean; // OFAC sanctions check result, true if in OFAC }; forbiddenCountriesList: string[]; // Countries excluded from the proof discloseOutput: { // Disclosed document information nullifier: string; // Unique proof identifier (prevents reuse) forbiddenCountriesListPacked: string[]; issuingState: string; // Country that issued the document name: string; // Full name (if disclosed) idNumber: string; // Document number nationality: string; // Nationality dateOfBirth: string; // Date of birth (if disclosed) gender: string; // Gender expiryDate: string; // Document expiry date olderThan: string; // Age verification result ofac: boolean[]; // OFAC check results [passportNo, nameAndDob, nameAndYob] }; userData: { userIdentifier: string; // User identifier from context userDefinedData: string; // Custom user data }; } ``` The method throws `ConfigMismatchError` when verification requirements don't match: ```typescript try { const result = await verifier.verify(attestationId, proof, pubSignals, userContextData); // Handle successful verification } catch (error: any) { if (error.name === 'ConfigMismatchError') { console.error('Configuration mismatches:', error.issues); // error.issues contains detailed information about what failed } else { console.error('Verification error:', error); } } ``` Common ConfigMismatch Types: * `InvalidId` - Attestation ID not in allowedIds * `InvalidScope` - Proof was generated for a different application * `InvalidRoot` - Merkle root not found on blockchain * `InvalidForbiddenCountriesList` - Countries don't match configuration * `InvalidMinimumAge` - Age requirement mismatch * `InvalidTimestamp` - Proof timestamp out of valid range (±1 day) * `ConfigNotFound` - Configuration not found in storage ## Types ### VerificationConfig ```typescript { minimumAge?: number; // Minimum age requirement excludedCountries?: Country3LetterCode[]; // ISO 3-letter country codes to exclude ofac?: boolean; // Enable OFAC sanctions checking } ``` ### VcAndDiscloseProof ```typescript { a: [BigNumberish, BigNumberish]; b: [[BigNumberish, BigNumberish], [BigNumberish, BigNumberish]]; c: [BigNumberish, BigNumberish]; } ``` ### AttestationId Document type identifiers: * `1` - Electronic passport * `2` - Biometric ID card * `3` - Aadhaar * `4` - KYC (exported in `@selfxyz/core` as `KYC_ATTESTATION_ID`) --- # Basic Integration (Legacy) Source: https://docs.self.xyz/docs/self-pass/contracts/basic-integration/ :::danger **Troubleshooting Celo Sepolia**: If you encounter a `Chain 11142220 not supported` error when deploying to Celo Sepolia, try to update Foundry to version 0.3.0: ```bash foundryup --install 0.3.0 ``` ::: ## Overview The `@selfxyz/contracts` SDK provides you with a `SelfVerificationRoot` abstract contract that wires your contract to the Identity Verification Hub V2. Your contract receives a callback with disclosed, verified attributes only after the proof succeeds. ### Key flow 1. Your contract exposes `verifySelfProof(bytes proofPayload, bytes userContextData)` from the abstract contract. 2. It takes a verification config from your contract and forwards a packed input to Hub V2. 3. If the proof is valid, the Hub calls back your contract’s `onVerificationSuccess(bytes output, bytes userData)` . 4. You implement custom logic in `customVerificationHook(...)`. ## SelfVerificationRoot This is an abstract contract that you must override by providing custom logic for returning a config id along with a hook that is called with the disclosed attributes. Here's what you need to override: ### 1. `getConfigId` ```solidity function getConfigId( bytes32 destinationChainId, bytes32 userIdentifier, bytes memory userDefinedData ) public view virtual override returns (bytes32) ``` Return the **verification config ID** that the hub should enforce for this request. In simple cases, you may store a single config ID in storage and return it. In advanced cases, compute a dynamic config id based on the inputs. **Example (static config):** ```solidity bytes32 public verificationConfigId; function getConfigId( bytes32, bytes32, bytes memory ) public view override returns (bytes32) { return verificationConfigId; } ``` ### 2. `customVerificationHook` ```solidity function customVerificationHook( ISelfVerificationRoot.GenericDiscloseOutputV2 memory output, bytes memory userData ) internal virtual override ``` This is called **after** hub verification succeeds. Use it to: * Mark the user as verified * Mint/allowlist/gate features * Emit events or write your own structs ## Constructor & Scope ```solidity constructor( address hubV2, string memory scopeSeed ) SelfVerificationRoot(hubV2, scopeSeed) {} ``` `SelfVerificationRoot` computes a **scope** at deploy time: * It Poseidon‑hashes the **contract address** (chunked) with your **`scopeSeed`** to produce a unique `uint256` scope. * The hub enforces that **submitted proofs match this scope**. Why scope matters: * Prevents cross‑contract proof replay. * Allow anonymity between different applications as the nullifier is calculated as a function of the scope. **Guidelines** * Keep `scopeSeed` short (≤31 ASCII bytes). Example: `"proof-of-human"`. * **Changing contract address changes the scope** (by design). Re‑deploys will need a fresh frontend config. * You can read the current scope on‑chain via `function scope() public view returns (uint256)`. :::note You can get the hub addresses from [deployed-contracts.md](/docs/self-pass/contracts/deployed-contracts/) ::: ## Setting Verification Configs A verification config is simply what you want to verify your user against. Your contract must reference a **verification config** that the hub recognizes. Typical steps: 1. **Format and register** the config off‑chain or in a setup contract: ```solidity SelfStructs.VerificationConfigV2 public verificationConfig; bytes32 public verificationConfigId; constructor( address hubV2, string memory scopeSeed, SelfUtils.UnformattedVerificationConfigV2 memory rawCfg ) SelfVerificationRoot(hubV2, scopeSeed) { // 1) Format the human‑readable struct into the on‑chain wire format verificationConfig = SelfUtils.formatVerificationConfigV2(rawCfg); // 2) Register the config in the Hub. **This call RETURNS the configId.** verificationConfigId = IIdentityVerificationHubV2(hubV2).setVerificationConfigV2(verificationConfig); } ``` 2. **Return the config id** from `getConfigId(...)` (static or dynamic): ```solidity function getConfigId( bytes32, bytes32, bytes memory ) public view override returns (bytes32) { return verificationConfigId; } ``` Here's how you would create a raw config: ```solidity import { SelfUtils } from "@selfxyz/contracts/contracts/libraries/SelfUtils.sol"; // Inside your contract constructor or setup function: string[] memory forbiddenCountries = new string[](1); forbiddenCountries[0] = CountryCodes.UNITED_STATES; // ISO 3-letter codes, max 40 countries SelfUtils.UnformattedVerificationConfigV2 memory verificationConfig = SelfUtils .UnformattedVerificationConfigV2({ olderThan: 18, // Minimum age (0 = no age check) forbiddenCountries: forbiddenCountries, // Countries to block (empty array = no restriction) ofacEnabled: false // Enable OFAC sanctions screening }); ``` :::warning Only a maximum of 40 countries are allowed! ::: ### Frontend ↔ Contract config must match :::danger The **frontend disclosure/verification config** used to produce the proof must **exactly match** the **contract’s verification config** (the `configId` you return). Otherwise the hub will detect a **mismatch** and verification fails. ::: Common pitfalls: * Frontend uses `minimumAge: 18` but contract config expects `21` . * Frontend uses different **scope** (e.g., points to a different contract address or uses a different `scopeSeed`). :::tip **Best practice:** Generate the config **once**, register it with the hub to get `configId`, and reference that same id in your dApp’s builder payload. ::: ### Extracting data from a users proof The `customVerificationHook` receives a `GenericDiscloseOutputV2` struct with all verified attributes. Here are all available fields: | Field | Type | Description | Requires Disclosure | |-------|------|-------------|:---:| | `attestationId` | `bytes32` | Document type: `1` = Passport, `2` = EU ID Card, `3` = Aadhaar, `4` = KYC | No | | `userIdentifier` | `uint256` | User's identifier. Derive address: `address(uint160(output.userIdentifier))` | No | | `nullifier` | `uint256` | Unique per-user per-scope value for Sybil resistance | No | | `forbiddenCountriesListPacked` | `uint256[4]` | Packed bitfield of excluded countries | No | | `olderThan` | `uint256` | Minimum age verified (e.g. `18`). `0` if not checked | No | | `ofac` | `bool[3]` | OFAC check results: `[0]` = passport number, `[1]` = name+DOB, `[2]` = name+YOB | No | | `issuingState` | `string` | ISO 3-letter code of issuing country (e.g. `"GBR"`) | Yes | | `name` | `string[]` | User's name fields from document | Yes | | `idNumber` | `string` | Document number (passport number, ID number, etc.) | Yes | | `nationality` | `string` | ISO 3-letter nationality code | Yes | | `dateOfBirth` | `string` | Date of birth in document format | Yes | | `gender` | `string` | Gender (`"M"` or `"F"`) | Yes | | `expiryDate` | `string` | Document expiry date | Yes | **"Requires Disclosure"** means the field is only populated if your frontend [disclosure config](/docs/self-pass/disclosures/) explicitly requests it. Fields not requested will be empty/zero. :::note The format of disclosed fields can vary by document type. Passports use ICAO MRZ format, Aadhaar uses different date and name formats, and KYC fields depend on the provider. See the [Document Specifications](/docs/self-enterprise/reference/document-specifications/) section for per-document-type details. ::: **Example — extracting nationality and age in your hook:** ```solidity function customVerificationHook( ISelfVerificationRoot.GenericDiscloseOutputV2 memory output, bytes memory userData ) internal override { // Always available require(output.olderThan >= 18, "Must be 18+"); // Only available if disclosure was requested string memory nationality = output.nationality; // e.g. "GBR" // Derive user's address address user = address(uint160(output.userIdentifier)); // Check OFAC results (if enabled in config) // ofac[0] = passport number match, ofac[1] = name+DOB, ofac[2] = name+YOB // All must be false (not sanctioned) for verification to pass } ``` The [boilerplate repo](https://github.com/selfxyz/self-integration-boilerplate) contains a full working example of extracting data from the `output` object. ## Minimal Example: Proof Of Human ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.28; import {SelfVerificationRoot} from "@selfxyz/contracts/contracts/abstract/SelfVerificationRoot.sol"; import {ISelfVerificationRoot} from "@selfxyz/contracts/contracts/interfaces/ISelfVerificationRoot.sol"; import {SelfStructs} from "@selfxyz/contracts/contracts/libraries/SelfStructs.sol"; import {SelfUtils} from "@selfxyz/contracts/contracts/libraries/SelfUtils.sol"; import {IIdentityVerificationHubV2} from "@selfxyz/contracts/contracts/interfaces/IIdentityVerificationHubV2.sol"; /** * @title ProofOfHuman * @notice Test implementation of SelfVerificationRoot for the docs * @dev This contract provides a concrete implementation of the abstract SelfVerificationRoot */ contract ProofOfHuman is SelfVerificationRoot { // Storage for testing purposes SelfStructs.VerificationConfigV2 public verificationConfig; bytes32 public verificationConfigId; // Events for testing event VerificationCompleted( ISelfVerificationRoot.GenericDiscloseOutputV2 output, bytes userData ); /** * @notice Constructor for the test contract * @param identityVerificationHubV2Address The address of the Identity Verification Hub V2 */ constructor( address identityVerificationHubV2Address, // Hub V2 address — see Deployed Contracts page string memory scopeSeed, // Unique app identifier (≤31 ASCII bytes), hashed into scope SelfUtils.UnformattedVerificationConfigV2 memory _verificationConfig // What to verify (age, countries, OFAC) ) SelfVerificationRoot(identityVerificationHubV2Address, scopeSeed) { verificationConfig = SelfUtils.formatVerificationConfigV2(_verificationConfig); verificationConfigId = IIdentityVerificationHubV2(identityVerificationHubV2Address) .setVerificationConfigV2(verificationConfig); } /** * @notice Implementation of customVerificationHook for testing * @dev This function is called by onVerificationSuccess after hub address validation * @param output The verification output from the hub * @param userData The user data passed through verification */ function customVerificationHook( ISelfVerificationRoot.GenericDiscloseOutputV2 memory output, bytes memory userData ) internal override { emit VerificationCompleted(output, userData); } function getConfigId( bytes32 /* destinationChainId */, bytes32 /* userIdentifier */, bytes memory /* userDefinedData */ ) public view override returns (bytes32) { return verificationConfigId; } } ``` --- # Deployed Contracts (Legacy) Source: https://docs.self.xyz/docs/self-pass/contracts/deployed-contracts/ Deployment addresses for the Self protocol, on Celo mainnet and testnet. ### Celo mainnet — Real passports
ContractDeployment address
IdentityVerificationHub0xe57F4773bd9c9d8b6Cd70431117d353298B9f5BF
### Celo Testnet — Mock passports
ContractDeployment address
IdentityVerificationHub0x16ECBA51e18a4a7e61fdC417f0d47AFEeDfbed74
--- # Working with userDefinedData (Legacy) Source: https://docs.self.xyz/docs/self-pass/contracts/working-with-userdefineddata/ The `userDefinedData` is mainly used in the frontend to allow users to pass in additional information during the time of verification. The data is passed into the `customVerificationHook` function on your `SelfVerificationRoot` contract, which can be used as desired to perform custom logic. :::warning The `userDefinedData` passed in the QRCode gets converted from a **string** to **bytes**. You will have to convert it back from bytes to a string again and work on top of that. ::: One use case that `userDefinedData` can be used for is to define what disclosures the user is asked to provide. In the example below, the `userDefinedData` is used create a key that maps to a particular setup of the verification config. ## Setting a config When setting a config just creating the config is not enough. You should register the config with the hub and this method will also return the config id. ```solidity //internal map that stores from hash(data) -> configId mapping(uint256 => uint256) public configs; function setConfig( string memory configDesc, SelfUtils.UnformattedVerificationConfigV2 config ) public { //create the key uint256 key = uint256(keccak256(bytes(configDesc))); //create the hub compliant config struct SelfStructs.VerificationConfigV2 verificationConfig = SelfUtils.formatVerificationConfigV2(_verificationConfig); //register and get the id uint256 verificationConfigId = IIdentityVerificationHubV2(identityVerificationHubV2Address) .setVerificationConfigV2(verificationConfig); //set it in the key configs[key] = verificationConfigId; } ``` ### Change the `getConfigId` in the `SelfVerificationRoot` Now we just have to change the `getConfigId` that returns the config ids from this map. This is pretty simple as now we just have to hash the existing bytes: ```solidity function getConfigId( bytes32 destinationChainId, bytes32 userIdentifier, bytes memory userDefinedData ) public view virtual returns (bytes32) { //the string is already converted to bytes uint256 key = keccak256(userDefinedData); return configs[key]; } ``` --- # Mobile SDK Alpha - Getting Started (Legacy) Source: https://docs.self.xyz/docs/self-pass/mobile-sdk/getting-started/ The Self Mobile SDK Alpha provides React Native screen components and client infrastructure for integrating Self's identity verification flows into your mobile application. ## Installation ```bash npm install @selfxyz/mobile-sdk-alpha ``` ## Native Modules Setup ⚠️ **Important**: The Mobile SDK requires native module configuration for both Android and iOS platforms. Before using the SDK, you must complete the native setup: - **[Native Modules Setup](/docs/self-pass/mobile-sdk/native-modules-setup/)** - Complete Android and iOS native configuration - **Android**: MainActivity configuration, build.gradle setup, permissions ## Quick Start The SDK requires two main integration points: 1. **SelfClientProvider** - Wraps your app with configured adapters and event listeners 2. **Screen Components** - Individual onboarding screens imported from specific paths ### Minimal Example ```tsx import React from 'react'; import { View } from 'react-native'; import { SelfClientProvider, createListenersMap, type Adapters } from '@selfxyz/mobile-sdk-alpha'; import { DocumentCameraScreen } from '@selfxyz/mobile-sdk-alpha/onboarding/document-camera-screen'; // Configure adapters (see selfclient-provider.md for details) const adapters: Adapters = { auth: { getPrivateKey: async () => "your-private-key" }, scanner: yourNFCScannerAdapter, network: { http: { fetch }, ws: { connect: (url) => new WebSocket(url) } }, crypto: { hash: async (data) => /* hash implementation */, sign: async (data, keyRef) => /* signing implementation */ }, documents: yourDocumentStorageAdapter }; // Configure event listeners for navigation const listeners = createListenersMap(); const config = {}; function App() { return ( console.log('Go back')} onSuccess={() => console.log('MRZ scan successful')} /> ); } ``` ## Next Steps - **[Native Modules Setup](/docs/self-pass/mobile-sdk/native-modules-setup/)** - Configure Android and iOS native modules - **[SelfClient Provider Setup](/docs/self-pass/mobile-sdk/selfclient-provider/)** - Configure adapters and event listeners - **[Onboarding Screen Components](/docs/self-pass/mobile-sdk/onboarding-screens/)** - Use the available screen components - **[Complete Example](/docs/self-pass/mobile-sdk/complete-example/)** - A runnable end-to-end integration ## Important Notes ⚠️ **Alpha Status**: This SDK is in alpha. The interface may change as it matures. Many current configuration options may be removed or simplified in future versions. ✅ **Core Stability**: The two main integration patterns (SelfClientProvider + screen components) are expected to remain stable. --- # SelfClient Provider Setup (Legacy) Source: https://docs.self.xyz/docs/self-pass/mobile-sdk/selfclient-provider/ The `SelfClientProvider` is a React context provider that must wrap all screens using the Mobile SDK. It configures the SDK with the necessary adapters and event listeners your app needs. ## Basic Setup ```tsx import { SelfClientProvider, createListenersMap, type Adapters, type Config } from '@selfxyz/mobile-sdk-alpha'; const config: Config = {}; const adapters: Adapters = { // Required adapters auth: authAdapter, scanner: scannerAdapter, network: networkAdapter, crypto: cryptoAdapter, documents: documentsAdapter, // Optional adapters (SDK provides defaults) analytics: analyticsAdapter, logger: loggerAdapter, clock: clockAdapter }; const { map: listeners } = createListenersMap(); function App() { return ( {/* Your app screens here */} ); } ``` ## Required Adapters ### Auth Adapter Manages private key access for cryptographic operations. ```tsx const authAdapter = { async getPrivateKey(): Promise { // Return private key as hex string (with or without 0x prefix) // Return null if no key available return await yourSecureStorage.getPrivateKey(); } }; ``` ### Scanner Adapter Provides NFC scanning capabilities. ```tsx // For React Native import { reactNativeScannerAdapter } from '@selfxyz/mobile-sdk-alpha'; const scannerAdapter = reactNativeScannerAdapter; // For Web (development/testing) import { webNFCScannerShim } from '@selfxyz/mobile-sdk-alpha'; const scannerAdapter = webNFCScannerShim; ``` ### Network Adapter Handles HTTP and WebSocket communications. ```tsx const networkAdapter = { http: { fetch: (input: RequestInfo, init?: RequestInit) => fetch(input, init) }, ws: { connect: (url: string) => { const socket = new WebSocket(url); return { send: (data) => socket.send(data), close: () => socket.close(), onMessage: (cb) => socket.addEventListener('message', ev => cb(ev.data)), onError: (cb) => socket.addEventListener('error', cb), onClose: (cb) => socket.addEventListener('close', cb) }; } } }; ``` ### Crypto Adapter Provides hashing and signing operations. ```tsx const cryptoAdapter = { async hash(data: Uint8Array, algo: 'sha256' = 'sha256'): Promise { // Use Web Crypto API or native crypto library const buffer = await crypto.subtle.digest('SHA-256', data); return new Uint8Array(buffer); }, async sign(data: Uint8Array, keyRef: string): Promise { // Implement signing logic for your key management throw new Error('Signing not implemented'); } }; ``` ### Documents Adapter Handles document storage and retrieval. ```tsx const documentsAdapter = { async loadDocumentCatalog(): Promise { return await yourStorage.getCatalog(); }, async saveDocumentCatalog(catalog: DocumentCatalog): Promise { await yourStorage.saveCatalog(catalog); }, async loadDocumentById(id: string): Promise { return await yourStorage.getDocument(id); }, async saveDocument(id: string, document: IDDocument): Promise { await yourStorage.saveDocument(id, document); }, async deleteDocument(id: string): Promise { await yourStorage.deleteDocument(id); } }; ``` ## Event Listeners & Navigation Event listeners handle SDK events and route to appropriate screens in your app. ```tsx import { SdkEvents } from '@selfxyz/mobile-sdk-alpha'; const { map: listeners, addListener } = createListenersMap(); // Handle country selection -> navigate to ID picker addListener(SdkEvents.DOCUMENT_COUNTRY_SELECTED, ({ countryCode, documentTypes }) => { navigation.navigate('IDPicker', { countryCode, documentTypes }); }); // Handle document type selection -> navigate to appropriate flow addListener(SdkEvents.DOCUMENT_TYPE_SELECTED, ({ documentType, countryCode }) => { switch (documentType) { case 'p': // Passport case 'i': // ID Card navigation.navigate('DocumentCamera'); break; case 'a': // Aadhaar navigation.navigate('AadhaarUpload', { countryCode }); break; case 'kyc': // KYC navigation.navigate('KycIntro', { countryCode }); break; default: navigation.navigate('ComingSoon', { countryCode }); } }); // Handle successful MRZ scan -> navigate to NFC scan addListener(SdkEvents.DOCUMENT_MRZ_READ_SUCCESS, () => { navigation.navigate('NFCScan'); }); // Handle MRZ scan failure -> navigate to troubleshooting addListener(SdkEvents.DOCUMENT_MRZ_READ_FAILURE, () => { navigation.navigate('DocumentTrouble'); }); ``` ## Common SDK Events | Event | Payload | Description | |-------|---------|-------------| | `DOCUMENT_COUNTRY_SELECTED` | `{ countryCode, documentTypes }` | User selected a country | | `DOCUMENT_TYPE_SELECTED` | `{ documentType, countryCode }` | User selected document type | | `DOCUMENT_MRZ_READ_SUCCESS` | `{}` | MRZ scanning completed successfully | | `DOCUMENT_MRZ_READ_FAILURE` | `{}` | MRZ scanning failed | ## Related Documentation - [Getting Started Guide](/docs/self-pass/mobile-sdk/getting-started/) - SDK overview and basic setup - [Onboarding Screen Components](/docs/self-pass/mobile-sdk/onboarding-screens/) - Available screen components - [Complete Example](/docs/self-pass/mobile-sdk/complete-example/) - These adapters and listeners assembled into a runnable app --- # Native Modules Setup (Legacy) Source: https://docs.self.xyz/docs/self-pass/mobile-sdk/native-modules-setup/ The Self Mobile SDK requires minimal native module configuration for Android and iOS to enable camera access and NFC scanning. ## Android Setup ### 1. MainActivity Configuration Add the import and NFC intent handling to your `android/app/src/main/java/com/yourapp/MainActivity.kt`: ```kotlin import com.selfxyz.selfSDK.RNSelfPassportReaderModule import android.content.Intent import android.util.Log class MainActivity : ReactActivity() { override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) Log.d("MAIN_ACTIVITY", "onNewIntent: " + intent.action) try { RNSelfPassportReaderModule.getInstance().receiveIntent(intent) } catch (e: IllegalStateException) { Log.w("MAIN_ACTIVITY", "RNSelfPassportReaderModule not ready; deferring NFC intent") setIntent(intent) } } } ``` ### 2. MainApplication Configuration Add the SelfSDK package to your `android/app/src/main/java/com/yourapp/MainApplication.kt`: ```kotlin import com.selfxyz.selfSDK.RNSelfPassportReaderPackage class MainApplication : Application(), ReactApplication { override fun getPackages(): List { val packages = PackageList(this).packages.toMutableList() packages.add(RNSelfPassportReaderPackage()) return packages } } ``` ### 3. Android Manifest Permissions Add these permissions to `android/app/src/main/AndroidManifest.xml`: ```xml ``` ### 4. NFC Meta Data Add this meta-data to your MainActivity in `AndroidManifest.xml`: ```xml ``` Create the NFC tech filter file at `android/app/src/main/res/xml/nfc_tech_filter.xml`: ```xml android.nfc.tech.IsoDep ``` ### 5. Build Configuration Add this to your `android/app/build.gradle`: ```gradle apply from: file("../../node_modules/@selfxyz/mobile-sdk-alpha/android/mobile-sdk-alpha-bom.gradle") android { buildFeatures { viewBinding true } } ``` ## iOS Setup ### 1. Enable NFC Capability In Xcode, enable the NFC capability for your app: 1. Open your project in Xcode 2. Select your app target 3. Go to "Signing & Capabilities" 4. Click "+ Capability" and add "Near Field Communication Tag Reading" **Important**: Set your build scheme to **Release** as Debug mode is not currently supported. ### 2. Info.plist Permissions Add these usage descriptions to your `ios/YourApp/Info.plist`: ```xml NSCameraUsageDescription Needed to scan the passport MRZ. NSFaceIDUsageDescription Needed to secure the secret NFCReaderUsageDescription Needed to read passport NFC chip for identity verification ``` ### 3. Podfile Configuration Add the following to your `ios/Podfile` in the `post_install` block: ```ruby post_install do |installer| installer.pods_project.targets.each do |target| if target.name == 'mobile-sdk-alpha' target.build_configurations.each do |config| xcframework_path = "$(PODS_ROOT)/../../mobile-sdk-alpha/ios/Frameworks/NFCPassportReader.xcframework" modules_path_device = "#{xcframework_path}/ios-arm64/SelfSDK.framework/Modules" modules_path_sim = "#{xcframework_path}/ios-arm64_x86_64-simulator/SelfSDK.framework/Modules" # Add module search paths config.build_settings['OTHER_SWIFT_FLAGS'] ||= ['$(inherited)'] config.build_settings['OTHER_SWIFT_FLAGS'] << "-I#{modules_path_device}" config.build_settings['OTHER_SWIFT_FLAGS'] << "-I#{modules_path_sim}" end end end end ``` ## Installation ```bash npm install @selfxyz/mobile-sdk-alpha # For iOS, install pods cd ios && pod install && cd .. ``` --- # Onboarding Screen Components (Legacy) Source: https://docs.self.xyz/docs/self-pass/mobile-sdk/onboarding-screens/ The Mobile SDK provides pre-built React Native screen components for the identity verification onboarding flow. These screens handle the UI and logic for document selection, camera scanning, and user interactions. ## Import Pattern Screens are imported directly from their onboarding paths, not from the main package index: ```tsx import { DocumentCameraScreen } from '@selfxyz/mobile-sdk-alpha/onboarding/document-camera-screen'; import IDSelectionScreen from '@selfxyz/mobile-sdk-alpha/onboarding/id-selection-screen'; import SDKCountryPickerScreen from '@selfxyz/mobile-sdk-alpha/onboarding/country-picker-screen'; ``` ## Provider Requirement ⚠️ **All screens must be wrapped in `SelfClientProvider`**. The screens use `useSelfClient()` internally and will throw an error if used outside the provider context. ## Available Screens ### DocumentCameraScreen Handles MRZ (Machine Readable Zone) scanning from identity documents using the device camera. ```tsx import { DocumentCameraScreen } from '@selfxyz/mobile-sdk-alpha/onboarding/document-camera-screen'; navigation.goBack()} onSuccess={() => navigation.navigate('NFCScan')} safeAreaInsets={{ top: 44, bottom: 34 }} // Optional /> ``` **Props:** - `onBack?: () => void` - Called when user taps back button - `onSuccess?: () => void` - Called when MRZ scan succeeds - `safeAreaInsets?: { top: number, bottom: number }` - Safe area padding **Behavior:** - Displays camera viewfinder with MRZ scanning overlay - Shows scan instructions and animations - Automatically processes MRZ data when detected - Emits `DOCUMENT_MRZ_READ_SUCCESS` or `DOCUMENT_MRZ_READ_FAILURE` events - Provides haptic feedback during scanning ### IDSelectionScreen Displays available document types for a selected country and handles document type selection. ```tsx import IDSelectionScreen from '@selfxyz/mobile-sdk-alpha/onboarding/id-selection-screen'; ``` **Props:** - `countryCode: string` - ISO country code (e.g., "USA", "GBR") - `documentTypes: string[]` - Available document types for the country **Behavior:** - Shows document type cards with icons (passport, ID card, Aadhaar, KYC, etc.) - Handles document type selection - Emits `DOCUMENT_TYPE_SELECTED` event with selected type and country - Provides visual feedback for selections ### CountryPickerScreen Displays a searchable list of countries for document selection. ```tsx import SDKCountryPickerScreen from '@selfxyz/mobile-sdk-alpha/onboarding/country-picker-screen'; ``` **Props:** - No props required - manages its own state internally **Behavior:** - Shows searchable country list with flags - Filters countries by name as user types - Loads available document types for each country - Emits `DOCUMENT_COUNTRY_SELECTED` event with country info - Optimized for performance with large country lists ## Screen Flow Example A typical onboarding flow might look like: ``` CountryPickerScreen ↓ (DOCUMENT_COUNTRY_SELECTED) IDSelectionScreen ↓ (DOCUMENT_TYPE_SELECTED) DocumentCameraScreen ↓ (DOCUMENT_MRZ_READ_SUCCESS) [Your NFC Scanning Screen] ``` ## Error Handling Screens handle errors internally and emit appropriate events. Listen for failure events in your event listeners: ```tsx addListener(SdkEvents.DOCUMENT_MRZ_READ_FAILURE, () => { // Navigate to error screen or retry flow navigation.navigate('DocumentScanError'); }); ``` ## Related Documentation - [Getting Started Guide](/docs/self-pass/mobile-sdk/getting-started/) - SDK overview and installation - [SelfClient Provider Setup](/docs/self-pass/mobile-sdk/selfclient-provider/) - Configure adapters and event listeners - [Complete Example](/docs/self-pass/mobile-sdk/complete-example/) - These screens wired into a runnable app --- # Complete Example (Legacy) Source: https://docs.self.xyz/docs/self-pass/mobile-sdk/complete-example/ A single runnable integration you can copy into a fresh React Native app: dev-stub adapters, event listeners wired to a navigator, and the screens assembled into one `App`. This page is only the assembly. The reference material it builds on lives elsewhere and is not repeated here: - [Native Modules Setup](/docs/self-pass/mobile-sdk/native-modules-setup/) — the Android and iOS config you must complete first. - [SelfClient Provider Setup](/docs/self-pass/mobile-sdk/selfclient-provider/) — what each adapter must implement, and the full `SdkEvents` catalog. - [Onboarding Screen Components](/docs/self-pass/mobile-sdk/onboarding-screens/) — every screen's props and error handling. ## 1. Adapters All five adapters are required (`auth`, `scanner`, `network`, `crypto`, `documents`). The stubs below are the smallest set that runs — see [Required Adapters](/docs/self-pass/mobile-sdk/selfclient-provider/#required-adapters) for what each one is actually for and what a production implementation needs. ```tsx import { createListenersMap, SdkEvents, SelfClientProvider, webNFCScannerShim, type Adapters, type Config, } from '@selfxyz/mobile-sdk-alpha'; const createAdapters = (): Adapters => ({ auth: { // you MUST provide a private key to the sdk that will be used when generating the zk circuits async getPrivateKey(): Promise { // In production, get from secure storage return '0x' + 'a'.repeat(64); // Dummy key for demo }, }, scanner: webNFCScannerShim, // Use web shim for development network: { http: { fetch: (input: RequestInfo, init?: RequestInit) => fetch(input, init), }, ws: { connect: (url: string) => { const socket = new WebSocket(url); return { send: (data) => socket.send(data), close: () => socket.close(), onMessage: (cb) => socket.addEventListener('message', (ev) => cb(ev.data)), onError: (cb) => socket.addEventListener('error', cb), onClose: (cb) => socket.addEventListener('close', cb), }; }, }, }, crypto: { async hash(data: Uint8Array): Promise { // Use Web Crypto API const buffer = await crypto.subtle.digest('SHA-256', data); return new Uint8Array(buffer); }, async sign(_data: Uint8Array, _keyRef: string): Promise { throw new Error('Signing not implemented in minimal example'); }, }, documents: { async loadDocumentCatalog() { return { documents: [] }; }, async saveDocumentCatalog(catalog) { console.log('Save catalog:', catalog); }, async loadDocumentById(id: string) { return null; }, async saveDocument(id: string, document) { console.log('Save document:', id, document); }, async deleteDocument(id: string) { console.log('Delete document:', id); }, }, // Optional: minimal analytics analytics: { trackEvent: (event: string, payload?: any) => { console.log('Analytics:', event, payload); }, }, }); ``` ## 2. Navigation listeners The SDK is event-driven: screens emit events, and your listeners decide where to go next. Building them in a factory that takes the navigator keeps the routing in one place. ```tsx function createNavigationListeners(navigation) { const { map, addListener } = createListenersMap(); // Country selection -> ID picker addListener(SdkEvents.DOCUMENT_COUNTRY_SELECTED, ({ countryCode, documentTypes }) => { navigation.navigate('IDPicker', { countryCode, documentTypes }); }); // Document type selection -> appropriate flow addListener(SdkEvents.DOCUMENT_TYPE_SELECTED, ({ documentType, countryCode }) => { switch (documentType) { case 'p': // Passport case 'i': // ID Card navigation.navigate('DocumentCamera'); break; case 'a': // Aadhaar navigation.navigate('AadhaarUpload', { countryCode }); break; case 'kyc': // KYC navigation.navigate('KycIntro', { countryCode }); break; default: navigation.navigate('ComingSoon', { documentType, countryCode }); } }); // MRZ scan success -> NFC scanning addListener(SdkEvents.DOCUMENT_MRZ_READ_SUCCESS, () => { navigation.navigate('NFCScan'); }); // MRZ scan failure -> troubleshooting addListener(SdkEvents.DOCUMENT_MRZ_READ_FAILURE, () => { navigation.navigate('DocumentTrouble'); }); return map; } ``` ### Event flow ``` CountryPickerScreen ↓ (DOCUMENT_COUNTRY_SELECTED) IDSelectionScreen ↓ (DOCUMENT_TYPE_SELECTED) DocumentCameraScreen ↓ (DOCUMENT_MRZ_READ_SUCCESS) [Your NFC Screen] ``` ## 3. Screen wrappers Each SDK screen is imported from its own path and wrapped so the navigator can pass it route params and back handlers. ```tsx import { SafeAreaView } from 'react-native'; import { DocumentCameraScreen } from '@selfxyz/mobile-sdk-alpha/onboarding/document-camera-screen'; import IDSelectionScreen from '@selfxyz/mobile-sdk-alpha/onboarding/id-selection-screen'; import SDKCountryPickerScreen from '@selfxyz/mobile-sdk-alpha/onboarding/country-picker-screen'; function CountryPickerScreen() { return ( ); } function IDPickerScreen({ route }) { const { countryCode, documentTypes } = route.params; return ( ); } function DocumentCameraStackScreen({ navigation }) { return ( navigation.goBack()} onSuccess={() => { console.log('MRZ scan successful!'); }} /> ); } ``` ## 4. Assemble the app `SelfClientProvider` wraps everything, so every SDK screen must render inside it. :::note The listeners need the navigator, but `SelfClientProvider` sits **outside** `NavigationContainer`, so `useNavigation()` isn't available at that level. Use React Navigation's [`createNavigationContainerRef`](https://reactnavigation.org/docs/navigating-without-navigation-prop/) and pass the ref to the container. ::: ```tsx import React, { useMemo } from 'react'; import { NavigationContainer, createNavigationContainerRef } from '@react-navigation/native'; import { createStackNavigator } from '@react-navigation/stack'; const Stack = createStackNavigator(); const navigationRef = createNavigationContainerRef(); export default function App() { const config: Config = useMemo(() => ({}), []); const adapters: Adapters = useMemo(() => createAdapters(), []); const listeners = useMemo(() => createNavigationListeners(navigationRef), []); return ( ); } ``` The `AadhaarUpload`, `KycIntro`, `NFCScan`, `DocumentTrouble`, and `ComingSoon` routes referenced by the listeners are yours to build — register them alongside the three above as you add each flow. --- # KMP SDK (Legacy) Source: https://docs.self.xyz/docs/self-pass/kmp-sdk/ :::warning The Self KMP SDK is currently in **alpha**. To get access or try it out, please [contact the Self team](https://self.xyz/#contact). ::: ## Overview The Self KMP SDK lets you add Self identity verification to a Kotlin Multiplatform app while keeping the verification API in shared Kotlin code. It's a good fit if your app already uses Kotlin Multiplatform and you want to keep verification logic in shared code. The SDK provides: - shared configuration with `SelfSdkConfig` - shared request construction with `VerificationRequest` - a common launch API and callback interface You'll need to provide: - secure storage setup on both platforms - Android activity binding before launch - iOS WebView provider and secure storage via `SelfSdkSwift` or your own implementations The SDK currently supports Android and iOS. On iOS, you also need the `SelfSdkSwift` companion package or equivalent native provider implementations. ## How It Works
Self ProtocolSelf Verification UISelf SDKHost AppSelf ProtocolSelf Verification UISelf SDKHost AppApp setupVerification sessionProof lifecycleCompletionalt[Verification succeeds][Verification fails][User cancels]Configure `SelfSdkConfig`Call `launch(request, callback)`Launch hosted verification flowComplete KYC flowDelete KYC documentsRegister identity proofDisclose & verify proofReturn verification outcomeSuccess`onSuccess()`Failure`onFailure(error)`Cancelled`onCancelled()`
## Quick Start All calling code lives in your `commonMain` source set: ```kotlin val sdk = SelfSdk.configure( SelfSdkConfig( environment = SelfEnvironment.PROD, ) ) val request = VerificationRequest( userId = "user-uuid", scope = "identity", disclosures = listOf("ofac"), ) sdk.launch( request = request, callback = object : SelfSdkCallback { override fun onSuccess() { println("Verification completed successfully") } override fun onFailure(error: SelfSdkError) { println("Error [${error.code}]: ${error.message}") } override fun onCancelled() { println("User dismissed verification") } } ) ``` ## Platform Setup ::::tabs :::tab{label="Android"} Android setup goes in your `androidMain` source set (typically your `MainActivity`). ```kotlin import android.os.Bundle import androidx.activity.ComponentActivity import xyz.self.sdk.api.SelfSdk import xyz.self.sdk.providers.EncryptedSharedPreferencesProvider import xyz.self.sdk.providers.SdkProviderRegistry class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 1. Register providers (demo provider — implement your own for production) if (SdkProviderRegistry.secureStorage == null) { SdkProviderRegistry.secureStorage = EncryptedSharedPreferencesProvider(this) } // 2. Bind the activity (required for launching the verification screen) SelfSdk.bindActivity(this) // Now SelfSdk.configure() and sdk.launch() work from commonMain setContent { App() } } } ``` `bindActivity()` registers an `ActivityResultLauncher` on the given `ComponentActivity`. Without it, `launch()` fails with `MISSING_ACTIVITY`. **Convenience overload** — combines configure + bind + launch in one call: ```kotlin SelfSdk.launch( activity = this, config = SelfSdkConfig(environment = SelfEnvironment.PROD), request = VerificationRequest(userId = "user-uuid"), callback = myCallback, ) ``` ::: :::tab{label="iOS"} iOS requires a Swift companion package (`SelfSdkSwift`) that provides native provider implementations using Keychain Services, CryptoKit, and WKWebView. ```swift import SwiftUI import ComposeApp // Your KMP framework (exports KMP types) import SelfSdkSwift // Swift companion package // Declare that Swift classes conform to KMP protocols. // Required because Swift needs to see both the protocol (from KMP) // and the class (from SelfSdkSwift) to establish conformance. extension SecureStorageProviderImpl: SecureStorageProvider {} extension WebViewProviderImpl: WebViewProvider {} @main struct iOSApp: App { init() { // Register providers (demo providers — implement your own for production) SdkProviderRegistry.shared.secureStorage = SecureStorageProviderImpl() IosProviderRegistry.shared.webView = WebViewProviderImpl() } var body: some Scene { WindowGroup { ContentView() } } } ``` KMP compiles Kotlin `object` singletons to Objective-C classes — Swift accesses them via `.shared`. Once providers are registered, call `SelfSdk.configure()` and `sdk.launch()` from your shared `commonMain` code — no iOS-specific launch code is needed. The Quick Start example above works as-is on both platforms. `SelfSdkSwift` is a convenience, not a requirement. You can provide your own classes as long as they conform to the KMP protocols (`SecureStorageProvider`, `WebViewProvider`). ::: :::: ## API Reference ### SelfSdkConfig ```kotlin SelfSdkConfig( endpoint: String = "https://api.self.xyz/verifier", environment: SelfEnvironment = SelfEnvironment.PROD, version: Int = 2, appName: String? = null, appEndpoint: String? = null, endpointType: String? = null, // derived from environment if not set chainID: Int? = null, ) ``` | Parameter | Default | Description | |-----------|---------|-------------| | `endpoint` | `"https://api.self.xyz"` | Self backend API endpoint | | `environment` | `PROD` | `SelfEnvironment.PROD` (real documents, mainnet) or `SelfEnvironment.STG` (mock documents, testnet). See the [environment guide](/docs/self-pass/key-concepts/#choose-your-environment) for details. | | `version` | `2` | Protocol version for the verification flow | | `appName` | `null` | Display name shown in the verification UI | | `appEndpoint` | `null` | URL of the backend verifier that verifies the ZK proof | | `endpointType` | derived | Derived from `environment` if not set: `"https"` for PROD, `"staging_https"` for STG | | `chainID` | `null` | Celo chain ID — `11142220` (testnet) or `42220` (mainnet) | ### VerificationRequest ```kotlin VerificationRequest( userId: String? = null, scope: String? = null, disclosures: List = listOf("ofac"), verificationId: String? = null, excludedCountries: List = emptyList(), userIdType: String? = null, userDefinedData: String? = null, selfDefinedData: String? = null, ) ``` | Parameter | Default | Description | |-----------|---------|-------------| | `userId` | `null` | User identifier for the verification | | `scope` | `null` | Verification scope (e.g., `"identity"`) | | `disclosures` | `listOf("ofac")` | Requested disclosures (e.g., `"full_name"`, `"dob"`, `"nationality"`, `"ofac"`) | | `verificationId` | `null` | Pre-created verification session ID | | `excludedCountries` | `emptyList()` | Country codes to exclude from verification | | `userIdType` | `null` | Type of user ID provided | | `userDefinedData` | `null` | Custom data passed through the verification flow | | `selfDefinedData` | `null` | Self-defined metadata | ### SelfSdkCallback ```kotlin interface SelfSdkCallback { fun onSuccess() fun onFailure(error: SelfSdkError) fun onCancelled() } ``` | Method | When it's called | |--------|-----------------| | `onSuccess()` | Verification completed successfully | | `onFailure(error)` | Verification failed | | `onCancelled()` | User dismissed without completing | ### SelfSdkError ```kotlin data class SelfSdkError( val code: String, val message: String, ) ``` | Code | Platform | Meaning | |------|----------|---------| | `MISSING_ACTIVITY` | Android | `bindActivity()` not called before `launch()` | | `VERIFICATION_IN_PROGRESS` | Both | A verification flow is already running | | `LAUNCHER_NOT_AVAILABLE` | Android | Could not initialize `ActivityResultLauncher` | | `VERIFICATION_FAILED` | Both | The verification flow failed | | `NO_VIEW_CONTROLLER` | iOS | Could not find a top `UIViewController` to present | ## Provider Interfaces Providers must be registered before calling `launch()`. ### Required Providers | Provider | Registry | Platform | Purpose | |----------|----------|----------|---------| | `SecureStorageProvider` | `SdkProviderRegistry` | Both | Keychain/keystore read/write | | `WebViewProvider` | `IosProviderRegistry` | iOS only | WKWebView creation and JS evaluation | Android does not need a `WebViewProvider` — the SDK manages the WebView internally. ### SecureStorageProvider ```kotlin interface SecureStorageProvider { fun get(key: String): String? fun set(key: String, value: String) fun remove(key: String) fun clear() } ``` ### WebViewProvider (iOS only) ```kotlin interface WebViewProvider { fun createWebView( onMessageReceived: (String) -> Unit, isDebugMode: Boolean, queryParams: String? = null, ): UIView fun evaluateJs(js: String) fun getViewController(): UIViewController fun isBridgeRequestAllowed(): Boolean fun configureRemoteLoading(remoteWebAppBaseURL: String?) fun configureDevServer(devServerUrl: String?) } ``` ### Demo Implementations The SDK ships demo implementations for quick prototyping. **These are not intended for production use** — you should implement your own `SecureStorageProvider` backed by your app's secure storage strategy. **Android** (shipped in the SDK): | Class | Backing API | |-------|-------------| | `EncryptedSharedPreferencesProvider` | AndroidX EncryptedSharedPreferences (AES256-SIV / AES256-GCM) | **iOS** (shipped in `SelfSdkSwift`): | Class | Backing API | |-------|-------------| | `SecureStorageProviderImpl` | Keychain Services | | `WebViewProviderImpl` | WKWebView with `WKScriptMessageHandler` bridge | ### Implementing Your Own Providers For production, implement the provider interfaces directly: ::::tabs :::tab{label="Android"} ```kotlin class MySecureStorage(context: Context) : SecureStorageProvider { override fun get(key: String): String? { /* ... */ } override fun set(key: String, value: String) { /* ... */ } override fun remove(key: String) { /* ... */ } override fun clear() { /* ... */ } } SdkProviderRegistry.secureStorage = MySecureStorage(context) ``` ::: :::tab{label="iOS"} ```swift class MySecureStorage: NSObject, SecureStorageProvider { func get(key: String) -> String? { /* ... */ } func set(key: String, value: String) { /* ... */ } func remove(key: String) { /* ... */ } func clear() { /* ... */ } } SdkProviderRegistry.shared.secureStorage = MySecureStorage() ``` ::: :::: --- # Troubleshooting (Legacy) Source: https://docs.self.xyz/docs/self-pass/troubleshooting/ Some of the most common errors you will encounter when integrating Self can be found below with possible solutions. If you find you are still having issues, email team@self.xyz. If you are using AI models to build your app and are encoutering issues, verify that you are using the most up to date versions of our packages. Check your `package.json` file in your frontend and backend. Many AI programs still utilize previous versions when building which will cause errors to occur. You can also make sure you are using a valid working version by using the ones specified in the workshop repo. Check for `@selfxyz/common`, `@selfxyz/qrcode`, `@selfxyz/contracts`, `@selfxyz/core`. * Workshop frontend `package.json`: [https://github.com/selfxyz/self-integration-boilerplate/blob/main/app/package.json](https://github.com/selfxyz/self-integration-boilerplate/blob/main/app/package.json) * Workshop smart contracts `package.json`: [https://github.com/selfxyz/self-integration-boilerplate/blob/main/contracts/package.json](https://github.com/selfxyz/self-integration-boilerplate/blob/main/contracts/package.json) * Workshop backend `package.json`: [https://github.com/selfxyz/self-integration-boilerplate/blob/main/app/package.json](https://github.com/selfxyz/self-integration-boilerplate/blob/main/app/package.json) ## Common errors while verifying proofs ### ScopeMismatch There is a mismatch of the scope value between the smart contract and the front end. Check the `scopeSeed` value that was used to deploy the smart contract (in your .env/deployment script/smart contract). Then, check the scope value you are passing into the `SelfAppBuilder` object you are building on your frontend (e.g. in your page.tsx). Ensure the `scope` string in `SelfAppBuilder` is the same as the `scopeSeed` used in your contract. ### Invalid 'to' address There is a mismatch between `endpoint` and `endpoint-type` in the `SelfAppBuilder` object. * If `endpointType` is `celo` , then `endpoint` value must be the contract address for a contract deployed on Celo Mainnet. * If `endpointType` is `staging_celo` , then `endpoint` value must be the contract address for a contract deployed on Celo Sepolia Testnet. * If `endpointType` is `https` , then `endpoint` value must be a http endpoint. * If `endpointType` is `staging_https` , then `endpoint` value must be a http endpoint. `staging_https` is to be used for verifying mock documents. ### -32000, "message" : "execution reverted" This is a generic error message thrown when something is wrong with your smart contract logic. This can be caused by a wide variety of factors. Try: * Checking that you deployed your contract with the [correct Hub address.](/docs/self-pass/contracts/deployed-contracts/) * Ensure that your `customVerificationHook` logic is sound and contains no errors. If the error is from a failing require check, the require statements error message will be displayed instead of the -32000 error message. Other common error codes for interacting with smart contracts can be found in the [EIP-1474 specifications](https://eips.ethereum.org/EIPS/eip-1474). ### Invalid config ID A common cause of this error is trying to use a Mock Passport to verify a proof on a Celo Mainnet smart contract. Mock Passports can only be used for deployments on Celo Sepolia. ### Config Mismatch Can be caused by various different issues to do with a mismatch being found between the Verification Config specified in the smart contract and the frontend - see the [section in Basic Integration](https://docs.self.xyz/contract-integration/basic-integration#setting-verification-configs). ### InvalidIdentityCommitmentRoot This is caused by trying to verify a mock passport on a contract deployed on Celo Mainnet, or verifying a real passport on a contract deployed on Celo Sepolia Testnet. * Real passports are only valid for Celo Mainnet * Mock passports are only valid for Celo Sepolia Testnet ### error decoding response body: expected value at line 1 coloumn 1 Not following the API spec properly. Check the [Endpoint API reference](https://docs.self.xyz/backend-integration/basic-integration#endpoint-api-reference) and verify you are following it. ### builder error: relative URL URL being used for endpoint is malformed. Try verifying it is correct and is being input correctly. ### Transaction failed with error: 0xf4d678b8 If you get an error with a message like this but with differing data after the 0x, it is likely the smart contract is hitting a custom error. The error displayed is the hex selector of the custom error you have defined, and so will be different based on the name of your custom error. For example, 0xf4d678b8 is the hex selector for the custom error `InsufficientBalance`. ### DOCTYPE Caused when you are developing locally and defining your public endpoint (`NEXT_PUBLIC_SELF_ENDPOINT` in the workshop example) with an older version of ngrok, or ngrok not setup properly. ### Due to technical issues This error can be caused from adding >40 countries to the exclusion list. --- # V1 to V2 Migration Guide (Legacy) Source: https://docs.self.xyz/docs/self-pass/migration-v1-v2/ This guide helps you migrate from Self Protocol V1 to V2. V2 introduces multi-document support, dynamic configuration, and improved data structures. ## Overview of Changes ### What's New in V2 * **Multi-document support**: E-Passports, EU ID Cards, Aadhaar, and KYC * **Dynamic configuration**: Switch configurations without redeployment * **Enhanced data formats**: Pre-extracted, human-readable outputs * **User context data**: Pass custom data through verification flow * **Improved error handling**: Detailed configuration mismatch reporting ### Breaking Changes * Backend SDK constructor requires new parameters * Configuration methods replaced with interface * Verification method signature changed * Frontend requires disclosures object * Smart contract interfaces updated ## Backend Migration ### 1. Update Dependencies ```bash npm install @selfxyz/core@latest ``` ### 2. Update Constructor **V1 (Old):** ```javascript const verifier = new SelfBackendVerifier( "my-app-scope", "https://api.example.com/verify", false // mock mode ); ``` **V2 (New):** ```javascript import { SelfBackendVerifier, AttestationId, UserIdType, IConfigStorage, AllIds } from '@selfxyz/core'; // Option 1: Use AllIds for all document types (recommended for most cases) const allowedIds = AllIds; // Option 2: Define specific allowed document types // const allowedIds = new Map(); // allowedIds.set(AttestationId.E_PASSPORT, true); // Accept passports // allowedIds.set(AttestationId.EU_ID_CARD, true); // Accept EU ID cards // allowedIds.set(AttestationId.AADHAAR, true); // Accept Aadhaar // allowedIds.set(AttestationId.KYC, true); // Accept KYC // Implement configuration storage class ConfigStorage implements IConfigStorage { async getConfig(configId: string) { // Return your verification requirements return { minimumAge: 18, excludedCountries: ['IRN', 'PRK'], ofac: true }; } async getActionId(userIdentifier: string, userDefinedData?: string) { // Return config ID based on your logic return 'default_config'; } } const verifier = new SelfBackendVerifier( "my-app-scope", "https://api.example.com/verify", false, // mock mode allowedIds, // NEW: allowed document types new ConfigStorage(), // NEW: config storage UserIdType.UUID // NEW: user ID type ); ``` ### 3. Update Configuration **V1 (Old):** ```javascript // Direct method calls verifier.setMinimumAge(18); verifier.excludeCountries('Iran', 'North Korea'); verifier.enablePassportNoOfacCheck(); verifier.enableNameOfacCheck(); verifier.enableDobOfacCheck(); ``` **V2 (New):** ```javascript // Configuration via IConfigStorage implementation class ConfigStorage implements IConfigStorage { async getConfig(configId: string) { // All configuration in one place return { minimumAge: 18, excludedCountries: ['IRN', 'PRK'], // Use ISO 3-letter codes ofac: true // Single boolean for all OFAC checks }; } } ``` ### 4. Update Verification Method **V1 (Old):** ```javascript app.post('/api/verify', async (req, res) => { const { proof, publicSignals } = req.body; try { const isValid = await verifier.verify(proof, publicSignals); res.json({ valid: isValid }); } catch (error) { res.status(400).json({ error: error.message }); } }); ``` **V2 (New):** ```javascript app.post('/api/verify', async (req, res) => { const { attestationId, proof, pubSignals, userContextData } = req.body; try { const result = await verifier.verify( attestationId, // NEW: 1 passport, 2 EU ID, 3 Aadhaar, 4 KYC proof, pubSignals, userContextData // NEW: hex-encoded context data ); if (result.isValidDetails.isValid) { res.json({ status: 'success', result: true, credentialSubject: result.discloseOutput, documentType: attestationId === 1 ? 'passport' : attestationId === 2 ? 'eu_id_card' : attestationId === 3 ? 'aadhaar' : 'kyc' }); } else { res.status(400).json({ status: 'error', result: false, details: result.isValidDetails }); } } catch (error) { if (error.name === 'ConfigMismatchError') { res.status(400).json({ status: 'error', message: 'Configuration mismatch', issues: error.issues }); } else { res.status(500).json({ error: error.message }); } } }); ``` ### 5. Handle New Response Format **V1 Response:** ```javascript // Simple boolean true/false ``` **V2 Response:** ```javascript { attestationId: 1, // Document type isValidDetails: { isValid: boolean, // Overall result isOlderThanValid: boolean, // Age check result isOfacValid: boolean // OFAC check result }, forbiddenCountriesList: [], // Excluded countries discloseOutput: { // Pre-extracted data nationality: "USA", minimumAge: "21", name: ["JOHN", "DOE"], dateOfBirth: "01-01-1990", issuingState: "USA", idNumber: "123456789", gender: "M", expiryDate: "01-01-2030", ofac: [true, true, true] }, userData: { userIdentifier: "uuid-here", userDefinedData: "custom-data" } } ``` ## Frontend Migration ### 1. Update Dependencies ```bash npm install @selfxyz/qrcode@latest ``` ### 2. Update QR Code Configuration **V1 (Old):** ```javascript import { SelfAppBuilder } from '@selfxyz/qrcode'; const selfApp = new SelfAppBuilder({ appName: "My App", scope: "my-app-scope", endpoint: "https://api.example.com/verify", userId: userId, logoBase64: logo }).build(); ``` **V2 (New):** ```javascript import { SelfAppBuilder } from '@selfxyz/qrcode'; const selfApp = new SelfAppBuilder({ appName: "My App", scope: "my-app-scope", endpoint: "https://api.example.com/verify", userId: userId, logoBase64: logo, version: 2, // NEW: Specify V2 userDefinedData: "custom-data", // NEW: Optional custom data disclosures: { // NEW: Must match backend config // Verification rules minimumAge: 18, excludedCountries: ['IRN', 'PRK'], ofac: true, // Data fields to reveal name: true, nationality: true, dateOfBirth: true } }).build(); ``` ### 3. Important: Disclosures Object The `disclosures` object in V2 contains **both** verification rules and data fields: ```javascript disclosures: { // Verification rules (must match backend exactly) minimumAge: 18, // Age requirement excludedCountries: ['IRN', 'PRK'], // ISO 3-letter codes ofac: true, // OFAC checking // Data fields to reveal name: true, // Full name nationality: true, // Nationality dateOfBirth: true, // Date of birth issuingState: true, // Issuing country idNumber: true, // Document number gender: true, // Gender expiryDate: true // Expiration date } ``` ## Smart Contract Migration ### 1. Update Contract Inheritance **V1 (Old):** ```solidity import { IIdentityVerificationHub } from "@selfxyz/contracts/interfaces/IIdentityVerificationHub.sol"; contract MyContract { IIdentityVerificationHub public hub; function verify(/* params */) external { // Direct hub integration } } ``` **V2 (New):** ```solidity import { SelfVerificationRoot } from "@selfxyz/contracts/abstract/SelfVerificationRoot.sol"; contract MyContract is SelfVerificationRoot { constructor( address _hub, bytes32 _scope ) SelfVerificationRoot(_hub, _scope) {} // Override to handle verification results function customVerificationHook( GenericDiscloseOutputV2 memory output, bytes memory userData ) internal override { // Your business logic here } // Override to provide configuration ID function getConfigId( bytes32 destinationChainId, bytes32 userIdentifier, bytes memory userDefinedData ) public view override returns (bytes32) { // Generate your config ID at https://tools.self.xyz/ // Default config ID: 0x7b6436b0c98f62380866d9432c2af0ee08ce16a171bda6951aecd95ee1307d61 return 0x7b6436b0c98f62380866d9432c2af0ee08ce16a171bda6951aecd95ee1307d61; } } ``` ### 2. Update Hub Addresses **V2 Hub Addresses:** ```solidity // Celo Mainnet address constant HUB_V2 = 0xe57F4773bd9c9d8b6Cd70431117d353298B9f5BF; // Celo Sepolia Testnet address constant HUB_V2_STAGING = 0x16ECBA51e18a4a7e61fdC417f0d47AFEeDfbed74; ``` ### 3. Handle New Data Structure **V1 Structure:** ```solidity struct VcAndDiscloseVerificationResult { uint256 attestationId; uint256 scope; uint256 userIdentifier; uint256 nullifier; uint256[3] revealedDataPacked; // Packed data } ``` **V2 Structure:** ```solidity struct GenericDiscloseOutputV2 { bytes32 attestationId; // Now bytes32 uint256 userIdentifier; uint256 nullifier; string issuingState; // Pre-extracted string[] name; // Pre-extracted array string idNumber; // Renamed from passportNumber string nationality; // Pre-extracted string dateOfBirth; // Pre-extracted format string gender; // Pre-extracted string expiryDate; // Pre-extracted uint256 minimumAge; bool[3] ofac; // Bool array uint256[4] forbiddenCountriesListPacked; } ``` ## Common Migration Issues ### 1. Configuration Mismatch **Problem:** Frontend disclosures don't match backend configuration ``` ConfigMismatchError: Configuration mismatch ``` **Solution:** Ensure frontend and backend have identical settings: ```javascript // Frontend disclosures: { minimumAge: 18, excludedCountries: ['IRN', 'PRK'], ofac: true } // Backend (in getConfig) return { minimumAge: 18, excludedCountries: ['IRN', 'PRK'], ofac: true }; ``` ### 2. Missing Attestation ID **Problem:** Verification fails with missing attestation ID **Solution:** Frontend must send attestation ID: ```javascript const requestBody = { attestationId: 1, // 1 passport, 2 EU ID, 3 Aadhaar, 4 KYC proof: proof, pubSignals: pubSignals, userContextData: userContextData }; ``` ### 3. Invalid User Context Data **Problem:** User context data validation fails **Solution:** Ensure proper hex encoding: ```javascript // Create user context data (256 bytes total) const userContextData = '0x' + '0'.repeat(512); // 512 hex chars = 256 bytes ``` ### 4. Document Type Not Allowed **Problem:** "Attestation ID is not allowed" error **Solution:** Add document type to allowedIds: ```javascript // Option 1: Use AllIds for all document types const allowedIds = AllIds; // Option 2: Define specific allowed document types // const allowedIds = new Map(); // allowedIds.set(AttestationId.E_PASSPORT, true); // Add passport // allowedIds.set(AttestationId.EU_ID_CARD, true); // Add EU ID card // allowedIds.set(AttestationId.AADHAAR, true); // Add Aadhaar // allowedIds.set(AttestationId.KYC, true); // Add KYC ``` ## Testing Your Migration ### 1. Test with Mock Passports ```javascript // Use staging/testnet for development const verifier = new SelfBackendVerifier( "test-scope", "https://test.ngrok.app/verify", true, // Enable mock mode allowedIds, configStorage, UserIdType.UUID ); // Disable OFAC for mock passports class ConfigStorage { async getConfig() { return { minimumAge: 18, ofac: false // Must be false for mock passports }; } } ``` ### 2. Test Multiple Document Types ```javascript // Test passport verification const passportResult = await verifier.verify( AttestationId.E_PASSPORT, // 1 passportProof, passportSignals, userContextData ); // Test EU ID card verification const idCardResult = await verifier.verify( AttestationId.EU_ID_CARD, // 2 idCardProof, idCardSignals, userContextData ); // Test KYC verification const kycResult = await verifier.verify( AttestationId.KYC, // 4 kycProof, kycSignals, userContextData ); ``` ### 3. Verify Configuration Switching ```javascript class DynamicConfigStorage { async getConfig(configId: string) { switch(configId) { case 'strict': return { minimumAge: 21, ofac: true }; case 'relaxed': return { minimumAge: 18, ofac: false }; default: return { minimumAge: 18, ofac: true }; } } async getActionId(userIdentifier: string, userDefinedData?: string) { // Select config based on user data return userDefinedData === 'premium' ? 'strict' : 'relaxed'; } } ``` ## Best Practices ### 1. Configuration Management * Store configurations in a database for easy updates * Version your configurations for rollback capability * Use meaningful config IDs (not just hashes) * Document configuration requirements ### 2. Error Handling ```javascript try { const result = await verifier.verify(/* params */); } catch (error) { if (error.name === 'ConfigMismatchError') { // Log detailed issues for debugging console.error('Config issues:', error.issues); // Return user-friendly message return { error: 'Verification configuration error' }; } // Handle other errors } ``` ### 3. Security Considerations * Always validate attestation IDs * Store and check nullifiers to prevent replay * Use appropriate scopes for different use cases * Never expose configuration details to frontend ### 4. Performance Optimization * Cache configuration objects * Reuse verifier instances * Batch verification requests when possible * Use connection pooling for RPC calls ## Resources * [Key Concepts](/docs/self-pass/key-concepts/) - Basic V2 setup * [Basic Integration](/docs/self-pass/contracts/basic-integration/) - Contract examples * [Workshop Example](https://github.com/selfxyz/self-integration-boilerplate) - Simple implementation ## Need Help? If you encounter issues during migration: 1. Review example implementations 2. Report issues at [GitHub Issues](https://github.com/selfxyz/self/issues) --- # Overview (Legacy) Source: https://docs.self.xyz/docs/self-pass/architecture/overview/ ### Introduction The Self protocol is an identity protocol designed to let people use their real-world attestation in a permissionless way, for Sybil resistance and selective disclosure. Our core thesis is that web-of-trust systems are hard to scale securely, and biometric verification à la Worldcoin has a long way to go, so bootstrapping from existing sources of trust like institutions is the most pragmatic way to verify identities securely today and in a privacy preserving way. We support passports, national IDs, Aadhaar, and KYC attestations. Self has **three** main components: * A mobile app that supports multiple identity flows (passport NFC, ID document flows, Aadhaar, and KYC). The app operates on both iOS and Android. * Zero-knowledge circuits that can be used to verify the validity of supported attestations, generate identity commitments and selectively disclose attributes. * Smart contracts that verify proofs, manage a merkle tree of identity commitments and allow for onchain disclosure of data while guaranteeing the permissionless aspect of the protocol. :::note KYC attestations (attestation ID `4`) are generated through the KYC verification flow and are verified through the same V2 verification stack. ::: ### Background on Biometric Passports Biometric passports were introduced in the 2000s as a way to streamline border control and reduce the risk of passport forgery. They are now issued in more than 170 countries, and their specifications are established by the ICAO (International Civil Aviation Organisation) and made available in Document 9303[ on their website](https://www.icao.int/publications/pages/publication.aspx?docnum=9303). Each biometric passport contains an embedded microchip that can be read by any NFC reader. It stores multiple datagroups (up to 16) along with a SOD (Document Security Object) that can be used to verify the integrity of the passport. The SOD contains hashes of all datagroups, a signature attesting to the validity of the passport, information on which hash functions and signature algorithms were used, and the certificate that signed the passport. All possible datagroups and their content can be found in the image below. DG1 and DG2 are mandatory, the rest is optional.\
Table of ePassport data groups: DG1 holds the machine readable zone details; DG2 to DG4 hold encoded face, finger, and eye biometrics; DG5 to DG7 displayed identification features; DG8 to DG16 security features, additional details, and the Active Authentication public key (DG15)
In particular: * `DG1` has the same content as the machine readable zone, and is the source of all the information we care to verify.
Specimen identity document with the two-line machine readable zone highlighted
* `DG2` contains the person’s photo. Because it contains a lot of entropy, it makes sure that the final signature can’t be dictionary-attacked starting with some of the person’s information. * `DG15` (optional) is the public key corresponding to the passport’s Active Authentication private key. We plan to use it to improve security in the future.
Signature chain diagram: data groups DG1 through DG15 are hashed into eContent, which is hashed into signedAttr, then hashed and signed to produce the ePassport signature
The passport data groups are hashed and process, and the resulting final hash is signed by a Document Signing Certificate (DSC), which is itself signed by a Country Signing Certificate Authority (CSCA) as part of a certificate chain. The DSC can be read from the passport's chip and the CSCA can be looked up on international registries such as the ICAO masterlist. The `eContent` consists of the concatenation of all DG hashes, while the `signedAttr` is the final message signed by the issuing country. Sometimes, different hashing algorithms are used at each step. According to the specifications, each DSC should sign up to 100k passports, each CSCA should rotate every 3-5 years, and a country should always have at minimum 2 valid CSCA at the same time.\ --- # Architecture (Legacy) Source: https://docs.self.xyz/docs/self-pass/architecture/zk-proof-architecture/ :::note **Who is this for?** This page covers the cryptographic design of Self Pass — registration flows, nullifier construction, certificate trees, and proof circuits. It's intended for protocol contributors, security auditors, and developers who want to understand what happens under the hood. You don't need this to integrate Self Pass — see [Key Concepts](/docs/self-pass/key-concepts/) instead. ::: ## High level Architecture Usage of the Self protocol involves two main steps: registration and disclosure. This design is somewhat similar to identity mixers such as Semaphore.
Registration flow: the mobile app reads the passport over NFC and generates a secret; the ZK proving service (TEE) verifies the data and DSC chain and derives a nullifier and commitment; the on-chain registry verifies the Groth16 proof and adds the commitment to the merkle tree
First, users prove they own a valid identity document (passport, EU ID card, Aadhaar, or KYC attestation) by generating a zero-knowledge proof of validity. This is done by proving the existence of a valid certificate authority chain for the user's document data for document-based attestations, while KYC attestations are produced through the KYC verification flow and then mapped into the same verification pipeline. This proof is generated in a TEE rather than on the user's mobile phone for performance reasons. To ensure that the TEE doesn't save or leak the user's personal information, the mobile app will connect to the TEE only after it verifies the TEE attestation which can verify the code that is running on the TEE.
Disclosure flow: the third-party web SDK shows a QR code, the mobile app scans it and sends encrypted passport data to the ZK proving service, which generates an action nullifier and disclosed data; the back-end server verifies the Groth16 proof, commitment merkle root, and nullifier
The resulting zk proof is then verified onchain and their identity commitment is added to the identity registry. The registry is in the form of a merkle tree, whereas the identity commitment is a hash of several of the passport’s key information points, along with a user-generated secret. We call the registry the identity pool. Subsequently, every time an application needs to check a user’s identity, the user can generate another zero-knowledge proof, the disclosure proof, that shows that they know how to reconstruct a commitment which is part of the identity pool. In particular, they need both the secret that was used when registering, and the passport data. In the proof, they are free to disclose information related to their identity using the DG1 information present in the commitment, while redacting any information they want to keep private. \ For example, such disclosure proofs can prove that a user is over a certain age, that they are a citizen of a certain country, that they are not a citizen of a set of countries, and that they are not on the OFAC list. These statements can be linked to an user's wallet address in the proof, which can be shared with third party applications or onchain protocols. Disclosure proofs can be requested by third party applications using the Web SDK. The goal of splitting registration and disclosure proving is two-fold: * First, it creates a level of indirection between registration and disclosure. As detailed below in the Nullifier section, a country storing all attestations or an attacker stealing an attestation could identify users with their nullifiers. Thus, such an attacker could see which users have registered, but thanks to this level of indirection, they would not be able to see which actions the user has carried out (ie the user's disclosure proofs). * Disclosure proofs are mainly Poseidon hashes, so they are cheap enough to be generated client side easily, whereas proving the passport validity involves much larger circuits. As mentioned above, proofs are currently generated on Secure Enclaves. Additionally, all proofs are relayed onchain using our relayer so users don’t have to manage gas. We’ll now detail some of the main design choices we made while building Self protocol. ### Nullifiers Self uses two nullifiers: attestation nullifiers and action nullifiers. Attestation nullifiers prevent the creation of multiple identities with one passport. Action nullifiers prevent performing an action multiple times with the same identity. #### Attestation Nullifiers Attestation nullifiers are derived by hashing the passport’s signed attributes (signed\_attr in the circuits) using poseidon. Signed attributes is the final message signed by the DSC certificate. It incorporates enough entropy from previous datagroups like DG2 (the photo) so that it’s not vulnerable to glossary attacks. We used to hash the final signature instead of signed\_attr, but signing signed\_attr is simpler and prevents any kind of malleability attack on ECDSA signatures. Because it’s derived deterministically from the passport’s passive attestation, an issuer keeping records of all signatures, or an attacker obtaining the passport’s attestation, can identify if a user registered. But thanks to the level of indirection between registration and disclosure, they can’t identify which actions the user took. In such an attack, the user and the issuer/attacker have exactly the same information, so it’s not possible to distinguish them easily without adding additional mechanisms like trust relying on biometrics. We explored designs such as keeping a salt in a TEE or a threshold secret using MPC and using an OPRF so salt attestation nullifiers. However, those solutions add assumptions on the absence of censorship, because losing access to this salt or preventing people from accessing the OPRF would prevent deduplication forever. This goes against our philosophy of keeping registration permissionless. We believe that knowing if the user registered is not very useful for attackers, as our goal is to host many applications, and given appropriate delays, the anonymity set can be large enough to guarantee privacy. Another possible path would have been to use Active Authentication when available (detailed below) to derive deterministic nullifiers that an attacker couldn’t derive with the passport’s passive attestation. This would have been done by having the private key embedded in the passport to sign a fixed, universal message that would always give the same nullifier. However, even when the Active Auth signatures use RSA, a salt is always added by the chip to the challenge signed, because the challenge is only 8 bytes long. This is done to prevent an attacker from bypassing Active Auth by presigning all challenges. This means in practice, Active Auth cannot be used to generate such a nullifier. #### Action Nullifiers Action nullifiers are derived by hashing the user’s secret along with a scope using Poseidon. The scope is a unique identifier of the application requesting a proof (i.e. an airdrop, or a website). The scope will be generated deterministically from the DNS of the application requesting the disclosure proof. This information will be verified in the mobile app to prevent applications from extracting a nullifier that the user has already used. ### Commitment During registration, the user’s commitment is added as a new leaf of the commitment merkle tree. This tree is deployed onchain and uses @zk-kit's[ lean-imt](https://github.com/privacy-scaling-explorations/zk-kit.solidity/tree/main/packages/lean-imt) library to ensure a scalable design. The commitment is a hash of several passport related data. Some of them are not used in this first version of the protocol, but as we want to build this privacy pool over the time and let the commitment structure untouched, we already include them. The values hashed to generate the commitment are: * `secret` only known by users, brings entropy and is used to generate `action nullifiers` * `dg1` the hash of the first DG, used for selective disclosure * `eContent` contains the hash of all the DG present in the passport. If we want to use D15 for active authentication or DG2 for zkML face matching we’ll just have to unfold eContent to use the DGs. * `DSC` certificate signing the passport. If one gets leaked, we will want to do a proof of non inclusion of a DSC hash. * `CSCA` certificate signing the DSC, recorded for the same reason. ### DSC and CSCA trees As mentioned above, verifying the validity of a passport involves two steps: checking it’s been signed by a DSC, and checking that the DSC has been signed by a CSCA. But proving those two steps for every registration would be expensive, and each DSC signs many passports. So every time we encounter a new DSC, we do a proof that it’s been signed by a CSCA and whitelist it by adding its hash to the DSC merkle tree. That way, at registration, users just have to prove that their passports has been signed by a certificate in the DSC tree. #### CSCA tree The CSCA tree is built from the ICAO masterlist registry using the scripts in[ /registry](https://github.com/zk-passport/openpassport/tree/main/registry). Each leaf is constructed as follows: ``` csca_leaf = poseidon2([csca_hash, csca_actual_length]) ``` white `csca_hash` being the raw CSCA padded with zeros to 1792 bytes, packed and hashed with poseidon. We pad certificates with 0s so we have a common length for them, as dynamic length poseidon hashing is hard to implement in circom. We chose 1792 because the longest certificate present in the masterlist is `1591` bytes long. We have to commit to the actual size of the certificate so when doing a proof, it’s not possible to point to the position of the certificate public key in the zero padding. Currently, the CSCA merkle tree root is managed by the owner account. We only update the root of the tree in a smart-contract, and give access to the whole tree offchain. The tree construction is fully auditable by running the scripts in[ /registry](https://github.com/zk-passport/openpassport/tree/main/registry). In the future, we can make sure the client rebuilds the CSCA tree to make sure it’s formed correctly, in the same way that we made proof verification transparent for the[ New American Primary](https://newdemocraticprimary.org/results). We also plan on decentralising the management of the CSCA tree in the future, most likely using multisigs or oracles. The size of the CSCA tree is set to 12, which represents more than 4k leaves. There’s currently \~500 CSCA present in the ICAO Masterlist. #### DSC tree At registration, users prove that their passport has been signed by a DSC present in the DSC merkle tree. Each leaf of this tree is computed as the following: ``` dsc_leaf = poseidon2([ poseidon2([dsc_hash, dsc_actual_length]) poseidon2([csca_hash, csca_actual_length]) // this is also csca_leaf ]) ``` Same as before, the `dsc_hash` is built by padding the raw DSC up to 1792 bytes. For technical reasons, the DSC is not just padded with 0s but with the sha padding, which includes a 128 bytes after the content and the actual size of the certificate after a certain number of blocks. This is because certificate signatures are checked with a dynamic sha in the `dsc.circom` circuit, so we pad the certificates outside the circuit. Adding leafs to this tree is permissionless, meanings that anyone proving that a given DSC has been signed by a CSCA can add the corresponding leaf. This tree is deployed onchain, and just like for the commitment merkle tree, we use `@zk-kit`'s[ lean-imt](https://github.com/privacy-scaling-explorations/zk-kit.solidity/tree/main/packages/lean-imt) library for scalability. The reason we keep information about the CSCA in those leafs is so that they can be passed to the commitment in the registration proofs. That way, we can keep track in commitments of which CSCA was responsible for this commitment being added, and if a CSCA is compromised, potentially blacklist all commitments generated from it. Each time a user scans their passport, the mobile application reads the DSC from the passport and checks its presence in the DSC merkle tree. If it’s not there, it sends the DSC to the TEE that will generate the DSC proof and relay it onchain. We set the maximal depth of the DSC tree to 21 in the circuits which allows having more than 2M leaves. As the onchain tree is incremental, if this value is exceeded (unlikely in the next 10 years), we’ll have to build new circuits. ### Secret Management and Recovery When registering an identity commitment, the user generates a secret. This prevents someone else that would access their passport from impersonating them, and brings the entropy necessary to generate application nullifiers that can’t be linked to passports. It also allows disclosure proofs to be done on the fly, instead of having to prove the whole passport validity every time. Just like with wallets, it’s important that users don’t lose access to their secret, otherwise they can’t generate disclosure proofs. We prevent that with multiple mechanisms: First, the secret is stored in the user’s keychain on their app. This allows the user to store it and retrieve it even if they uninstall and reinstall the app. Depending on whether the user uses iOS or Android, and has keychain cloud backups enabled, they can also retrieve it from other devices. When this is turned on by the user, this places a trust assumption on Apple/Google, that we think is acceptable for most users. We also let users store their secret themselves by showing them the BIP39 seed phrase associated with their secret, so that they can manage it themselves if they don’t trust Apple or Google. Second, we prompt users to do an extra backup on cloud services, which are iCloud for iOS and Google Drive for Android. This is similar to what many wallets do, and provide an extra level of backup with the same trust assumptions mentioned above. Cloud backups are less sensitive than wallet seed phrases, because passport data is still necessary to use the identity, whereas with wallets funds can be spent directly. Third, we designed the architecture so that it’s possible to add a recovery mechanism for identities. One central attack vector is that, while committing to a secret prevents someone else accessing the passport to do disclosure proofs, it also allows an attacker to register before the user if they get access to their passport. The way recovery could work is the following: the user initiates recovery by proving that they have a passport that corresponds to an existing nullifier, and after a delay, if the previous owner does not intervene, they get to replace the commitment corresponding to this nullifier with a new one with a new secret. This is not very practical with Passive Authentication, as passive attestations can be stored, but becomes way better when using Active Authentication, as being in physical possession of the passport is required, and a signature of a recent blockhash can be asked for, so it’s not possible for an attacker to access the passport once then initiate recovery over and over. More details on Active Authentication below. In this last case, recovering an identity lets someone register a new commitment with a new secret while disabling the previous one. So that users can't use both commitments to do disclosure proofs, we can keep track of commitments that were disabled in a list or a sparse merkle tree, and require disclosure proofs to prove their commitment is not part of this tree. It’s possible to add that to our design before our contracts are upgradeable and it wouldn’t have to change the format of the commitment, although new circuits and trusted setups would be required. Just like wallets do, we aim at being transparent in the app UI that backing up the secret is important, and that losing it entails not being able to do proofs with this identity document, at least before we ship a recovery mechanism for identities that lets people add commitment with a new secret. ### Trusted Setup We ran the trusted setup ceremony on [ceremony.pse.dev](https://ceremony.pse.dev/projects/Self%20ZK%20Passport%20Ceremony). Thanks to the p0tion team at PSE for making this happen! p0tion manages coordination between contributors by letting them connect with Github, get in a queue, and contribute to each circuit. It doesn’t add trust assumption, as contributors choose which hardware they want to use to contribute, and can add extra randomness to their toxic waste. In the past, we ran our own p0tion infra for the[ New Democratic Primary](https://newdemocraticprimary.org/results) we organized, but this time we got approval from PSE to run it on their p0tion infra, which simplifies things for us, for instance because it manages storing and distributing zkeys. p0tion also provides full auditability by publishing all intermediate zkeys and metadata regarding contributions, so anyone can verify the whole ceremony. Our trusted setup seems to be one of the largest to be done with Groth16, with around 38 circuits ranging from 143k to 10.6M circom constraints. We will continue running setups when adding support for more countries. ### Zero-knowledge proof stack We choose Circom with Groth16 for multiple reasons: * Because it’s the oldest stack, it’s been examined carefully and has been used in production in a variety of applications. More recent stacks like Noir are easier to start with, but have not been audited and are not ready for production use. * Fully succinct proofs with cheap onchain verification gas costs. * An optimized proving stack with[ https://github.com/0xPolygonID/witnesscalc](https://github.com/0xPolygonID/witnesscalc) and[ https://github.com/iden3/rapidsnark](https://github.com/iden3/rapidsnark), along with tooling like[ https://github.com/privacy-scaling-explorations/p0tion](https://github.com/privacy-scaling-explorations/p0tion), which yields fast proving time. The main downsides of Groth16 for us are the following: * Circuit-specific trusted setups * Large proving key For the next iterations of Self, we’re excited by newer proving stacks that break those tradeoffs. ### Timing Attack prevention The register and disclosure flow of the Self protocol resembles the Tornado Cash design. It functions as an identity mixer, and is susceptible to the same vulnerability known as a timing attack. Here's how it works: A user registers by generating a commitment and adding it to the tree. If they generate their first disclosure proof and verify it onchain shortly after, linking the disclosure proof to their user identifier, an observer monitoring chain activity can reasonably assume it's the same person. If this observer has access to passport data, either because it’s the issuing country or because it accessed it in the past, they link the person’s identity to their identifier by deriving their nullifier. For onchain applications, the user identifier is most of the time their address. There are two scenarios to consider here: 1. The disclosure proof is verified offchain by an application that does not publish proofs. In this case, the application would have to collude with an issuer or an attacker to link identities to their address. 2. The disclosure proof is verified onchain, or published by an application that verifies it offchain. In this case, if no delay has been introduced, it’s possible to look at which merkle root was used to do the disclosure proof, and infer that the person registered just before. Our main mitigation is to communicate clearly with the user when they could be exposed to a timing attack. When they first register, they are prompted not to do a disclosure proof right away, but to wait until a delay has passed. We can balance security and user experience by sending a notification to users after a random time that invites them to generate their disclosure proof. This random time interval can be chosen according to the number of registration happening, so as to keep the anonymity set reasonable. ### Active Authentication We currently use Passive Authentication, which involves verifying the attestation that issuers store on chips. More recently, new security mechanisms such as Active Authentication (AA) and Chip Authentication (CA) have been adopted by some countries. In both mechanisms, the passport’s chip is equipped with an embedded private key. In AA, the passport signs an 8 bytes challenge provided by the reader. The public key of the passport can be found in DG15, and the integrity of DG15 can be verified with the passive attestation. In CA, the passport and the reader perform DH key exchange and derive a symmetric key. The public key of the passport can be found in DG14. [Support for AA and CA varies by countries.](https://www.inverid.com/blog/cloning-detection-identity-documents) CA can’t be used to craft signatures because its all messages are repudiable, but AA can. Although we don’t support registration with Active Authentication currently, we designed Self so that it can be added easily in the future. The logic would be the following: * In the registration circuit, check if DG15 is present. * If it is not, register with the passive attestation. * If it is, the circuit requires the prover to provide a valid signature of a fixed message that matches the public key in DG15. To prevent issuers or attackers from pre-signing messages using AA when they have access to the passport, we can require the message to be a recent blockhash, for instance of the Ethereum blockchain. Assuming it’s infeasible to predict them and it’s impractical to extract private keys from secure chips, it would guarantee that the person registering is in physical possession of the passport. \\
Active Authentication registration: DG15 is hashed into eContent for the registration circuit, while the Active Auth public key, challenge, and signature feed a DG15 signature verification step
We are optimistic on the benefits of adding support for Active Authentication for the passports that support it, and we believe our architecture will requires little change to do so. ### Adding back client-side proving Up until recently, we used to use only client-side proving, but we recently switched to using Secure Enclaves. This is due both to the compute requirements of proving for some signature algorithms such as ECDSA, and proving key size. However, we devised a way to add back enough client-side proving so that there would be no trust assumption on Secure Enclaves for privacy. We can still offload some of the computation to a server/trusted enclave by only leaking which DSC was used to sign the passport. The way we can it is the following: * For registration proofs, we split the process between a small proof generated client side that just hashes DG1, and a larger proof generated server-side that includes the rest of the registration logic. Both proofs output a blinded commitment of DG1 that can be checked to make sure they refer to the same passport. * For disclosure proofs, they are very light so they can be generated locally. We estimate the maximal size of the proving key for this new registration circuit to be around 30mb zipped, which should be manageable for most devices, even with poor bandwidth. --- # Verification in the IdentityVerificationHub (Legacy) Source: https://docs.self.xyz/docs/self-pass/architecture/verification-hub/ :::note **Who is this for?** This page explains the internal verification flow of the on-chain IdentityVerificationHub contract — how proofs are decoded, routed, and verified. It's useful if you're building custom verification hooks or need to understand the on-chain data structures. For basic smart contract integration, see [Smart Contract Integration](/docs/self-pass/contracts/basic-integration/). ::: The IdentityVerificationHub V2 is the core verification engine that processes zero-knowledge proofs and executes identity verification workflows. ## How the Hub Works The Hub operates as a central verification coordinator that: * **Receives Verification Requests** from contracts implementing `ISelfVerificationRoot` * **Processes ZK Proofs** using specialized circuit verifiers * **Applies Verification Rules** based on stored configurations * **Returns Structured Results** to the calling contract ## Complete Verification Flow ### 1. Request Initiation ```solidity // User contract calls Hub with proof data function verifySelfProof(bytes calldata proofPayload, bytes calldata userContextData) external; ``` **What happens:** #### TEE Proof Generation (Gas-Free for Users) * **User Input**: User completes a supported document flow in the mobile app (passport, ID, Aadhaar, or KYC) * **TEE Processing**: Trusted Execution Environment securely processes identity data * **ZK Proof Creation**: TEE generates zero-knowledge proof without revealing raw identity data #### Relayer (Sponsored Transactions) * **Proof Relay**: Relayer receives ZK proof from TEE * **Gas Sponsorship**: Relayer pays all transaction gas fees on behalf of user * **Onchain Submission**: Relayer submits proof to user's contract via blockchain transaction * **User Experience**: User gets verified identity without any crypto/gas requirements #### Contract Processing * Contract receives ZK proof from TEE/Relayer * Calls Hub's `verifySelfProof` with proof + user context data * Hub begins processing the verification request ### 2. Input Decoding & Context Processing ```solidity // Hub internal: _decodeInput() and _decodeUserContextData() (HubInputHeader memory header, bytes calldata proofData) = _decodeInput(baseVerificationInput); (configId, destChainId, userIdentifier, remainingData) = _decodeUserContextData(userContextData); ``` **What happens:** * **Header Extraction**: Gets contract version, scope, attestation ID * **Context Parsing**: Extracts config ID, destination chain, user identifier * **Data Preparation**: Prepares proof data for verification ### 3. Configuration Retrieval ```solidity // Hub loads verification configuration by configId VerificationConfigV2 memory config = $v2._v2VerificationConfigs[configId]; ``` **What happens:** * Hub looks up stored verification configuration using configId * Configuration contains all verification rules (age, countries, OFAC, etc.) * If config doesn't exist, verification fails ### 4. Document Type Detection & Routing ```solidity // Based on attestationId in header if (attestationId == AttestationId.E_PASSPORT) { // Route to passport verification logic } else if (attestationId == AttestationId.EU_ID_CARD) { // Route to EU ID card verification logic } else if (attestationId == AttestationId.AADHAAR) { // Route to Aadhaar verification logic } else if (attestationId == AttestationId.KYC) { // Route to KYC verification logic } ``` **What happens:** * Hub identifies document type from attestation ID * Routes to appropriate verification pipeline * Uses document-specific circuit verifiers ### 5. Basic Verification (\_basicVerification) **Purpose:** Validates the **cryptographic correctness** and **security** of the ZK proof itself. ```solidity // Hub performs 4 core verification steps bytes memory proofOutput = _basicVerification( header, vcAndDiscloseProof, userContextData, userIdentifier ); ``` **What happens (4 verification scopes):** #### Scope 1: Contract Validation * **Scope Check**: Ensures proof was generated for the correct contract * **User Identifier Check**: Validates user identity consistency #### Scope 2: Registry & Timestamp Validation * **Root Check**: Validates against Merkle tree in identity registry * **Current Date Check**: Ensures proof is within valid time window #### Scope 3: Cryptographic Proof Verification * **Groth16 Proof Verification**: Validates ZK proof using circuit verifier * **Public Signals Validation**: Verifies proof inputs/outputs match expectations #### Scope 4: Raw Data Extraction * **Output Generation**: Creates `PassportOutput` or `EuIdOutput` with raw field data * **Data Preparation**: Prepares extracted data for business logic verification **Output:** Raw identity data (`PassportOutput`/`EuIdOutput`) ready for custom verification. ### 6. Custom Verification (CustomVerifier.customVerify) **Purpose:** Applies **business logic rules** and validates **identity attributes** against configuration requirements. ```solidity // Hub applies custom verification logic GenericDiscloseOutputV2 memory output = CustomVerifier.customVerify( header.attestationId, config, proofOutput ); ``` **What happens:** * **Document Type Routing**: Routes to passport, ID card, Aadhaar, or KYC-specific verification * **Business Rule Application**: Applies age, geographic, and sanctions requirements * **Identity Data Extraction**: Converts raw data to structured, human-readable format * **Final Validation**: Ensures all configuration requirements are met **Output:** Structured identity data (`GenericDiscloseOutputV2`) with verification results. #### Age Verification (olderThanEnabled) ```solidity if (verificationConfig.olderThanEnabled) { if (!CircuitAttributeHandlerV2.compareOlderThan( attestationId, passportOutput.revealedDataPacked, verificationConfig.olderThan )) { revert InvalidOlderThan(); } } ``` * Validates user meets minimum age requirement * Uses circuit-extracted age data for verification * Example: config requires 18+, user is 20 → ✅ passes * Example: config requires 21+, user is 18 → ❌ fails #### Geographic Restrictions (forbiddenCountriesEnabled) ```solidity if (verificationConfig.forbiddenCountriesEnabled) { for (uint256 i = 0; i < 4; i++) { if (passportOutput.forbiddenCountriesListPacked[i] != verificationConfig.forbiddenCountriesListPacked[i]) { revert InvalidForbiddenCountries(); } } } ``` * Validates forbidden countries list matches exactly * Uses packed representation for gas efficiency (4 uint256 array) * Order in config must match proof's forbidden countries list #### OFAC Sanctions Verification ```solidity if (verificationConfig.ofacEnabled[0] || verificationConfig.ofacEnabled[1] || verificationConfig.ofacEnabled[2]) { if (!CircuitAttributeHandlerV2.compareOfac( attestationId, passportOutput.revealedDataPacked, verificationConfig.ofacEnabled[0], // passport number verificationConfig.ofacEnabled[1], // name + DOB verificationConfig.ofacEnabled[2] // name + YOB )) { revert InvalidOfacCheck(); } } ``` * **Mode 0**: OFAC check using passport number * **Mode 1**: OFAC check using name + date of birth * **Mode 2**: OFAC check using name + year of birth * Each mode can be independently enabled/disabled * Uses circuit-provided OFAC verification results ### 7. Output Formatting & Generation ```solidity // Hub formats verification results into structured output output = _formatVerificationOutput(header.contractVersion, genericDiscloseOutput); ``` **What happens:** * Raw proof signals converted to human-readable data * Structured identity information extracted * Verification results (age, OFAC, etc.) included ### 8. Result Delivery ```solidity // Hub calls back to the original contract ISelfVerificationRoot(callingContract).onVerificationSuccess( abi.encode(output), userData ); ``` **What happens:** * Hub calls `onVerificationSuccess` on the requesting contract * Passes structured output + user-defined data * Contract can then execute its custom business logic ## Data Structures ### VerificationConfigV2 ```solidity struct VerificationConfigV2 { bool olderThanEnabled; // Enable age verification uint256 olderThan; // Minimum age requirement bool forbiddenCountriesEnabled; // Enable country restrictions uint256[4] forbiddenCountriesListPacked; // Packed forbidden countries bool[3] ofacEnabled; // OFAC verification modes } ``` ### GenericDiscloseOutputV2 (Verification Result) ```solidity struct GenericDiscloseOutputV2 { bytes32 attestationId; // E_PASSPORT, EU_ID_CARD, AADHAAR, or KYC uint256 userIdentifier; // User's unique identifier uint256 nullifier; // Anti-replay nullifier uint256[4] forbiddenCountriesListPacked; // Forbidden countries used // Disclosed identity information string issuingState; // Document issuing country string[] name; // [first, middle, last] names string idNumber; // Passport/ID number string nationality; // User's nationality string dateOfBirth; // Birth date (DD-MM-YY) string gender; // User's gender string expiryDate; // Document expiry date // Verification results uint256 olderThan; // Verified minimum age bool[3] ofac; // OFAC results [passport number, name+dob, name+yob] } ``` ## Key V2 Improvements ### Multi-Document Support * Automatic routing for attestation types: E\_PASSPORT, EU\_ID\_CARD, AADHAAR and KYC * Document-specific verification pipelines * Unified interface for different document types ### Structured Output * Rich `GenericDiscloseOutputV2` with pre-extracted attributes * No more manual parsing of raw field elements * Type-safe access to identity data ### Flexible Configuration Management * Reusable `VerificationConfigV2` stored in Hub * Create configurations without contract redeployment * Use [Self Configuration Tools](https://tools.self.xyz/) to register configs ### Enhanced Security * Separate verification logic for each document type * Improved timestamp and replay protection * Gas-optimized verification checks --- # OFAC & CSCA Auto-Updaters (Legacy) Source: https://docs.self.xyz/docs/self-pass/architecture/ofac-csca-auto-updaters/ Self Pass relies on two automated systems to keep verification data current: the **OFAC sanctions list updater** and the **CSCA certificate tree updater**. These run automatically and require no action from developers integrating Self Pass. ## OFAC Sanctions List Updater The OFAC (Office of Foreign Assets Control) auto-updater keeps the on-chain sanctions Merkle tree synchronized with the latest US Treasury OFAC sanctions data. This enables Self Pass to perform real-time sanctions screening as part of the zero-knowledge proof generation process. **How it works:** 1. The updater fetches the latest OFAC sanctions data from the Self API 2. Three separate Merkle trees are maintained for different matching strategies: * **Passport number + nationality** — exact document match * **Name + date of birth** — biographical match * **Name + year of birth** — broader biographical match 3. The updated Merkle roots are submitted on-chain to the registry contract 4. When users generate proofs with OFAC enabled, the circuit verifies non-inclusion in these trees ID card verification uses a subset of these trees (skipping the passport-number tree). ## CSCA Certificate Tree Updater The CSCA (Country Signing Certificate Authority) auto-updater maintains the Merkle tree of trusted certificate authority public keys used to validate passport signatures. **How it works:** 1. The updater fetches the latest certificates from the ICAO (International Civil Aviation Organization) masterlist 2. Public keys are extracted and validated against known Subject Key Identifiers (SKIs) 3. A Merkle tree is constructed from the validated public keys (~500 CSCAs, tree depth 12) 4. The updated root is submitted on-chain This ensures that newly issued or rotated country signing certificates (which typically cycle every 3-5 years) are recognized by the verification system. The tree supports permissionless leaf addition, meaning new certificates can be added by anyone. ## For Developers These systems are transparent to developers integrating Self Pass. When you enable OFAC checking in your disclosure configuration (`ofac: true`), the proof generation and verification automatically use the latest sanctions data. Similarly, passport signature verification always uses the most current CSCA tree. The updater scripts live in the [Self Protocol contracts repository](https://github.com/selfxyz/self). --- # Verification Result (Legacy) Source: https://docs.self.xyz/docs/self-pass/architecture/self-attestation/ The `VerificationResult` type is the response returned by `SelfBackendVerifier.verify()` from `@selfxyz/core`. It contains the verification status, disclosed identity attributes, and user context data. ## VerificationResult ```typescript type VerificationResult = { attestationId: AttestationId; isValidDetails: { isValid: boolean; isMinimumAgeValid: boolean; isOfacValid: boolean; }; forbiddenCountriesList: string[]; discloseOutput: GenericDiscloseOutput; userData: { userIdentifier: string; userDefinedData: string; }; }; ``` ### Fields | Field | Type | Description | | ----- | ---- | ----------- | | `attestationId` | `AttestationId` | Identifier for the attestation type (passport, national ID, etc.) | | `isValidDetails.isValid` | `boolean` | Whether the ZK proof verified successfully on-chain | | `isValidDetails.isMinimumAgeValid` | `boolean` | Whether the user meets the configured minimum age requirement | | `isValidDetails.isOfacValid` | `boolean` | Whether the user passed the OFAC sanctions check | | `forbiddenCountriesList` | `string[]` | Country codes unpacked from the proof's public signals | | `discloseOutput` | `GenericDiscloseOutput` | Disclosed identity attributes from the ZK circuit | | `userData.userIdentifier` | `string` | User identifier extracted from the verification context | | `userData.userDefinedData` | `string` | Application-defined data passed through the verification flow | ## GenericDiscloseOutput The disclosed identity attributes requested during verification. Only fields that were requested in the disclosure configuration will contain meaningful values. ```typescript type GenericDiscloseOutput = { nullifier: string; forbiddenCountriesListPacked: string[]; issuingState: string; name: string; idNumber: string; nationality: string; dateOfBirth: string; gender: string; expiryDate: string; minimumAge: string; ofac: boolean[]; }; ``` ### Fields | Field | Type | Description | | ----- | ---- | ----------- | | `nullifier` | `string` | Unique nullifier derived from the proof (prevents double-use) | | `forbiddenCountriesListPacked` | `string[]` | Packed representation of forbidden countries from the circuit | | `issuingState` | `string` | Three-letter code of the document's issuing country | | `name` | `string` | Full name from the identity document | | `idNumber` | `string` | Document number | | `nationality` | `string` | Three-letter nationality code | | `dateOfBirth` | `string` | Date of birth | | `gender` | `string` | Gender as recorded on the document | | `expiryDate` | `string` | Document expiry date | | `minimumAge` | `string` | The verified minimum age value | | `ofac` | `boolean[]` | OFAC check results | ## VerificationConfig The configuration passed to `SelfBackendVerifier` to specify what checks to perform. ```typescript type VerificationConfig = { minimumAge?: number; excludedCountries?: Country3LetterCode[]; ofac?: boolean; }; ``` | Field | Type | Description | | ----- | ---- | ----------- | | `minimumAge` | `number` (optional) | Minimum age requirement. Verification fails if the user is younger. | | `excludedCountries` | `Country3LetterCode[]` (optional) | List of three-letter country codes to exclude | | `ofac` | `boolean` (optional) | Whether to check the user against the OFAC sanctions list | --- # Deployments (Legacy) Source: https://docs.self.xyz/docs/self-pass/architecture/deployments/ Deployment addresses for the Self protocol, on Celo mainnet. ## Core protocol
ContractDeployment address
IdentityRegistry0x37F5CB8cB1f6B00aa768D8aA99F1A9289802A968
IdentityRegistryImplV10xC473d5F784e424A70Bf7aCf887E33448E64F8798
IdentityVerificationHub0x77117D60eaB7C044e785D68edB6C7E0e134970Ea
IdentityVerificationHubImplV10x85FD004B2312a6703F1ce293242Dc15B719772b1
## Helpers
ContractDeployment address
PoseidonT30xF134707a4C4a3a76b8410fC0294d620A7c341581
VerifyAll0xe6D61680A6ED381bb5A0dB5cF4E9Cc933cF43915
## Proof verifiers | Contract | Deployment address | | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | Verifier\_vc\_and\_disclose | [0x44d314c2F9b3690735808d26d17dFCc9F906A9B4](https://celoscan.io/address/0x44d314c2F9b3690735808d26d17dFCc9F906A9B4) | | Verifier\_dsc\_sha1\_ecdsa\_brainpoolP256r1 | [0xE7B4A70fc1d96D3Fb6577206c932eF1e634Cf2d0](https://celoscan.io/address/0xE7B4A70fc1d96D3Fb6577206c932eF1e634Cf2d0) | | Verifier\_dsc\_sha1\_rsa\_65537\_4096 | [0x19E25a5772df0D7D6Db59D94a4d6FBd7098a3012](https://celoscan.io/address/0x19E25a5772df0D7D6Db59D94a4d6FBd7098a3012) | | Verifier\_dsc\_sha256\_ecdsa\_brainpoolP256r1 | [0x1F3afAe85992B1B8CF6946B091225dAF8307675d](https://celoscan.io/address/0x1F3afAe85992B1B8CF6946B091225dAF8307675d) | | Verifier\_dsc\_sha256\_ecdsa\_brainpoolP384r1 | [0x52A6EF39655D662A8Cf8eB56CD853883fe43eb2b](https://celoscan.io/address/0x52A6EF39655D662A8Cf8eB56CD853883fe43eb2b) | | Verifier\_dsc\_sha256\_ecdsa\_secp256r1 | [0x643735Cd44F8b2BDa47b4a7962c8BDf12E6CDdf8](https://celoscan.io/address/0x643735Cd44F8b2BDa47b4a7962c8BDf12E6CDdf8) | | Verifier\_dsc\_sha256\_ecdsa\_secp384r1 | [0x00F0D1A32Def293DAB78100A6569ebb4EC035F82](https://celoscan.io/address/0x00F0D1A32Def293DAB78100A6569ebb4EC035F82) | | Verifier\_dsc\_sha256\_rsa\_65537\_4096 | [0x711e655c43410fB985c4EDB48E9bCBdDb770368d](https://celoscan.io/address/0x711e655c43410fB985c4EDB48E9bCBdDb770368d) | | Verifier\_dsc\_sha256\_rsapss\_3\_32\_3072 | [0xDAFF470e561F3f96C7410AeF02196913E981fF1B](https://celoscan.io/address/0xDAFF470e561F3f96C7410AeF02196913E981fF1B) | | Verifier\_dsc\_sha256\_rsapss\_65537\_32\_3072 | [0x07B6C2FFB098B131eAD104396d399177014ae15f](https://celoscan.io/address/0x07B6C2FFB098B131eAD104396d399177014ae15f) | | Verifier\_dsc\_sha256\_rsapss\_65537\_32\_4096 | [0xFBDDADb864b24B2c4336081A22f41D04E7b35DA9](https://celoscan.io/address/0xFBDDADb864b24B2c4336081A22f41D04E7b35DA9) | | Verifier\_dsc\_sha384\_ecdsa\_brainpoolP384r1 | [0x6a40dfa6f99FA178aB6cc88928Bf30661e917A76](https://celoscan.io/address/0x6a40dfa6f99FA178aB6cc88928Bf30661e917A76) | | Verifier\_dsc\_sha384\_ecdsa\_secp384r1 | [0x1719430107E66717d8b34d4190838dfABAf810e6](https://celoscan.io/address/0x1719430107E66717d8b34d4190838dfABAf810e6) | | Verifier\_dsc\_sha512\_rsa\_65537\_4096 | [0xf5eE920d6D50a8A83C22f548bf406fCBcD558751](https://celoscan.io/address/0xf5eE920d6D50a8A83C22f548bf406fCBcD558751) | | Verifier\_dsc\_sha512\_rsapss\_65537\_64\_4096 | [0x5438C4ebFD8Fcce6eb54542e3A5C192B22227f70](https://celoscan.io/address/0x5438C4ebFD8Fcce6eb54542e3A5C192B22227f70) | | Verifier\_register\_sha1\_sha1\_sha1\_ecdsa\_brainpoolP224r1 | [0x8588e473428cf415F10AC96CAa701F6Cd1C8641F](https://celoscan.io/address/0x8588e473428cf415F10AC96CAa701F6Cd1C8641F) | | Verifier\_register\_sha1\_sha1\_sha1\_rsa\_65537\_4096 | [0x15fd0d58cfF9DaA4A60105c0DAC73659530BB7f7](https://celoscan.io/address/0x15fd0d58cfF9DaA4A60105c0DAC73659530BB7f7) | | Verifier\_register\_sha1\_sha256\_sha256\_rsa\_65537\_4096 | [0xaC5166A01Aee75A10703177896122F4d6e3836d1](https://celoscan.io/address/0xaC5166A01Aee75A10703177896122F4d6e3836d1) | | Verifier\_register\_sha224\_sha224\_sha224\_ecdsa\_brainpoolP224r1 | [0x7d9b7D2A95541b50CECDB44d82c0570a818111Ac](https://celoscan.io/address/0x7d9b7D2A95541b50CECDB44d82c0570a818111Ac) | | Verifier\_register\_sha256\_sha224\_sha224\_ecdsa\_secp224r1 | [0x48cEc90de8d746efD316968Ea65417e74C6A1a74](https://celoscan.io/address/0x48cEc90de8d746efD316968Ea65417e74C6A1a74) | | Verifier\_register\_sha256\_sha256\_sha256\_ecdsa\_brainpoolP256r1 | [0x9C5Af0FC9A32b457e300905929A05356D3C0DB25](https://celoscan.io/address/0x9C5Af0FC9A32b457e300905929A05356D3C0DB25) | | Verifier\_register\_sha256\_sha256\_sha256\_ecdsa\_brainpoolP384r1 | [0x5286E20745A0d4C35E6D97832D56e30A28303BD6](https://celoscan.io/address/0x5286E20745A0d4C35E6D97832D56e30A28303BD6) | | Verifier\_register\_sha256\_sha256\_sha256\_ecdsa\_secp256r1 | [0xaC861bf9FC8B44ccbAde8E2A39C851bbCf38c392](https://celoscan.io/address/0xaC861bf9FC8B44ccbAde8E2A39C851bbCf38c392) | | Verifier\_register\_sha256\_sha256\_sha256\_ecdsa\_secp384r1 | [0x03FCc979cf2d69275647095E4079A3389F24525D](https://celoscan.io/address/0x03FCc979cf2d69275647095E4079A3389F24525D) | | Verifier\_register\_sha256\_sha256\_sha256\_rsa\_3\_4096 | [0xbE036B26317F013D2c6cB092Aa1fa903220be846](https://celoscan.io/address/0xbE036B26317F013D2c6cB092Aa1fa903220be846) | | Verifier\_register\_sha256\_sha256\_sha256\_rsa\_65537\_4096 | [0xE80537B3399bd405e40136D08e24c250397c09F1](https://celoscan.io/address/0xE80537B3399bd405e40136D08e24c250397c09F1) | | Verifier\_register\_sha256\_sha256\_sha256\_rsapss\_3\_32\_2048 | [0xe063BD3188341B2D17d96cE38FD31584147d3219](https://celoscan.io/address/0xe063BD3188341B2D17d96cE38FD31584147d3219) | | Verifier\_register\_sha256\_sha256\_sha256\_rsapss\_65537\_32\_2048 | [0xe93Be9382868f30150cAF77793aF384905c2C7E4](https://celoscan.io/address/0xe93Be9382868f30150cAF77793aF384905c2C7E4) | | Verifier\_register\_sha256\_sha256\_sha256\_rsapss\_65537\_32\_3072 | [0xD39E5eAfb6d266E3c4AC8255578F23a514fd8B36](https://celoscan.io/address/0xD39E5eAfb6d266E3c4AC8255578F23a514fd8B36) | | Verifier\_register\_sha384\_sha384\_sha384\_ecdsa\_brainpoolP384r1 | [0xd2F65a76A10f5E0e7aE9d18826ab463f4CEb33C9](https://celoscan.io/address/0xd2F65a76A10f5E0e7aE9d18826ab463f4CEb33C9) | | Verifier\_register\_sha384\_sha384\_sha384\_ecdsa\_secp384r1 | [0xC33E6A04b7296A3062Cf438C33dc8D8157c3916d](https://celoscan.io/address/0xC33E6A04b7296A3062Cf438C33dc8D8157c3916d) | | Verifier\_register\_sha384\_sha384\_sha384\_rsapss\_65537\_48\_2048 | [0xa7A5A581C2Eb8dF39f486e9ABBc4898546D70C3e](https://celoscan.io/address/0xa7A5A581C2Eb8dF39f486e9ABBc4898546D70C3e) | | Verifier\_register\_sha512\_sha512\_sha512\_rsa\_65537\_4096 | [0x6C88A6Afc38cA2859e157532b1b872EcC1ED0424](https://celoscan.io/address/0x6C88A6Afc38cA2859e157532b1b872EcC1ED0424) | | Verifier\_register\_sha512\_sha512\_sha512\_rsapss\_65537\_64\_2048 | [0x04A1D0d51Bc078CB137088424b2ec569699dd7A5](https://celoscan.io/address/0x04A1D0d51Bc078CB137088424b2ec569699dd7A5) |