# Private Identity Verification

Human ID is a privacy-preserving identity protocol developed by Holonym Foundation for the [human.tech](https://human.tech/) framework. Human ID uses zero knowledge proofs to allow for proving facts about oneself without revealing the whole identity, enabling organizations and smart contracts to verify their users without storing any sensitive information.

<table data-view="cards"><thead><tr><th></th><th data-hidden data-card-cover data-type="files"></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td>Explore the human.tech framework</td><td><a href="/files/0KmzPjlHC3Vn1h9u77cZ">/files/0KmzPjlHC3Vn1h9u77cZ</a></td><td><a href="https://docs.human.tech/">https://docs.human.tech/</a></td></tr></tbody></table>

## Motivation

Identity verification is often required for legal or practical reasons such as KYC, fraud prevention, proof-of-age, and Sybil resistance. However, verifying identity while also preserving privacy has historically been difficult. This problem is even more prominent in web3, where data is stored on a public ledger. Not only is obtaining privacy on a blockcchain difficult, but the adverse affects of losing privacy are greater: "doxxing" a user's wallet address once reveals all of the address' past and future activity.

Human ID is a private credential system part of the human.tech tool-set that makes privacy-preserving identity possible and simple for both on- and off-chain use cases.

## Privacy-preserving proofs

With a Human ID, user can prove a variety of statements without revealing their identity. These include facts needed for meeting legal requirements, Sybil resistance to prevent bot attacks, and account or key recovery. Example statements are:

> "I have US residency"
>
> "I have non-US residency"
>
> "I have never received this airdrop from any other crypto address"
>
> "I am an accredited investor"
>
> "I am over 18"
>
> "I am the same person who created this wallet" if the wallet needs recovery
>
> "I have never voted in this DAO's governance"

{% hint style="success" %}
Many of these are ready to use out-of-the-box, and others can be enabled by [contacting us](mailto:hello@holonym.id) and/or contributing to our [GitHub](https://github.com/holonym-foundation)
{% endhint %}

{% hint style="info" %}
**Note**: Proof generation currently takes about 30ms-4s on standard consumer devices, depending on the proof type and the device's specifications.&#x20;
{% endhint %}

To start using Human ID in your (d)App, continue to

{% content-ref url="/pages/0fHiLJGaCcklYcnGZ93N" %}
[Integrating Human ID](/for-developers/start-here)
{% endcontent-ref %}

Otherwise, take a peak into the architecture of human.tech

{% content-ref url="/pages/rcXcgW5zvwwqWFr6QMYj" %}
[Overview](/architecture/overview)
{% endcontent-ref %}

Continue on to learn about programmable privacy for meeting legal requirements across compliance regimes,


# The Issue of Regulatory Compliance & Non-Consensual Data Sharing

While Zero Knowledge Proofs are helpful, they aren't enough...

Zero-knowledge KYC prevents data honeypots and massive surveillance. However, on its own, it isn't sufficient for cases where regulators or law enforcement need to investigate users suspected of criminal activity.

As a result, maintaining honeypots has become a necessary compliance practice: organizations store sensitive data that is frequently breached, just to make sure it remains available for investigations.

## Solving The Dilemma: Proof of Clean Hands With Human Network

While ZK proofs (ZKP) aren't trivial, ZKPs with compliance are even more complex. Holonym has built [Human Network](https://x.com/mishtinetwork), an AVS on EigenLayer that allows provable threshold encryption, to unlock ZKPs with programmable privacy for selective and consensual disclosure. The network allows developers to hard-code policies that require pre-existing conditions to occur before a third party can decrypt user data. Users can verify and encrypt their data to the Network's key to create ZKPs which can be "decrypted" by pre-approved parties only when a smart contract is triggered.&#x20;

While this function can be used to gate any sort of data behind programmable policies, it is especially useful for provable encryption of identity.

Human ID packages this functionality into a very easy to use solution called [**Proof of Clean Hands**](https://human.tech/blog/proof-of-clean-hands) that serves as a general-purpose tool for meeting compliance requirements.&#x20;

Once a user has a proof-of-clean-hands SBT (soul bound token), we know the data is encrypted to an observer that can only decrypt a small amount of data per day, making surveillance or honeypots impossible while enabling compliance with law enforcement and regulators.

For more information, see the architecture:

{% content-ref url="/pages/79lVhax7CoMI0KeE1a1r" %}
[Clean Hands Architecture](/architecture/clean-hands)
{% endcontent-ref %}

Or usage:

{% content-ref url="/pages/dVx5xlPVpaqx4HrJys8p" %}
[Clean Hands Attestations](/for-developers/clean-hands)
{% endcontent-ref %}


# Need Support?

If you need support with using the Human ID application, please follow the instructions on this page

If you need support when verifying your identity using Human ID, please follow the steps below:

1. Review or search the contents of this site, especially our [Frequently Asked Questions (FAQs)](/for-users/faqs).
2. ***Click on the support icon in the bottom right corner of this page*** — or in the Human ID app — to get in contact with a support agent.

<figure><img src="/files/kLx2YlQgpKl96nw3NTit" alt="" width="375"><figcaption><p>Please click the intercom button to access our support team</p></figcaption></figure>

Thank you!

[<br>](https://support.passport.xyz/passport-knowledge-base/partner-campaigns/shape-stack-proof-of-humanity)


# FAQs

You got questions? We got answers!

<details>

<summary>What is human.tech?</summary>

human.tech is a technology platform that enhances the security, transparency, and compliance of digital interactions through advanced cryptographic techniques and decentralized networks.

</details>

<details>

<summary>How does human.tech's technology work?</summary>

human.tech leverages blockchain technology to create secure, immutable records of transactions and digital identities to ensure data integrity and privacy.

</details>

<details>

<summary>What are human.tech's main products?</summary>

* **Human ID:** A private digital identity solution that verifies users without storing or seeing sensitive information. Data stays on user devices, and only proofs of identity and compliance are submitted.
* **Human Network:** Provides decentralized primitives for a better web. It enables privacy-preserving KYC with AML, and supports account login and recovery based on passwords and privacy-preserving biometrics.
* **Human Wallet:** A secure, noncustodial onboarding solution that resembles a wallet as a service but is self-custodial, free, and addresses security issues inherent in embedded wallets. It uses the Human Network for secure onboarding and recovery.
* **Human Passport:** An identity verification application and Sybil resistance protocol with more than 2M users. It enables users to collect verifiable credentials, or Stamps, that prove their identity and trustworthiness without exposing personally identifying information. To date, Human Passport has protected over $225M in airdrop and grant funds.

</details>

<details>

<summary>How does Human ID ensure data security?</summary>

Human ID uses advanced cryptographic techniques to protect data from unauthorized access and tampering, ensuring only authorized parties can access sensitive information.

</details>

<details>

<summary>What measures does Human ID take to protect user privacy?</summary>

Human ID technology keeps data on user devices, submitting only proofs of compliance and identity. This approach ensures that sensitive information is never exposed or stored centrally.

</details>

<details>

<summary>What is ZK KYC? How is it different from KYC?</summary>

When most people hear KYC, they think “loss of privacy”, “centralization”, and “getting doxxed”. Holonym uses “Zero Knowledge” KYC that prevents any identity authority or third party (including us) from linking your credentials with your wallet address.

This means that even if a third party KYC provider snoops on your data, there is no way to know which person you are on-chain.\\

</details>

<details>

<summary>How does Human ID help with regulatory compliance?</summary>

Human ID automates compliance processes through real-time transaction monitoring and smart contracts, reducing administrative burdens and ensuring adherence to regulatory standards.

</details>

<details>

<summary>What specific regulatory standards does Human ID support?</summary>

Human ID supports various regulatory standards by providing privacy-preserving KYC and AML capabilities, facilitating easier compliance with financial regulations.

</details>

<details>

<summary>What should I do if my wallet was hacked?</summary>

We’re sorry to hear about your situation. Unfortunately, we cannot change your wallet address, as we do not support proof revocation. You will need to wait until your verification expires in one year before you can verify again. You may proceed with a refund then.

</details>

<details>

<summary>Where can I find the proof contracts?</summary>

You can find a complete list of ZK-SBT contracts supported by Holonym here: [https://github.com/holonym-foundation/holonym-relayer/blob/main/constants/contract-addresses.json\ <br>](https://github.com/holonym-foundation/holonym-relayer/blob/main/constants/contract-addresses.json)Just go to <https://optimistic.etherscan.io/> and search the contract address to see more information.

</details>

<details>

<summary>I minted my ZK SBT to the wrong address! Can I transfer it?</summary>

It is not possible to transfer SBTs at this time because it breaks Sybil Defense with the current implementation.

</details>

<details>

<summary>How does Human ID guarantee privacy for me?</summary>

Human ID is designed to minimize any chance of data leakage. For any form of document-based verification, some agent has to assess a document and check it against a large centralized database. Almost all KYC providers in Web3 keep a centralized database that can be linked to an individual wallet address (see FTX disclosure).\
\
Holonym allows users to prove on-chain facts about themselves while making it highly difficult, near impossible, to link a real world document to an on-chain identity. Not even we can link your data to your wallet.

**How is this possible?**

1. During verification, all data is sent over an encrypted channel to a third party credential issuer.
2. We request deletion of the data by the third party as soon as it is successfully verified and handed back to the user. In the event document is not successfully verified, the record is logged and kept for up to 90 days to help users debug why their verification failed. It is deleted after the 90 day period.
3. We use a nullifier scheme to make it nearly impossible for the third party issuer to track the user after they verify them. This means that the user encrypts the cryptographically signed credential with a secret only they know after receiving it from the identity issuer.
4. We use a relayer to add a user’s leaf to the Merkle Tree so the verified credentials (VCs) can’t be linked to an on-chain address. The Merkle Tree is an identity mixer, or anonymity set, that the user can prove set membership and knowledge of specific attributes about from a pseudonymous ethereum address.
5. The user’s identity data is stored on the user’s local device. All ZK proofs on identity are computed on the client side, not our server.
6. We keep an encrypted backup of user data in the event that the user clears their cookies, local storage, or switched devices. The data is encrypted with the user’s ethereum key and can be requested to be deleted at any time by contacting the team.
7. Users can increase their privacy by using:

   \-- a VPN network (be careful to use the same country when verifying)

   \-- a privacy preserving browser like Tor\
   \-- waiting some period of time after adding their leaf to the merkle tree before minting their SBT

</details>

<details>

<summary>How many points do I earn for verifying my identity?</summary>

You’ll earn 16 points for KYC verification and 1.5 points for verifying your phone number.

</details>

<details>

<summary>Is there a specific age limit for completing ID verification with Human ID?</summary>

Yes, you need to be at least 18 years old.

</details>

<details>

<summary>My ID verification always fails. Can I submit my ID through a ticket and have it verified there?</summary>

Unfortunately, manual review of IDs isn't supported; the process relies on automation.

</details>

<details>

<summary>Can I obtain a Human Passport stamp by verifying my phone number?</summary>

Yes, phone verification is now supported.

</details>

<details>

<summary>What type of documents can I use to verify my identity for proof of uniqueness?</summary>

You can verify with any of the following standard un-expired and valid physical documents that match your identity:

* passport
* driver's license
* national ID
* residence card
* voter's ID

We also have expanded support for less common identity cards to meet the communities needs such as:

* Provisional driver's licenses
* India PAN cards
* India Local ID cards
* Russian internal passport
* Nigeria voter cards
* USA passport card
* Swedish Identitetskort
* Spain ID

We do not support docs that have high likelihood of forgery such as:

* "Federal Limits Apply" US IDs
* Italian Refugee Cards
* Italian Paper ID cards
* Student ID cards
* Nigerian National Identification Numbers

</details>

<details>

<summary>Are there any countries that aren't supported for verification?</summary>

Yes, the unsupported countries are North Korea, Iran, Syria, and Cuba.

</details>

<details>

<summary>Verification complete! What’s next?</summary>

Continue to the minting page:

1. Connect your wallet: <https://silksecure.net/holonym/diff-wallet>.
2. Select Phone Verification or KYC/Government ID Verification.
3. Click "Mint SBT" to finalize the process.
4. Pay on your preferred Network. Confirm the payment on your wallet.
5. Get SBT on Optimism Network if you are verifying for Human Passport.
6. Confirm that the wallet address is correct, as you can only receive **one** SBT of this type and cannot transfer it to another wallet.<br>

You're all set!

</details>

<details>

<summary>Are there any verification guidelines I can use to avoid common issues?</summary>

To ensure a smooth verification process, please follow these steps:

• **Clear Photos** – Avoid blurriness by holding your phone still while capturing your ID and selfie. \
• **Scan Both Sides** – If using a driver's license, upload both front and back (no duplicates). \
• **Unaltered Documents** – Russian internal passports must have a visible, unedited machine-readable area. \
• **Valid ID** – Ensure your document is not expired. \
• **Selfie Match** – Your selfie must clearly match the photo on your ID. \
• **Use a Physical Document** – Do not take a picture of an ID displayed on a screen. \
• **Age Requirement** – You must be 18+ years old to verify.

⚠️ **Unsupported Countries:** North Korea, Iran, Syria, and Cuba. ⚠️ Unsupported IDs: Nigeria's NIN and Bangladesh's resident permit.

Following these guidelines will help prevent verification issues.

</details>

<details>

<summary>Can I verify again but with a different wallet?</summary>

No, each user can verify only one wallet per identity. If the system detects multiple verification attempts with different wallets, they will all fail. If you encountered an *'already registered'* error, do not attempt verification again. For now, your only option is to request a refund and wait until the first verification expires after a year before trying again.

</details>

<details>

<summary>How can I proceed with verification after making a payment?</summary>

If your payment was successful, you can go ahead and verify directly on the website.

Note: do not make another payment if you were not redirected to the verification page the first time. If you're having trouble and can't continue, open a support ticket for assistance.

</details>

<details>

<summary>What to do if my verification was unsuccessful?</summary>

If verification was unsuccessful, you can request a refund by following the refund steps outlined in our FAQs.

</details>

<details>

<summary>Can I retry verification if my first attempt failed?</summary>

After receiving your refund, you can try again, but we recommend using a different ID than the one you previously used.

</details>

<details>

<summary>How can I verify my identity?</summary>

You can verify your identity using standard unexpired and valid physical documents such as a passport, driver's license, national ID, or residence card. Unsupported documents include student IDs, Nigeria's NINS, and Bangladesh's resident permit IDs.

You can review the supported countries and documents with the following link: \
• [https://onfido.com/supported-documents](https://onfido.com/supported-documents/)

</details>

<details>

<summary>Is verification a one-time fee, or will I need to re-verify later?</summary>

Verification is valid for one year. After that, you’ll need to pay again to verify your identity.

</details>

<details>

<summary>Do I need to verify and mint SBT again if I’ve already minted it on the old app?</summary>

No, you don’t need to verify again as long as your previous verification is still valid.

</details>

<details>

<summary>What are the SBT Contract addresses?</summary>

Here are the SBT contract addresses:

**• For V3:** 0x2AA822e264F8cc31A2b9C22f39e5551241e94DfB \
\&#xNAN;**• For Legacy ID:** 0x7A81f2f88b0eE30eE0927c9F672487689C6dD7Ce \
\&#xNAN;**• For Legacy Phone:** 0xe337aD5aA1Cb84e12a7Aab85aEd1Ab6cb44C4a8e

</details>

<details>

<summary>I minted my ZK SBT to the wrong address! Can I transfer it?</summary>

SBTs (Soulbound Tokens) are intentionally non-transferable, making them a key part of our Sybil Defense system. To verify a different wallet, you must wait for the current verification proof to expire, which happens one year after minting.

</details>

<details>

<summary>Can I try verifying again with a different ID?</summary>

Each paid verification allows for one attempt. If it fails, you can request a refund and try again, but you'll need to submit a new payment and restart the process.&#x20;

</details>

<details>

<summary>Do I get a full refund if my verification failed?</summary>

If your verification fails, you can request a full refund.

</details>

<details>

<summary>How do I request a refund?</summary>

You can request a refund by submitting a support ticket.

</details>

<details>

<summary>What to do if I received the "Failed" message?</summary>

This means your verification attempt was unsuccessful. This is not a system bug, as the process is managed by the provider and subject to their approval. Verification failures can happen for various reasons, and unfortunately, we do not have the exact reason for each unsuccessful attempt. If you wish to try again, please use a different ID than the one you previously used.

</details>

<details>

<summary>Should I add my country code when entering phone number for verification?</summary>

Do NOT include the country code when entering your phone number. Select your country from the provided list instead.

</details>

<details>

<summary>I have troubles receiving OTP, what should I do?</summary>

**Follow this OTP Troubleshooting Guide:**

1. Do NOT include the country code when entering your phone number. Select your country from the provided list instead.
2. Restart your phone to refresh the connection with your network provider.
3. Check WhatsApp and Viber if SMS isn't received.

**Important Notes:**&#x20;

• Burner phones and virtual numbers are NOT accepted due to security reasons, as they weaken Sybil resistance.&#x20;

• Do NOT keep retrying without following these steps—doing so may lock you out after reaching the maximum attempts, preventing verification.&#x20;

• You have a maximum of three attempts. If you fail all three times, your only option is to request a refund. Please follow the refund process.

</details>

<details>

<summary>I received "Loading Credentials" error, what should I do?</summary>

If you received a *"Loading Credentials"* error, follow these steps:

1. Allow up to 30 minutes for the provider to complete your ID verification.
2. DO NOT close, refresh, or exit the page during this process.
3. Once verification is complete, check your profile— the *"Verify"* label on your picture will change to *"Verified"*.
4. After verification, click "Prove" to proceed with minting your SBT.

</details>

<details>

<summary>What to do if I received the "Failed to Fetch or V3SybilResistance.r1cs" error while minting SBT?</summary>

If you receive a *"Failed to Fetch or V3SybilResistance.r1cs"* error during the minting process, try the following solutions:

• **Use a Different Browser or Device** \
Copy the minting page link and open it in another browser or device.

**• Uninstall Internet Download Manager (IDM)** \
IDM can interfere with the minting process, so removing it may resolve the issue.

If the issue persists, try clearing your cache or using incognito mode.

</details>

<details>

<summary>Why am I getting an "Already Registered" error?</summary>

If you receive an *"Already Registered"* message, it means you have already completed verification with the same wallet or with different wallet.&#x20;

Each user can verify only once and submit a valid proof one single time. Any further attempts will result in errors. This rule ensures Holonym's sybil resistance.

</details>

<details>

<summary>What should I do if I'm facing issues with the payment page?</summary>

If you're having trouble with the payment page, try clearing your browser's cache and restarting it, then attempt again. You can also try using a different browser.

</details>

<details>

<summary>What should I do if verification still fails after two attempts?</summary>

• If you've failed twice, we don’t recommend  trying again unless you have a different ID and are willing to restart the entire process.&#x20;

• Even if you try again with a different ID, successful verification is not guaranteed, as it is still subject to the provider's approval.

</details>

<details>

<summary>How to unlock more Discord channels and roles?</summary>

:bulb: **Roles and Permissions**: Some channels require specific roles to access. Make sure you have the right roles to unlock more content.&#x20;

1. **For Token-Gated Roles**: Go to the :white\_check\_mark:│verify-roles channel and connect your wallet through our custom bot.
2. **Discover More Channels & Roles**: \
   • Click “Channels & Roles” in the left sidebar. \
   • Select “Customize” to answer a few questions and gain access to additional channels and roles. \
   • Use “Browse Channels” to filter and join channels that match your interests.&#x20;
3. **View Hidden Channels**: If some channels appear collapsed, click on the category name to expand and see all available channels.

</details>


# Verifying Identity with Human ID

Using Human ID to privately prove your identity

The Human ID verification process consists of two key stages:

1. **Issuance of Credentials** – Obtaining credentials through identity verification.
2. **Proving Facts About Credentials** – Demonstrating specific details without revealing unnecessary personal information.

## **Issuance**

On the [issuance page](https://silksecure.net/holonym/diff-wallet), users scan a QR code to begin the verification process by photographing the required identity documents.

## **Proving**

In the proof section, users select a wallet address to link their verified proof. Once the user has a Human ID, they can generate proofs of unique personhood, with more proof types planned for the future. To do so, visit <https://silksecure.net/holonym/diff-wallet>, where they will receive a soulbound token confirming whether their address belongs to a unique person.

{% hint style="info" %}
**Convenience vs. Privacy** \
If users trust the KYC provider not to track their wallet, they can immediately prove facts about their credentials. However, for those seeking a cryptographic guarantee that the KYC provider cannot track them, waiting—potentially for days or even weeks—enhances privacy. This follows the same principle as Tornado Cash or ZCash, where longer wait times improve anonymity.
{% endhint %}


# Identity Verification User Flow

Using Human ID to privately prove your identity

The identity verification process is divided into two steps: **issuance** and **proving**. Once a user has credentials issued, they can prove facts about themself.

## Issuance

A user can obtain their credentials by visiting <https://silksecure.net/holonym/diff-wallet> For a smoother experience, it's recommended to use the custom URLs provided in:

{% content-ref url="/pages/0fHiLJGaCcklYcnGZ93N" %}
[Integrating Human ID](/for-developers/start-here)
{% endcontent-ref %}

## Proving

Once a user has a Human ID, they can generate proofs of unique personhood, with more proof types planned for the future. To do so, they simply visit <https://silksecure.net/holonym/diff-wallet>, where they will receive a soulbound token confirming whether their address belongs to a unique person.


# Verifying ePassport

You need to have NFC-enabled passport to complete this verification

You can verify your ePassport using Holonym's Private Passport Verifier to increase Sybil resistance score. Verification is free.

To perform the ePassport verification using your NFC passport, follow the steps below:

1. Navigate to <https://silksecure.net/holonym/diff-wallet>.
2. Click the "ePassport" card.
3. Follow the steps to download the Private Passport Verifier mobile app.
4. Open the Private Passport Verifier app. Follow the steps to enter your information. Then scan your NFC passport.
5. After scanning, you will be redirected to <https://silksecure.net/holonym/nfc-handoff>.
6. At this page, a zero-knowledge proof is generated. This zero knowledge proof conceals your private information while proving that you are a unique person.
7. Enter your wallet address. Click submit. When you click submit, your address will receive a soul-bound token (SBT) attesting to your uniqueness.&#x20;

Note that you can only receive one SBT per NFC passport.


# Using Human ID with NEAR

In order to use Human ID with NEAR Protocol, follow the steps below:&#x20;

1. Visit <https://silksecure.net/holonym/diff-wallet> if you have an already-funded wallet you would like to use to pay fees on Aurora, Ethereum, Optimism, Avalanche, or Fantom.
2. Visit <https://silksecure.net/holonym/silk> if you do not already have a funded wallet on these chains or want to use the Human Wallet for other reasons.
3. Follow the steps to get the proof of unique government ID or phone.
4. When you prove, enter your NEAR account ID that you want the uniqueness proof to be publicly linked to.

Now you're done! You've obtained the uniqueness proof for the NEAR SBT. You can use it with sites like <https://www.nada.bot/> to prove that you are a unique person!

{% hint style="info" %}
*For higher privacy, you can connect and pay with a wallet address different than the one you want the proof-of-uniqueness to be linked to. You may also delay between paying and proving to prevent timing information from linking your ID to your wallet address. If you do so, even in case of a malicious KYC provider your address with the uniqueness proof can't be linked to your identity.*
{% endhint %}


# Using Human ID with Stellar

In order to use Human ID with Stellar, follow the steps below:&#x20;

1. Visit <https://silksecure.net/holonym/diff-wallet> with an already-funded wallet you would like to use to pay fees on Aurora, Ethereum, Optimism, or Avalanche.
2. Follow the steps to get the proof of unique government ID or phone.
3. When you prove, enter your Stellar address that you want the uniqueness proof to be publicly linked to.

Now you're done! You've obtained the uniqueness proof for the Stellar SBT.

{% hint style="info" %}
*For higher privacy, you can connect and pay with a wallet address different than the one you want the proof-of-uniqueness to be linked to. You may also delay between paying and proving to prevent timing information from linking your ID to your wallet address. If you do so, even in case of a malicious KYC provider your address with the uniqueness proof can't be linked to your identity.*
{% endhint %}


# Getting Refunded

If you verification was unsuccessful, please follow the steps below to begin the refund process:

### Step 1: Watch the Video Guide (Optional)

To make the process easier, you can watch this helpful video guide that walks you through the refund steps: [Watch the Video Guide](https://www.youtube.com/watch?v=QkGi0VHvmuA)

### Step 2: Visit the Refund Link

Click on the following link to initiate the refund process: [Refund Link](https://silksecure.net/holonym/diff-wallet)

### Step 3: Find the "Eligible for Refund" Label

Once you're on the page, locate the **"Eligible for Refund"** label next to your cards.

### Step 4: Click on the Refund Label

Click on the **"Eligible for Refund"** label to access the refund page.

### Step 5: Complete the Refund Process

Follow the on-screen instructions to finalize your refund.

We hope this guide, along with the video, helps you complete your refund process smoothly. Let us know if you have any questions!

<br>


# Integrating Human ID

What you can do with Human  ID and how to start

While Holonym can be used off-chain it is by far most commonly used on-chain. We use the term Soul-Bound Token (SBT) to refer to an on-chain record linked to a user's address. To use Holonym, you simply must direct the user to get their SBT, then you can read the SBT from from our API in one line of code, or from the blockchain itself.

## 1. Send a user to Holonym to get an SBT

### Option A: Via Link or Redirect

You may send the user to

```
https://id.human.tech/<credentialType>
```

&#x20;`credentialType` is one of:

* `gov-id` (for government ID credentials).
* `clean-hands` (for sanctions checks).
* `phone` (for phone number credentials).
* `biometrics` (for non-personally identifying face uniqueness and liveness check).

#### ePassport

Users can also verify for free using an NFC-enabled government ID document. Please see [Verifying ePassport](/for-users/how-to-verify-epassport) for user instructions.

Note that the ePassport SBT is distinct from the gov-id SBT. A user can get both an ePassport SBT and a gov-id (KYC) SBT.

### Option B: Via Human ID SDK

To directly embed Human ID into your website, you can import the [Human ID SDK](https://www.npmjs.com/package/@holonym-foundation/human-id-sdk) and call

```javascript
humanID.requestSBT(credentialType)
```

where `credentialType` is either `'kyc'` (for government ID credentials), `'clean-hands'`,  `'phone'` (for phone number credentials) or `'biometrics'`.\
This will prompt the user to complete the SBT minting flow.

## 2. Read a user's SBT

### Option A: Holonym API

To check whether a user has a certain SBT, you can query the Holonym API. The JavaScript example shows how to call the Holonym API (which calls the SBT smart contract) to get whether the user is unique for the given action ID. (Use the default action ID of `123456789`.)

{% tabs %}
{% tab title="JavaScript" %}

```javascript
// `action-id` is the action ID. The only action ID currently 
// in use is 123456789.
// `user` is the address to be checked.
const resp = await fetch('https://api.holonym.io/sybil-resistance/gov-id/optimism?action-id=123456789&user=0x0000000000000000000000000000000000000000');
const { result: isUnique } = await resp.json();
```

{% endtab %}
{% endtabs %}

### Option B (DEPRECATED): On chain

You can call the SBT contract directly. The Solidity example gives anyone 1 gwei who has proven they're from the US. (See more examples in the [examples repo](https://github.com/holonym-foundation/id-hub-contracts/tree/main/contracts/examples).)

{% tabs %}
{% tab title="Solidity" %}

<pre class="language-solidity"><code class="lang-solidity"><strong>// NOTE: This example is deprecated with Holonym V3.
</strong>
<strong>pragma solidity ^0.8.0;
</strong>import "../interfaces/IResidencyStore.sol";

// US Residents have had it hard this year! let's send 1 gwei to anyone who can prove they're from the US
contract USResidency {
    IResidencyStore resStore;
    constructor() {
        resStore = IResidencyStore(0x7497636F5E657e1E7Ea2e851cDc8649487dF3aab); //ResidencyStore address can be found on https://github.com/holonym-foundation/app.holonym.id-frontend/blob/main/src/constants/proofContractAddresses.json
    }

    // NOTE: there are better ways to send ETH. Please don't copy this code
    function sendStimmy() public {
        require(resStore.usResidency(msg.sender), "You have not proven you are from the US");
        payable(msg.sender).send(1);
    }
}
</code></pre>

{% endtab %}
{% endtabs %}

You may have noticed one person can claim the gwei many times in the Solidity example! This an example; please do not implement a US stimulus program from it.

{% hint style="info" %}
**Note:** by default, proofs can only be done once-per-ID to prevent Sybil attacks & bribery. You don't want somebody to sell their US residency proof to others. This means a user can only verify one of their addresses as being a US resident.
{% endhint %}

## Example: Sybil Resistance

### 1. Send users to Holonym to get government ID uniqueness SBTs

Send users to the following URL.

```
https://id.human.tech/gov-id/kyc/issuance/prereqs
```

Users will complete verification and get an SBT at the address of their choosing.

### 2. Read users' uniqueness SBTs

The below JS example checks whether an address belongs to a unique person.

The smart contract in Solidity gives a privacy-preserving airdrop once-per-person to only unique people.

{% tabs %}
{% tab title="JavaScript" %}

<pre class="language-javascript"><code class="lang-javascript"><strong>//`user` is the address that should be checked for uniqueness
</strong><strong>// `action-id` should be 123456789, unless you specified a custom action ID
</strong><strong>const resp = await fetch('https://api.holonym.io/sybil-resistance/gov-id/optimism?user=0x0000000000000000000000000000000000000000&#x26;action-id=123456789');
</strong>const { result: isUnique } = await resp.json();
</code></pre>

{% endtab %}

{% tab title="Solidity" %}

```solidity
// NOTE: This example is deprecated with Holonym V3

pragma solidity ^0.8.0;
import "../interfaces/IAntiSybilStore.sol";

/* NOTE: you can replace airdrop with any other action that needs Sybil resistance. 
This uses an airdrop as an example, but it can be adapted for voting, play-to-earn, play-to-learn, etc.
Example: You want to give an airdrop to all your early users. You want to make your early users get their fare share and far more than they would get if it was just bots swooping up the airdrop and dumping.
*/
contract SybilFreeAction {
    IAntiSybilStore registeredActions; // Where actions (including the actionId we care about) are registered
    uint actionId; // The ID for this particular action. We reccomend the default ID of 123456789
    mapping (address => bool) hasClaimedAirdrop;

    constructor(uint actionId_) {
        registeredActions = IAntiSybilStore(0xFcA7AC96b1F8A2b8b64C6f08e993D6A85031333e); // AntiSybilStore address for different chains can be found on https://github.com/holonym-foundation/app.holonym.id-frontend/blob/main/src/constants/proofContractAddresses.json
        actionId = actionId_;
    }

    function claimAirdrop() public {
        require(registeredActions.isUniqueForAction(msg.sender, actionId), "You have not yet claimed this action with your Holo");
        require(!hasClaimedAirdrop[msg.sender], "You already got your airdrop!");
        hasClaimedAirdrop[msg.sender] = true;
        _giveAirdrop(msg.sender);
    }

    function _giveAirdrop(address recipient) private {
        // not implemented
    }
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
**Note**: we highly recommend using the default actionID of `123456789`. If you would like to explore custom actionIDs, please see [Custom Sybil Resistance](/for-developers/custom-sybil-resistance). Otherwise, feel free to ignore the concept of an actionID and just use `123456789`as shown in the example.
{% endhint %}


# Custom Sybil Resistance

Political, social, and economic systems often need Sybil resistance. Sybil resistance is anything of the form:

*1 person => 1 x*

e.g.,&#x20;

* [ ] 1 person => 1 vote (democracy)
* [ ] 1 person => 1 stimulus check
* [ ] 1 person => social security check
* [ ] 1 person => 1 number of tokens (fair airdrop)
* [ ] 1 person => 1 player (play-to-earn game preventing spam)
* [ ] 1 person => 1 number of comments (social network preventing spam)
* [ ] 1 person => 1 account (generic application preventing spam)
* [ ] 1 person => 1 number of accounts (generic application preventing spam but a little more lenient)

## Standard Sybil Resistance

To just see whether a unique person owns a wallet address, you may visit [Integrating Human ID](/for-developers/start-here)

## More Advanced Sybil Resistance

Sometimes you want to prove Sybil resistance for a specific action. E.g., if a user wants to vote in multiple elections from different addresses as to not reveal their voting history. This can be done by giving each election a unique `actionID`.&#x20;

Importantly, this also means bribery would be easier. Imagine an action somebody doesn't care about. That person can provide a new address that `actionID`. One can imagine a DAO vote where a bad actor is passionate about the outcome, and can create 1000 new address, then bribe 1000 random people to register one of those addresses for that specific `actionID`. Since the random people don't care about the vote outcome, they don't mind giving away their uniqueness to the new address for that particular `actionID`.&#x20;

For the default `actionID`, it's far harder to launch a large-scale bribery attack, as giving up a vote to a briber is giving up everything to a briber: all future votes, universal basic income, and airdrops will go to the briber. While some people would gladly give up these rewards for the right price, doing so would require the presentation of government ID in order to participate in a black market. This risk far outweighs any rewards, so we assume such bribery on the default `actionID` will be negligible.&#x20;

## How to set an actionID

To prove uniqueness with respect to a specific action, you must create an actionId. Please use a large random number less than `21888242871839275222246405745257275088548364400416034343698204186575808495617` (the bn254 scalar field order). Let's say your actionId is `11223344556677`, and after your users are done proving uniqueness, you want to them to be sent back to yourwebsite.com/yourcallback.&#x20;

Then, they may visit [https://holonym.io/prove/uniqueness/11223344556677/yourwebsite%2E](https://holonym.io/prove/uniqueness/1234567890/yourwebsite.com)[com%2Fyourcallback](https://holonym.io/prove/uniqueness/1234567890/yourwebsite.com)

You can then view their verification status with respect to the custom actionID by the standard steps in [Integrating Human ID](/for-developers/start-here). Simply replace the `actionID` in the code examples with your custom `actionID`.&#x20;


# API Reference

Reference for https\://api.holonym.io

## Endpoints

* [**GET** `/sybil-resistance/gov-id/<network>`](#get-sybil-resistance-less-than-credential-type-greater-than-less-than-network-greater-than-user-less)
* [**GET** `/sybil-resistance/epassport/<network>`](#get-sybil-resistance-less-than-credential-type-greater-than-less-than-network-greater-than-user-less)
* [**GET** `/sybil-resistance/phone/<network>`](#get-sybil-resistance-less-than-credential-type-greater-than-less-than-network-greater-than-user-less)
* [**GET** `/sybil-resistance/biometrics/<network>`](#get-sybil-resistance-less-than-credential-type-greater-than-less-than-network-greater-than-user-less)
* [**GET** `/residence/country/us/<network>`](#get-residence-country-us-less-than-network-greater-than-user-less-than-user-address-greater-than)
* [**GET** `/snapshot-strategies/residence/country/us`](#get-snapshot-strategies-residence-country-us-network-less-than-network-greater-than-and-snapshot-les)
* [**GET** `/snapshot-strategies/sybil-resistance/gov-id`](#get-snapshot-strategies-sybil-resistance-gov-id-network-less-than-network-greater-than-and-snapshot)
* [**GET** `/snapshot-strategies/sybil-resistance/phone`](#get-snapshot-strategies-sybil-resistance-phone-network-less-than-network-greater-than-and-snapshot-l)
* [**GET** `/snapshot-strategies/sybil-resistance/biometrics`](#get-snapshot-strategies-sybil-resistance-biometrics-network-less-than-network-greater-than-and-snaps)

#### **GET** `/sybil-resistance/<credential-type>/<network>?user=<user-address>&action-id=<action-id>`

Get whether the user has registered for the given action-id.

When a user "registers", they are establishing that the given blockchain address is a unique person for the action ID. See the section Sybil resistance for more information about how action IDs can be used.

If `credential-type` is `gov-id`, this endpoint uses Holonym smart contracts to check whether the user has completed KYC with a unique government ID. If `credential-type` is `epassport`, this endpoint uses Holonym smart contracts to check whether the user has a unique NFC-enabled passport. If `credential-type` is `phone`, this endpoint uses Holonym smart contract to check whether the user has proven ownership of a unique phone number. If `credential-type` is `biometrics`, this endpoint uses Holonym smart contract to check whether the user has proven ownership of a unique (non-personally identifying) face.

See the following documentation [How to get user's proofs](https://holonym.gitbook.io/holonym-alpha/usage/how-to-stop-sybil-attacks-using-holonym#how-to-get-the-proof) for how to use action IDs.

* Parameters

  | name              | description                         | type   | in    | required |
  | ----------------- | ----------------------------------- | ------ | ----- | -------- |
  | `credential-type` | 'gov-id' or 'phone' or 'biometrics' | string | path  | true     |
  | `network`         | 'optimism' or 'base-sepolia'        | string | path  | true     |
  | `user`            | User's blockchain address           | string | query | true     |
  | `action-id`       | Action ID                           | string | query | true     |
* Example

  ```JavaScript
  const resp = await fetch('https://api.holonym.io/sybil-resistance/gov-id/optimism?user=0x0000000000000000000000000000000000000000&action-id=123456789');
  const { result: isUnique } = await resp.json();
  ```
* Responses
  * 200

    ```JSON
    {
        "result": true,
    }
    ```
  * 200

    Result if user has not submitted a valid proof.

    ```JSON
    {
        "result": false,
    }
    ```

#### **GET** `/residence/country/us/<network>?user=<user-address>`

Get whether the user resides in the US.

For the `/residence/country/<country-code>` endpoints, `<country-code>` will be a 2-letter country code following the [ISO 3166 standard](https://www.iso.org/iso-3166-country-codes.html). Holonym currently only supports queries for US residency.

* Parameters

  | name      | description                     | type   | in    | required |
  | --------- | ------------------------------- | ------ | ----- | -------- |
  | `network` | 'optimism' or 'optimism-goerli' | string | path  | true     |
  | `user`    | User's blockchain address       | string | query | true     |
* Example

  ```
  const resp = await fetch('https://api.holonym.io/residence/country/us/optimism?user=0x0000000000000000000000000000000000000000');
  const { result: isUSResident } = await resp.json();
  ```
* Responses
  * 200

    Result if user resides in the US.

    ```
    {
        "result": true,
    }
    ```
  * 200

    Result if user has not submitted a valid proof that they reside in the US.

    ```
    {
        "result": false,
    }
    ```

#### **GET** `/snapshot-strategies/residence/country/us?network=<network>&snapshot=<snapshot>&addresses=<addresses>`

Returns a list of scores indicating, for each address, whether the address has submitted a valid and unique proof of US residency.

Every score is either 1 or 0.

| score | description                           |
| ----- | ------------------------------------- |
| 1     | Address has proven US residency       |
| 0     | Address has *not* proven US residency |

**Use with Snapshot**

To use with the ["api"](https://github.com/snapshot-labs/snapshot-strategies/tree/master/src/strategies/api) Snapshot strategy, specify the strategy parameters using the following format.

```
{
  "api": "https://api.holonym.io",
  "symbol": "",
  "decimals": 0,
  "strategy": "snapshot-strategies/residence/country/us"
}
```

**Use without Snapshot**

* Parameters

  | name        | description                                    | type   | in    | required |
  | ----------- | ---------------------------------------------- | ------ | ----- | -------- |
  | `network`   | Chain ID                                       | string | query | true     |
  | `snapshot`  | Block height                                   | string | query | true     |
  | `addresses` | List of blockchain address separated by commas | string | query | true     |
* Example

  ```
  const resp = await fetch('https://api.holonym.io/snapshot-strategies/residence/country/us?network=420&snapshot=9001&addresses=0x0000000000000000000000000000000000000000,0x0000000000000000000000000000000000000001');
  const data = await resp.json();
  ```
* Responses
  * 200

    ```
    {
      "score" : [
          {
            "address" : "0x0000000000000000000000000000000000000000",
            "score" : 0
          },
          {
            "address" : "0x0000000000000000000000000000000000000001",
            "score" : 1
          }
      ]
    }
    ```

#### **GET** `/snapshot-strategies/sybil-resistance/gov-id?network=<network>&snapshot=<snapshot>&addresses=<addresses>&action-id=<action-id>`

Returns a list of scores indicating, for each address, whether the address has submitted a valid proof of uniqueness for the given action-id.

Every score is either 1 or 0.

| score | description                                       |
| ----- | ------------------------------------------------- |
| 1     | Address has proven uniqueness for action-id       |
| 0     | Address has *not* proven uniqueness for action-id |

**Use with Snapshot**

To use with the ["api"](https://github.com/snapshot-labs/snapshot-strategies/tree/master/src/strategies/api) Snapshot strategy, specify the strategy parameters using the following format. We highly recommend that projects use the default action-id `123456789` to avoid cases where users sell actions associated with action-ids that they do not care about.

```
{
  "api": "https://api.holonym.io",
  "symbol": "",
  "decimals": 0,
  "strategy": "snapshot-strategies/sybil-resistance/gov-id",
  "additionalParameters": "action-id=123456789"
}
```

**Use without Snapshot**

* Parameters

  | name        | description                                    | type   | in    | required |
  | ----------- | ---------------------------------------------- | ------ | ----- | -------- |
  | `network`   | Chain ID                                       | string | query | true     |
  | `snapshot`  | Block height                                   | string | query | true     |
  | `addresses` | List of blockchain address separated by commas | string | query | true     |
* Example

  ```
  const resp = await fetch('https://api.holonym.io/snapshot-strategies/sybil-resistance/gov-id?network=420&snapshot=9001&addresses=0x0000000000000000000000000000000000000000,0x0000000000000000000000000000000000000001&action-id=123');
  const data = await resp.json();
  ```
* Responses
  * 200

    ```
    {
      "score" : [
          {
            "address" : "0x0000000000000000000000000000000000000000",
            "score" : 0
          },
          {
            "address" : "0x0000000000000000000000000000000000000001",
            "score" : 1
          }
      ]
    }
    ```

#### **GET** `/snapshot-strategies/sybil-resistance/phone?network=<network>&snapshot=<snapshot>&addresses=<addresses>&action-id=<action-id>`

Returns a list of scores indicating, for each address, whether the address has submitted a valid proof of uniqueness (using phone number) for the given action-id.

Every score is either 1 or 0.

| score | description                                       |
| ----- | ------------------------------------------------- |
| 1     | Address has proven uniqueness for action-id       |
| 0     | Address has *not* proven uniqueness for action-id |

**Use with Snapshot**

To use with the ["api"](https://github.com/snapshot-labs/snapshot-strategies/tree/master/src/strategies/api) Snapshot strategy, specify the strategy parameters using the following format. We suggest that you use the default action-id `123456789`. If you are using a different action-id, replace `123456789` with your action-id.

```
{
  "api": "https://api.holonym.io",
  "symbol": "",
  "decimals": 0,
  "strategy": "snapshot-strategies/sybil-resistance/phone",
  "additionalParameters": "action-id=123456789"
}
```

**Use without Snapshot**

* Parameters

  | name        | description                                    | type   | in    | required |
  | ----------- | ---------------------------------------------- | ------ | ----- | -------- |
  | `network`   | Chain ID                                       | string | query | true     |
  | `snapshot`  | Block height                                   | string | query | true     |
  | `addresses` | List of blockchain address separated by commas | string | query | true     |
* Example

  ```JavaScript
  const resp = await fetch('https://api.holonym.io/snapshot-strategies/sybil-resistance/phone?network=420&snapshot=9001&addresses=0x0000000000000000000000000000000000000000,0x0000000000000000000000000000000000000001&action-id=123');
  const data = await resp.json();
  ```
* Responses
  * 200

    ```JSON
    {
      "score" : [
          {
            "address" : "0x0000000000000000000000000000000000000000",
            "score" : 0
          },
          {
            "address" : "0x0000000000000000000000000000000000000001",
            "score" : 1
          }
      ]
    }
    ```

#### **GET** `/snapshot-strategies/sybil-resistance/biometrics?network=<network>&snapshot=<snapshot>&addresses=<addresses>&action-id=<action-id>`

Returns a list of scores indicating, for each address, whether the address has submitted a valid proof of uniqueness (using non-personally identifying face vectors) for the given action-id.

Every score is either 1 or 0.

| score | description                                       |
| ----- | ------------------------------------------------- |
| 1     | Address has proven uniqueness for action-id       |
| 0     | Address has *not* proven uniqueness for action-id |

**Use with Snapshot**

To use with the ["api"](https://github.com/snapshot-labs/snapshot-strategies/tree/master/src/strategies/api) Snapshot strategy, specify the strategy parameters using the following format. We suggest that you use the default action-id `123456789`. If you are using a different action-id, replace `123456789` with your action-id.

```
{
  "api": "https://api.holonym.io",
  "symbol": "",
  "decimals": 0,
  "strategy": "snapshot-strategies/sybil-resistance/biometrics",
  "additionalParameters": "action-id=123456789"
}
```

**Use without Snapshot**

* Parameters

  | name        | description                                    | type   | in    | required |
  | ----------- | ---------------------------------------------- | ------ | ----- | -------- |
  | `network`   | Chain ID                                       | string | query | true     |
  | `snapshot`  | Block height                                   | string | query | true     |
  | `addresses` | List of blockchain address separated by commas | string | query | true     |
* Example

  ```JavaScript
  const resp = await fetch('https://api.holonym.io/snapshot-strategies/sybil-resistance/biometrics?network=420&snapshot=9001&addresses=0x0000000000000000000000000000000000000000,0x0000000000000000000000000000000000000001&action-id=123');
  const data = await resp.json();
  ```
* Responses
  * 200

    ```JSON
    {
      "score" : [
          {
            "address" : "0x0000000000000000000000000000000000000000",
            "score" : 0
          },
          {
            "address" : "0x0000000000000000000000000000000000000001",
            "score" : 1
          }
      ]
    }
    ```


# Dry Runs

(Coming soon)

Human ID has dry run flows. These allow developers to (a) complete verification flows and (b) get mock SBTs on testnet while they are integrating Human ID into their dapp.

## Get mock SBT

### Option A: With Human Wallet SDK

...

### Option B: With Redirect

...

## Read mock SBT

Use the Holonym API to query the [sybil-resistance endpoint](/for-developers/api-reference#get-sybil-resistance-less-than-credential-type-greater-than-less-than-network-greater-than-user-less). For the `network` option, specify "base-sepolia" instead of "optimism".


# Sign Protocol Attestations

How to read Holonym attestations on Sign Protocol

Holonym issues an Sign Protocol attestation for every SBT it sends. The following is an example of how to query and validate Holonym attestations.

### **Query**

See the [Sign Protocol docs](https://docs.sign.global/developer-apis/index/api/get/index-service#query-attestations) for how to construct queries.

This query gets the first page of attestations issued by Holonym.&#x20;

Query parameters used:

* `schemaId` - Filters for the HolonymV3 Sign Protocol schema.
* `attestor` - Filters for Holonym's attestor address, `0xB1f50c6C34C72346b1229e5C80587D0D659556Fd`.&#x20;

```bash
curl 'https://mainnet-rpc.sign.global/api/index/attestations?schemaId=onchain_evm_10_0x1&attester=0xB1f50c6C34C72346b1229e5C80587D0D659556Fd'
```

The response looks like this...

```json
{
   "data" : {
      "page" : 1,
      "rows" : [
         {
            "attestTimestamp" : "1714819085000",
            "attestationId" : "0x65",
            "attester" : "0xB1f50c6C34C72346b1229e5C80587D0D659556Fd",
            "chainId" : "10",
            "chainType" : "evm",
            "data" : "0x000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000004230783732396436363065316330326534653431393734356536313764363433663839376135333836373363636631303531653039336262666135386230613132306200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000068080e7f000000000000000000000000ededf460a77928f59c27f37f73d4853fd8a0798400000000000000000000000000000000000000000000000000000000075bcd151cb8413a579d6138d257450bb5209579bde35c885f7a4405e3767a5a5d2ea6df03fae82f38bf01d9799d57fdda64fad4ac44e4c2c2f16c5bf8e1873d0a3e1993",
            "dataLocation" : "onchain",
            "extra" : {},
            "from" : "0xB1f50c6C34C72346b1229e5C80587D0D659556Fd",
            "fullSchemaId" : "onchain_evm_10_0x1",
            "id" : "onchain_evm_10_0x65",
            "indexingValue" : "0x1cb8413a579d6138d257450bb5209579bde35c885f7a4405e3767a5a5d2ea6df",
            "lastSyncAt" : null,
            "linkedAttestation" : "",
            "mode" : "onchain",
            "recipients" : [
               "0xEdedf460A77928f59c27f37F73D4853FD8a07984"
            ],
            "revokeReason" : null,
            "revokeTimestamp" : null,
            "revokeTransactionHash" : "",
            "revoked" : false,
            "schema" : {
               "chainId" : "10",
               "chainType" : "evm",
               "data" : [
                  {
                     "name" : "circuitId",
                     "type" : "string"
                  },
                  {
                     "name" : "publicValues",
                     "type" : "uint256[]"
                  }
               ],
               "dataLocation" : "onchain",
               "description" : "Holonym V3 SBT",
               "extra" : {
                  "data" : "{\"name\":\"HolonymV3\",\"description\":\"Holonym V3 SBT\",\"data\":[{\"name\":\"circuitId\",\"type\":\"string\"},{\"name\":\"publicValues\",\"type\":\"uint256[]\"}]}"
               },
               "id" : "onchain_evm_10_0x1",
               "maxValidFor" : "0",
               "mode" : "onchain",
               "name" : "HolonymV3",
               "originalData" : "{\"name\":\"HolonymV3\",\"description\":\"Holonym V3 SBT\",\"data\":[{\"name\":\"circuitId\",\"type\":\"string\"},{\"name\":\"publicValues\",\"type\":\"uint256[]\"}]}",
               "registerTimestamp" : "1714776243000",
               "registrant" : "0xcaFe2eF59688187EE312C8aca10CEB798338f7e3",
               "resolver" : "0x0000000000000000000000000000000000000000",
               "revocable" : true,
               "schemaId" : "0x1",
               "syncAt" : "1714776266473",
               "transactionHash" : "0xfd378e1a6758a5fbafad18f21da353485c8106013dccf501268b6cf126a0eef0"
            },
            "schemaId" : "0x1",
            "syncAt" : "1714819103980",
            "transactionHash" : "0x6216e5793cb20d350680942e97f33688bb075be6c2bbbc960b71d254a2e7bc96",
            "validUntil" : "0"
         },
         ...
      ],
      "size": 100,
      "total": 195
   },
   "message" : "ok",
   "statusCode" : 200,
   "success" : true
}
```

If you are querying for a specific recipient, filter by the recipient.

### Validate

For a Holonym V3 SBT, we always want to verify the *circuit ID* as well as the *action ID* and *issuer* used to generate the ZKP. We want the circuit ID to match whatever circuit we are filtering for. We want the action ID to be the default action ID used for Holonym Sybil resistance proofs. We want the issuer to match whatever Holonym issuer we are filtering for.

See [here](https://github.com/holonym-foundation/holonym-api/blob/main/src/constants/misc.js#L6) for the circuit IDs, action ID, and issuers.

{% code overflow="wrap" %}

```javascript
// We are using Ethers v5
const { ethers } = require("ethers");

// Extract attestation data. 
// This is data.rows[0].data in the API response above
const data = "0x000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000004230783732396436363065316330326534653431393734356536313764363433663839376135333836373363636631303531653039336262666135386230613132306200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000068080e7f000000000000000000000000ededf460a77928f59c27f37f73d4853fd8a0798400000000000000000000000000000000000000000000000000000000075bcd151cb8413a579d6138d257450bb5209579bde35c885f7a4405e3767a5a5d2ea6df03fae82f38bf01d9799d57fdda64fad4ac44e4c2c2f16c5bf8e1873d0a3e1993";

const decoded = ethers.utils.defaultAbiCoder.decode(["string", "uint256[]"], data);

const circuitId = decoded[0];

// Public values of the ZKP
const publicValues = decoded[1];

const actionId = publicValues[2];
const issuer = publicValues[4];

// Make sure circuitId matches KYC circuit ID
if (circuitId != "0x729d660e1c02e4e419745e617d643f897a538673ccf1051e093bbfa58b0a120b") {
    throw new Error("Invalid circuit ID");
}

// Validate action ID
if (actionId.toString() != "123456789") {
    throw new Error("Invalid action ID");
}

// Make sure issuer is the KYC Holonym issuer
if (issuer.toHexString() != "0x03fae82f38bf01d9799d57fdda64fad4ac44e4c2c2f16c5bf8e1873d0a3e1993") {
    throw new Error("Invalid issuer")
}
```

{% endcode %}


# Verax Attestations

How to read Holonym attestations on Verax

Holonym issues a Verax attestation for every SBT it sends. The following is an example of how to query and validate Holonym attestations.

### Install

```bash
npm i @verax-attestation-registry/verax-sdk
```

### Code

In this example, we check the attestation for the address `0xdcA2e9AE8423D7B0F94D7F9FC09E698a45F3c851`.

It's important to filter for Holonym's schema ID and attester account and to validate the attestation payload. Here, we are making sure the user has a uniqueness KYC SBT.

See [here](https://github.com/holonym-foundation/holonym-api/blob/main/src/constants/misc.js#L6) for the circuit IDs, action ID, and issuers.

```javascript
const { VeraxSdk } = require('@verax-attestation-registry/verax-sdk');

// Initialize Verax
const address = '0xdcA2e9AE8423D7B0F94D7F9FC09E698a45F3c851';
const veraxSdk = new VeraxSdk(
  VeraxSdk.DEFAULT_LINEA_MAINNET, 
  address
);

// Query attestation registry
const attestations = await veraxSdk.attestation.findBy(
  undefined,
  undefined,
  {
    // Holonym's schemaId
    schemaId: "0x1c14fd320660a59a50eb1f795116193a59c26f2463c0705b79d8cb97aa9f419b",
    // Holonym's attester account
    attester: "0xB1f50c6C34C72346b1229e5C80587D0D659556Fd",
    // Our address of interest
    subject: "0xdcA2e9AE8423D7B0F94D7F9FC09E698a45F3c851"
  }
);

const { circuitId, publicValues, revoked } = attestations[0].decodedPayload[0];

const actionId = publicValues[2].toString();
const issuer = publicValues[4];

// Make sure circuitId matches KYC circuit ID
if (circuitId != "0x729d660e1c02e4e419745e617d643f897a538673ccf1051e093bbfa58b0a120b") {
    throw new Error("Invalid circuit ID");
}

// Validate action ID
if (actionId != "123456789") {
    throw new Error("Invalid action ID");
}

// Make sure issuer is the KYC Holonym issuer
if (issuer != BigInt("0x03fae82f38bf01d9799d57fdda64fad4ac44e4c2c2f16c5bf8e1873d0a3e1993")) {
    throw new Error("Invalid issuer")
}
```


# Off-Chain Proofs

### Overview

The flow is the following:

1. Your site redirects user to Holonym where the user generates the requested proof.
2. After generating the proof, the user is redirected to a callback specified by the organization. The proof is included in the URL query parameters. At the callback location, the organization can verify the proof.

### 1. Redirect user to Holonym

Redirect user to the following URL:

```
https://app.holonym.id/prove/off-chain/<proofType>?callback=<callback>
```

Parameter descriptions:

* `proofType` - Either `uniqueness` (for government ID proof of uniqueness), `uniqueness-phone` (for phone proof of uniqueness), or `us-residency` (for proof of US residency).
* `callback` - The URL to which the user should be redirected once they've generated their proof. The proof will be added a query parameter to this callback. For example, if `callback` is `https://example.com`, the user will be redirected to `https://example.com?proof=<proof>`.

### 2. Verify proof

The user will not be redirected to the callback unless the proof is valid. But you may want to verify the proof yourself.&#x20;

You can use the [@holonym-foundation/off-chain-sdk](https://www.npmjs.com/package/@holonym-foundation/off-chain-sdk) package to verify proofs.

```javascript
import { 
  verifyUniquenessProof
} from '@holonym-foundation/off-chain-sdk';

/**
 * @param proof - The value of the "proof" query parameter passed to the callback URL
 * @returns {Promise<boolean>}
 */
async function verify(proof) {
  const parsedProof = JSON.parse(proof);
  const result = await verifyUniquenessProof(parsedProof);
  console.log(`User is unique: ${result}`);
  return result;
}
```


# Clean Hands Attestations

How to read Clean Hands attestations

Human ID issues its Clean Hands attestation to users who prove that they are not on any sanctions lists (see all the lists here: [Clean Hands Architecture](/architecture/clean-hands#lists-checked-for-proof-of-clean-hands)).

## Off-chain with Sign Protocol

Use Sign Protocol's scan API to see whether a user has a valid Clean Hands attestation.

```typescript
// Set user address
const address = '0x123'

const resp = await fetch(`https://mainnet-rpc.sign.global/api/scan/addresses/${address}/attestations`)
const data = await resp.json()
const cleanHandsAttestations = data.data.rows.filter((att) => (
  att.fullSchemaId == 'onchain_evm_10_0x8' &&
  att.attester == '0xB1f50c6C34C72346b1229e5C80587D0D659556Fd' &&
  att.isReceiver == true && 
  !att.revoked &&
  att.validUntil > (new Date().getTime() / 1000)
))
const hasValidAttestation = cleanHandsAttestations.length > 0
```

## On Optimism with Sign Protocol

Tutorial coming soon...

## On-chain (not Optimism)

Use our off-chain attester, and verify its signature on-chain. Our attester returns a signature of the circuit ID, action ID, and user address, if the address has a clean hands attestation.

Our attester address is `0xa74772264f896843c6346ceA9B13e0128A1d3b5D`.

#### Query for signature

```typescript
// Set user address
const userAddress = '0x123'

const actionId = 123456789
const resp = await fetch(`https://api.holonym.io/attestation/sbts/clean-hands?action-id=${actionId}&address=${userAddress}`)
const { isUnique, signature, circuitId } = await resp.json();
```

#### Verify signature in Solidity

Warning: this Solidity code is untested.

```solidity
pragma solidity ^0.8.4;

import "openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol";

contract SignatureVerification {
    using ECDSA for bytes32;

    address public humanIdAttester = 0xa74772264f896843c6346ceA9B13e0128A1d3b5D;

    function verifySignature(
        uint256 circuitId,
        uint256 actionId,
        address userAddress,
        bytes memory signature
    ) public pure returns (bool) {
        bytes32 digest = keccak256(abi.encodePacked(
            circuitId,
            actionId,
            userAddress
        ));

        bytes32 personalSignPreimage = keccak256(abi.encodePacked(
            "\x19Ethereum Signed Message:\n32",
            digest
        ));

        (
            address recovered,
            ECDSA.RecoverError err,
            bytes32 _sig
        ) = ECDSA.tryRecover(personalSignPreimage, publicSignalsSig);

        return recovered == humanIdAttester;
    }
}
```

#### Verify signature in JavaScript

```typescript
// Verify using ethers v5
const digest = ethers.utils.solidityKeccak256(
  ["uint256", "uint256", "address"],
  [circuitId, actionId, userAddress]
);
const personalSignPreimage = ethers.utils.solidityKeccak256(
  ["string", "bytes32"],
  ["\x19Ethereum Signed Message:\n32", digest]
);
const recovered = ethers.utils.recoverAddress(personalSignPreimage, signature)
console.log(recovered === '0xa74772264f896843c6346ceA9B13e0128A1d3b5D')
```

## Sui SBTs

#### TypeScript

We also allow users to mint SBTs on Sui. The following code verifies that the address possesses a Clean Hands SBT on Sui.

```typescript
import { SuiClient, getFullnodeUrl } from '@mysten/sui.js/client';
const PACKAGE_ID = '0x53ddebd997f0e57dc899d598f12001930e228dddadf268a41d4c9a7c1df47a97';
const SBT_TYPE = `${PACKAGE_ID}::sbt::SoulBoundToken`;
const client = new SuiClient({
    url: getFullnodeUrl('mainnet'),
});
async function hasSBT(address: string): Promise<boolean> {
    try {
        // Query all objects owned by the address
        const objects = await client.getOwnedObjects({
            owner: address,
            options: { showType: true }, // Include object type in the response
        });
        // Check if any object matches the SBT type
        const hasSbt = objects.data.some((obj) => {
            const type = obj.data?.type;
            return type === SBT_TYPE;
        });
        return hasSbt;
    } catch (error) {
        console.error('Error querying SBT ownership:', error);
        return false;
    }
}
```

#### Move

See the example [SBT verifier package](https://github.com/holonym-foundation/id-hub-contracts/blob/main/contracts/sui/sbt-verifier/sources/sbt_verifier.move) for how to verify the SBT on chain.

## Decrypt

See [Decryption of Provably Encrypted Data](https://docs.mishti.network/usage-instructions/making-requests-to-mishti-network/decryption-of-provably-encrypted-data).


# Stellar

How to read Human ID SBTs on Stellar

Users can opt to receive SBTs on Stellar. See [Using Human ID with Stellar](/for-users/using-with-stellar) for user-facing instructions.

### Off-chain

Use the Stellar SDK to simulate a read transaction to get the user's SBT.

```typescript
import {
  Horizon,
  rpc,
  TransactionBuilder,
  Networks,
  Contract,
  scValToNative,
  nativeToScVal,
} from '@stellar/stellar-sdk'

/// Types ///
type StellarSbt = {
  action_nullifier: bigint,
  circuit_id: bigint,
  expiry: bigint,
  id: bigint,
  minter: string, // Stellar address
  public_values: Array<bigint>,
  recipient: string, // Stellar address
  revoked: boolean
}
type StellarSbtStatus = 'valid' | 'expired' | 'revoked' | 'none'
type GetStellarSBTRetVal = {
  sbt?: StellarSbt
  status: StellarSbtStatus
}

/// Constants ///
const horizonServerUrl = 'https://horizon.stellar.org'
const sorobanRpcUrl = 'https://mainnet.sorobanrpc.com'
const stellarSBTContractAddress = 'CCNTHEVSWNDOQAMXXHFOLQIXWUINUPTJIM6AXFSKODNVXWA4N7XV3AI5'

/// Get SBT ///
async function getStellarSBTByAddress(
  address: string,
  circuitId: string
): Promise<GetStellarSBTRetVal | null> {
  const sorobanServer = new rpc.Server(sorobanRpcUrl)
  const userAccount = await sorobanServer.getAccount(address)
  const contract = new Contract(stellarSBTContractAddress)

  const operation = contract.call(
    'get_sbt',
    nativeToScVal(address, { type: 'address' }),
    nativeToScVal(circuitId, { type: 'u256' })
  )

  const transaction = new TransactionBuilder(userAccount, {
    Networks.PUBLIC,
    fee: '100',
  })
    .addOperation(operation)
    .setTimeout(60)
    .build()

  const simulationResponse = await sorobanServer.simulateTransaction(transaction)
  const parsed = rpc.parseRawSimulation(simulationResponse)

  // Happy path. User has the desired SBT.
  if (rpc.Api.isSimulationSuccess(simulationResponse)) {
    const _parsed = parsed as rpc.Api.SimulateTransactionSuccessResponse

    if (!_parsed.result) {
      throw new Error('Unexpected: Could not get "result" field from parsed Stellar transaction simulation for SBT query')
    }

    const sbt = scValToNative(_parsed.result?.retval)

    return {
      sbt,
      status: 'valid'
    }
  }
  // Error cases. User does not have the SBT.
  else if (rpc.Api.isSimulationError(simulationResponse)) {
    const _parsed = parsed as rpc.Api.SimulateTransactionErrorResponse

    // Error code 1 is "SBT not found"
    if (_parsed.error.includes('HostError: Error(Contract, #1)')) {
      return { status: 'none' }
    }

    // Error code 5 is "SBT revoked"
    if (_parsed.error.includes('HostError: Error(Contract, #5)')) {
      return { status: 'revoked' }
    }

    // Error code 6 is "SBT expired"
    if (_parsed.error.includes('HostError: Error(Contract, #6)')) {
      return { status: 'expired' }
    }

    throw new Error(`Stellar transaction simulation for SBT query failed: ${_parsed.error}`)
  } else {
    throw new Error('Unexpected: Stellar transaction simulation for SBT query returned an unexpected response')
  }
}

```

### On-chain (Soroban)

#### 1. Fetch the wasm for the Human ID SBT contract.

```bash
soroban contract fetch --id CCNTHEVSWNDOQAMXXHFOLQIXWUINUPTJIM6AXFSKODNVXWA4N7XV3AI5 -o ./human_id_sbt_contract.wasm --network mainnet --network-passphrase "Public Global Stellar Network ; September 2015" --rpc-url https://mainnet.sorobanrpc.com
```

#### 2. Incorporate into your Soroban contract.

This example contract has a `get_sbt` function that simply wraps the function from the SBT contract.

Check out the full [Human ID SBT contract here](https://github.com/holonym-foundation/id-hub-contracts/blob/main/contracts/stellar/human-id/contracts/sbt/src/lib.rs).

For the list of circuit IDs, see [here](https://github.com/holonym-foundation/holonym-api/blob/235c57310781e66d19cf5ad675132088d266da87/src/constants/misc.js#L18C1-L25C72).

```rust
#![no_std]
use soroban_sdk::{contract, contractimpl, Env, Address, U256};

mod human_id_sbt_contract {
    soroban_sdk::contractimport!(
        file = "../../human_id_sbt_contract.wasm"
    );
}

#[contract]
pub struct MyContract;

#[contractimpl]
impl MyContract {
    /// * `recipient` - The address of the SBT recipient.
    /// * `circuit_id` - This determines the type of SBT to look up. For example, KYC, ePassport, and phone number
    ///   SBTs each have a different circuit ID.
    pub fn get_sbt(env: Env, recipient: Address, circuit_id: U256) -> human_id_sbt_contract::SBT {
        let human_id_sbt_contract_addr = Address::from_str(&env, "CCNTHEVSWNDOQAMXXHFOLQIXWUINUPTJIM6AXFSKODNVXWA4N7XV3AI5");
        let human_id_sbt_client = human_id_sbt_contract::Client::new(&env, &human_id_sbt_contract_addr);

        human_id_sbt_client.get_sbt(&recipient, &circuit_id)
    }
}
```


# Run an Observer

If you want to run an observer, please reach out to the Human ID team.

## Overview

The observer is a component of the Clean Hands Stack for programmable privacy, used for GDPR-compliant storage of encrypted user data with the DecryptBabyJubJub method for KYC.

The observer is a primary component in the Clean Hands stack with [**Human ID**](https://docs.holonym.id). To interact with the observer, a user generates a ZKP they have passed sanctions checks, and this ZKP outputs the ciphertext of the user's personal identifiable information (PII) and the user's associated blockchain address. The Observer's role in this system is to verify ZKPs, issue attestations to users with valid ZKPs, and to store the public outputs of these ZKPs so that the ciphertext can be decrypted if Human Network permits.

## Endpoints

### POST /observations

This endpoint does the following.

* Verify the Clean Hands ZKP. Uses [this circuit](https://github.com/holonym-foundation/id-hub-contracts/blob/main/zk/circuits/circom/V3CleanHands.circom) to verify a proof which should have been generated using [this package](https://www.npmjs.com/package/wasm-vole-zk-adapter).
* Make sure the encryption key output by the circuit is Human Network’s public key.
* Make sure the issuer address output by the circuit is the configured clean hands issuer.
* Make sure the conditions contract signed by the user is on our whitelist.
* Verify the user’s signature of the conditions contract.
* Store the ZKP’s public values, user's address, user's signature, and signed access contract in the `observations` collection.
* Issue an attestation on Sign Protocol.

### POST /observations/sui

Identical to `POST /observations` but mints an SBT on Sui instead of issuing an attestation on Sign Protocol.

### GET /observations?user\_address=\<address>

This endpoint queries the database for an observation for the provided user address and returns the result.

## Schemas

```rust
pub struct ObservationSchema {
    /// Distinct from _id. This is a hash of the fields of the observation. Allows for 
    /// more efficient lookups to make sure we don't store the same observation twice.
    pub id: String,
    pub user_address: String,
    pub signature: String,
    pub access_contract: String,
    pub zkp_public_values: Vec<String>,
}
```

## Environment variables

Create a .env file with the following variables. All are necessary.

```bash
# You might want to modify the following
MONGODB_URI=mongodb://localhost:27017
CLEAN_HANDS_ISSUER_ADDRESS=3953516660401541564649985379958697237340496801951929947163239598560489169274

# The following variables MUST be changed
ATTESTOR_PRIVATE_KEY=123 
OP_RPC_URL=abc
SUI_PRIVATE_KEY=123
SUI_PRIVATE_KEY_SCHEME=ed25519
```

`MONGODB_URI` - URI for MongoDB. The observer stores ZKP outputs, the user's blockchain address, the user's signature, and the address of the access conditions contract in a collection titled "observations".

`CLEAN_HANDS_ISSUER_ADDRESS` - The address that issued the credentials used as inputs to the ZKP. This is used to validate the issuer address output by the ZKP.

`ATTESTOR_PRIVATE_KEY` - The private key of the account used to issue attestations. This private key is used to create transactions on Optimism. It's account remain funded; otherwise attestations will not be issued.

`OP_RPC_URL` - URL for Optimism RPC node.

`SUI_PRIVATE_KEY` - The output of the `sui keytool export` command.

`SUI_PRIVATE_KEY_SCHEME` - ed25519 | secp256k1 | secp256r1

## Run

```bash
docker pull holonym/observer
```

```
docker run --env-file .env holonym/observer
```


# Overview

How Holonym works

### Actors and actions in Holonym

At a high level, the Holonym system can be understood in terms of the following *actors* and *actions*.

#### **Actors**

* **User.** Someone who receives credentials and prove facts about themselves, often for the purpose of fulfilling requirements set by an *organization*.
* **Issuer.** A party that issues credentials to users. In the process of issuing credentials, an issuer may use 3rd party identity providers to verify users' identities.
* **Consumer.** A party that modifies its users' access or actions based on the facts its users have proven about themselves using. For example, an organization may require its users to reside in a certain country in order to vote on certain proposals.

#### **Actions**

1. Issuer issues a credential to a user. In the user flow, this step is called "issuance".
2. User proves facts about themselves using their credentials. In the user flow, this step is called "proving".
3. Consumer allows a user to access something or to perform an action, depending on what the user has proven.


# Flow of Data

## Architecture

The Human ID protocol consists of the following components:

* Client (website or mobile app)
* Issuers (either using the Human ID credential format or custom formats such as ICAO9303 for NFC passports issued by the government)
* Hub smart contracts (1 per chain)
* Relayer

The flow of data is outlined in the action sequence described in the [Overview](/architecture/overview#actions) section. The following describes the flow of data in more detail. The diagram illustrates the *issuance+storage* (designated by 1.\*) and *proving* (designated by 2.\*) steps. An organization can grant access to users based on the results stored in the proof contract.

<figure><img src="/files/3OS5jdsan1QmR35OFpPy" alt=""><figcaption><p>Previous V2 architecture. In V3 storage is done in the Human Wallet, there is no Roots smart contract, and the Proof smart contract is called the Hub.</p></figcaption></figure>

1.1. User verifies themself to an issuer

1.2. The issuer signs credentials

1.3. The user generates a proof that credentials were sigend with particular attributes

1.5. User encrypts their credentials client-side. The encryption key is generated from the user's signature.

1.6. User stores their encrypted credentials in the Human Wallet

2.1. User generates a proof and (e.g., a proof that they are a US resident) and submits it to the verifier

2.2. The verifier attests to it and returns it to either the user or the relayer, depending on how the attestation is to be consumed

2.3. The attestation is given to the recipient: either the Hub smart contract or an offchain consumer


# Flow of Data: KYC Proof of Personhood

Human ID's Proof of Personhood via KYC consists of the following components:

* User agent (UI)
* Human ID server
* ID verification provider
* Verifier

The flow of data is outlined in the following sequence diagram. Please refer to notes for detailed explanations for relevant parts.

{% @mermaid/diagram content="sequenceDiagram
participant U as User / User agent
participant Z as Human  ID server
participant IDV as IDV Provider
participant V as Verifier

```
Note over U, Z: 1. Initiate ID verification
Note left of U: User visits /gov-id
U->>Z: Initiate ID verification process
Z->>Z: Create session
Z->>U: Request payment
U->>Z: Make payment
Z->>Z: Verify payment and save TX hash
Note over U, IDV: 2. Complete ID verification
Z->>IDV: Request IDV session
Note right of IDV: IDV Providers: Veriff, Onfido, Facetec
IDV->>Z: Return IDV session
U->>U: Render IDV session on UI
U->>IDV: User submits selfie and document to complete verification
Note right of IDV: Refer to notes on handling of user data by IDV Providers
IDV->>Z: Return IDV session result
Z->>U: Return signed IDV result
U->>U: IDV session result is encrypted on client-isde
Note right of U: Refer to notes on client-side encryption
U->>Z: Ciphertext stored
Note right of Z: Refer to notes on ciphertext storage
Note over U, V: 3. Proof of ID uniqueness
U->>U: Generate ZKP for uniqueness
U->>V: Send ZKP to verifier
V->>V: Verifier verifies the ZKP
V->>V: Issue SBT with embedded metadata
Note right of V: Refer to notes on SBT issuance
V->>U: Soul-bound token is sent to the specified address on the specified chain" %}
```

#### Issuance and Proving

Sections 1 and 2 in the sequence diagram constitute *issuance*. This is where the user's private credentials are issued.

Section 3 is proving, where the user proves facts about their issued credentials.

#### Notes on handling of user data by IDV Providers

Following data are requested by IDV providers as photo or/and video stream during the verification process.

* Selfie (photo, video stream)
* One of the following documents
  * Passport
  * Driver License
  * Identity Card

Currently, following IDV providers are supported.

* [Veriff](https://trust.veriff.com/)
* [Onfido](https://onfido.com/privacy/)
* [Facetec](https://dev.facetec.com/privacy-site)

**Veriff** has clearly outlined in its [trust center](https://trust.veriff.com/)

* a list of compliances (i.e: GDPR)
* regarding data collection, retention and deletion [controls](https://trust.veriff.com/controls#data-and-privacy)
  * a list of [subprocessors](https://trust.veriff.com/controls#documentation)

**Onfido** has its [privacy policy](https://onfido.com/privacy/)

* a list of [compliances](https://onfido.com/company/certifications/)
* regarding data
  * [collection](https://onfido.com/privacy/#toc-2-the-information-we-collect-and-how-we-use-it-on-behalf-of-our-clients-3)
  * [processing](https://onfido.com/privacy/#toc-3-using-information-as-controller-4)
  * [security](https://onfido.com/privacy/#toc-5-information-security-6)
  * [storage](https://onfido.com/privacy/#toc-6-data-storage-7)
* ControlCase has issued compliance certificate for ISO 27001

**Facetec** has two privacy policies ([site](https://dev.facetec.com/privacy-site) and [sdk](https://dev.facetec.com/privacy-sdk))

> *SDK privacy policy seems more relevant for usage for ID verification. Its documentation on privacy is sparse compared to the other 2 providers.*

* In article #2, it mentions that any data sent to its server is encrypted, siloed and is never stored with any additional personally identifiable information (PII).
* In article #6, it provides detailed info on its compliance to GDPR for EU residents.

#### Notes on client-side encryption of IDV session result

IDV provider returns the session result to user.

With Human Wallet:

The result is encrypted on client-side using a derivative of the PRF.

With other wallets:

The result is encrypted with key derived with `hash(userSignature(aConstantMessage))` to generate ciphertext.

#### Notes on ciphertext and storage of userCredentials

Only the encrypted ciphertext which is non PII is stored in Human ID database as below.

```json
// userCredentialsv2
{
  "_id": {
    "$oid": "676d..."
  },
  "holoUserId": "f111...",
  "encryptedGovIdCreds": {
    "ciphertext": "0x...",
    "iv": "0x...",
    "_id": {
      "$oid": "676d..."
    }
  },
  "__v": {
    "$numberInt": "0"
  }
}

```

View [Credentials](/how-it-works/credentials#government-id-issuer)to see the data included in user credentials.

#### Notes on verifier and SBT issuance

The user submits a zero knowledge proof of uniqueness ([see the circuit here](https://github.com/holonym-foundation/id-hub-contracts/blob/main/zk/circuits/circom/V3SybilResistance.circom)) to the verifier server. The verifier verifies the ZKP, and upon verification, issues a soulbound token to the user. The circuit ID, issuer address, expiry, and actionNullifier, the ZK proof are embedded in the Soul-bound token.


# Where Data Is(n't) Stored

## Credential storage

Credentials are stored by the Human Wallet self-custodial wallet so that other than the user who owns them, nobody can access them.

When the user scans government IDs, the data is not stored in Holonym servers. For modern NFC-based ID and Aadhaar verification, a ZKP is created without Holonym ever seeing the data. For older types of government ID verification that do not involve ePassport NFC reading, the user data briefly passes through Holonym servers so that Holonym can sign the credentials, attesting to their authenticity, to return to the user. Data is immediately deleted from Holonym servers and from the KYC provider's servers after the signature is generated. Even if the user does legacy credential verification and does not trust a KYC provider, the protocol gives additional privacy guaranteed: Each user's ID is anonymized in what we call the **Privacy Pool**, i.e. an anonymity set. The Privacy Pool exists to break the link between a user's identity and a user's crypto address. This creates a level of privacy where even if Holonym, an issuer, or anyone tries to surreptitiously collect user data, they still cannot identify user wallets.

## Proof storage

Proofs can be submitted on-chain or off-chain. Proofs, such as "I am a unique person" should not contain sensitive information. The Holonym website and app only permit proofs that do not give sensitive data, regardless of whether they're on- or off-chain.


# VOLE-based ZK

## What it enables

We implemented an ([open source](https://github.com/holonym-foundation/vole-zk-prover)) VOLE-based prover to enable fast identity proofs that otherwise aren't practical to do with privacy. Privacy requires all proofs to be done on consumer hardware without expensive servers. Yet most interesting proofs such as NFC-enabled passports and email proofs require expensive RSA, SHA256, or Keccak256 operations. The prover can take the existing circuits in circom, where these primitives have already been implemented and audited. We have utilized this to allow mobile browsers to perform proof of passport in seconds without running out of memory.

## How it works

We use a variant of VOLE-based ZK called [VOLE in the head](https://eprint.iacr.org/2023/996), where we modified the linear code to be the [repeat-multiple-accumulate (RMA) code](http://pfister.ee.duke.edu/papers/isita2000.pdf). VOLE-based ZK is orders of magnitude more efficient than popular proving systems, but it is not used for rollups because it is not succinct and difficult to make noninteractive. Yet for identity, especially when offchain, VOLE-based ZK is ideal. VOLE in the head renders VOLE-based ZK noninteractive with roughly a 2x performance cost. We replace the repetition code with the RMA code to have faster performance.

&#x20;VOLE-based ZK consists of two phases:

### 1. Commitment / Preprocessing:

VOLE is performed, giving a two-party "commitment": the verifier receives functions describing many lines with the same slope, and the prover receives random points on each of these lines. The parties do not learn the other parties' values. Notice that by showing a verifier one of his points, a prover can open one of his commitments.&#x20;

A derandomized step is performed so instead of commitment to random values, the parties commit to the witness.

### 2. Proving:

To see the committed output of any addition gates, the verifier just adds her commitments. In fact, any linear operation is trivial because the VOLE commitment scheme is linearly homomorphic.

For multiplication gates, a bit more effort is required: the prover must send the commited values and prove they are correct. We use [quicksilver](https://eprint.iacr.org/2021/076.pdf) for this.

Finally, after all of the circuit's gates' outputs have been learned by the verifier, she can ask the prover to open any of the public outputs.

### Making It Noninteractive

This involves a special type of VOLE based off of SoftSpokenOT, in which the outputs that the prover recieves are independent of the verifier's choices of randomness. This allows the verifier's randomness to be created by the Fiat-Shamir heuristic. However, with this variant of VOLE, the verifier's choices of random values are limited to a very small set, making it too easy for the prover to guess the verifier's choices -- in our case, the verifier has only two options, so this is highly insecure.&#x20;

As a result, linear codes are used to enforce that the prover must guess at least *d* secrets the verifier chose, where *d* is the minimum distance of the linear code. For more information see the [VOLE in the head](https://eprint.iacr.org/2023/996) paper.


# On-Chain Proofs

To put the proofs on-chain, a Verifier and Relayer are used. The Verifier attests to the proof being complete (since the proof size is too large to cheaply be put on-chain ) and the Relayer posts the attestations on-chain to the Hub contract. *We plan to progressively decentralize this Verifier by achieving consensus amongst a permissionless set of verifiers.*

### Hub Contract

Before a user can submit a proof on-chain, the Hub ensures&#x20;

1. An allowed verifier, either a server, network, or smart contract, has verified a VOLE-based ZKP
2. This verifier has posted any necessary data off-chain so others can check its steps in verifying the proof
3. Any nullifier revealed by the ZKP has not been spent

Once a proof is on-chain, the Hub sends a non-transferable ERC721 representing a SBT. The Hub also allows reading the relevant metadata pertaining to the SBT, i.e. the public values of the corresponding proof, via the method `getSBT`.

{% hint style="danger" %}
**Attention** The Hub does not check any custom logic on the public values of the proof beyond the following two standard public input values: nullifier and expiry. While it checks the nullifier has not been used before and the expiry has, it cannot check custom logic. For example, some proofs require that you check the credential's issuer given in the public outputs is actually a trusted issuer of credentials. To do so, you must call `getSBT`and check the public value corresponding to the issuer address is valid.
{% endhint %}

For more information, you may look at the [source code](https://github.com/holonym-foundation/id-hub-contracts/blob/main/contracts/Hub.sol)


# Clean Hands Architecture

Human ID's Proof of Clean Hands establishes that a user

1. has completed KYC,
2. has verified that they are not on any sanctions or PEP lists,
3. has encrypted their identifying data to Human Network, and
4. has selected a smart contract that determines the conditions under which their data can be decrypted.

## Components

The Clean Hands system is made possible by the following:

* Credential issuer.
* Zero knowledge circuit.
* Observer node.
* Smart contract(s).
* Human Network.

The novelty of the Clean Hands system is in its encryption and decryption. Each user encrypts their data to Human Network and *proves* encryption. When they encrypt, they select a smart contract that determines who is allowed to decrypt. The user's ciphertext is sent to an "observer" node. An entity granted access to the user's ciphertext by the observer node and granted decryption rights by the smart contract can decrypt the user's ciphertext.

## Flow

There are five steps in the Clean Hands flow.

#### 1. Credential issuance

The user verifies their identity and that they are not on any sanctions or PEP lists. Human ID's credential issuer issues a signature to the user attesting to the veracity of the user's identity data and sanctions list status.

#### 2. Proof generation

The user generates a zero knowledge proof. This proof uses the credentials and signature issued by the credential issuer. The proof establishes that the user has completed KYC and sanctions list checks and that they have encrypted their name and date of birth to Human Network. At this step, the user also signs, using the ephemeral private key used in encryption, the address of a smart contract. This smart contract determines who can decrypt the user's data.

#### 3. Attestation issuance

The user sends the proof to an observer node. The observer verifies the proof, stores the proof's public outputs so that the user's ciphertext can be retrieved at a later date by authorized entities, and issues an on-chain attestation to the user. This attestation attests to the fact that the user has completed all of these first 3 steps.

#### 4. On-chain activity

With the attestation, the user is now able to interact with smart contracts that require proof of clean hands, for example, [the Ethereum-Aztec bridge](https://github.com/holonym-foundation/aztec/) which allows verified users to transaction privately.

#### 5. Decryption

If necessary, the ciphertext from the user’s zero knowledge proof is decrypted. The entity that wants to decrypt must request the user's ciphertext from the observer. They must also be granted decryption rights from the smart contract associated with the ciphertext. With the ciphertext and decryption rights, the decrypter requests decryption from Human Network.

## Lists checked for Proof of Clean Hands

* US / OFAC Capta List (CAP)
* US / OFAC Non-SDN Chinese Military-Industrial Complex Companies List (CMIC)
* US / BIS Denied Persons List (DPL)
* US / DOS ITAR Debarred (DTC)
* US / BIS Entity List (EL)
* INT / Financial Action Task Force
* US / FBI Most Wanted
* US / FINCEN Financial Crimes Enforcement Network - 311
* US / OFAC Foreign Sanctions Evaders (FSE)
* INT / Interpol Red Notices
* US / DOS Nonproliferation Sanctions (ISN)
* US / BIS Military End User (MEU) List
* US / OFAC Consolidated Sanctions List (Non-SDN Lists)
* US / OFAC Non-SDN Menu-Based Sanctions List (NS-MBS List)
* US / HRJ US Department of the Treasury Sanctioned Countries
* US / HRJ OFAC Sanctioned Countries (Military)
* US / HRJ OFAC Sanctioned Countries
* INT / Politically Exposed Persons
* US / OFAC Palestinian Legislative Council List (PLC)
* US / OFAC Specially Designated Nationals (SDN)
* US / OFAC Sectoral Sanctions Identifications List (SSI)
* US / DOS Cuba Restricted List


# Modularity of the Stack

Holonym creates modular infrastructure for privacy, flexibility, and security.

Holonym applies modularity to the identity verification stack. This fixes a trilemma of security, flexibility, and privacy in identity verification. It has historically been difficult to combine the benefits of centralization (e.g., rigor and ease of use) with decentralization (e.g., flexibility, privacy, and composability). Holonym introduces four layers of identity verification to enable "best of both worlds" approaches.

1. Issuance layer
2. Privacy layer
3. Audit layer
4. ReLayer

## Issuance layer

Unlike blockchains which benefit from being decentralized, *some* use cases of identity benefit from being centralized. The most common form of rigorous identification is government ID, as centralized as possible. Governments have successfully disbursed over $1 trillion in annual benefits with a relatively low amount of benefit fraud through identity theft. They have been able to conduct large-scale elections using identity to prevent fraudulent votes. These have been possible due to extensive personal information governments are privy to.&#x20;

## Privacy layer

While there is clearly utility (e.g., social security, secure voting processes) in having an institution like a government privy to personal information, the other risks of centralization should be mitigated. Thus, we introduce a privacy layer to separate the issuer from all other aspects of the protocol. This way, an issuer's power is only limited to verifying once; it cannot track subsequent actions of a user.

The privacy is obtained by a global Merkle tree of all credentials. For more information, see

{% content-ref url="/pages/16wbIPymElqTff0JMzSg" %}
[Hub](/how-it-works/hub)
{% endcontent-ref %}

or

{% content-ref url="/pages/bnh77tK8P1V3jgqIoJtP" %}
[Credentials](/how-it-works/credentials)
{% endcontent-ref %}

## Audit Layer

Regulators often need to investigate activities when something goes wrong. Not having identity data available for regulators is illegal in most countries and can have penalties exceeding $100M. Currently, companies record all user activity to have an audit record for regulators, as there has been no other way to comply. In current systems, absent of a way to selectively enforce data privacy, every user is monitored regardless of whether or not they are being investigated.&#x20;

However, in Holonym, the user can be required to encrypt their own audit trail to the data audit layer. The audit layer should have two functions: data availability and permissioned access, which can in turn be two separate layers. Permissioned access involves a smart contract that ensures that nobody can unscrupulously spy on user activity without proper consent or a court order. It enforces that only certain actors under certain conditions can decrypt the data, allowing for provable privacy for honest users while retaining regulatory oversight for bad actors. These conditions are enforced via a smart contract rather than trusting a third party to maintain privacy.

## ReLayer

The relayer layer is the way in which zero-knowledge proofs are sent. Sending and receiving reveal IP addresses and/or wallet addresses if gas is needed. Currently, Holonym uses a relayer to submit transactions and hide the address they came from. It is important that this layer remains flexible so that a relayer node can be replaced by a relayer network, and mixnets can be employed to hide IP addresses.

&#x20;


# Issuer

Anybody can make an issuer, as this protocol is decentralized at the base layer. Of course, issuers of credentials should be trusted, despite the fact that anybody can create an issuer. Furthermore, Holonym Foundation operates the frontend and can integrate the issuer into the frontend, so we recommend reaching out in the discord to talk to us and the community before making a custom issuer.


# Credentials

Credentials are signed attestations by an issuer. While it supports legacy credential types, Human  ID has a standard format for such credentials designed for simplicity and efficiency in a ZKP.

## Human ID Standard Credential Format <a href="#what-is-a-leaf" id="what-is-a-leaf"></a>

A leaf is an element of a Merkle tree. Merkle trees allow for efficient and anonymous proofs of set membership in zero-knowledge. Leaves are Poseidon hashes of 6 254-bit field elements:

<table><thead><tr><th width="122">Index</th><th width="302">Name</th><th>Description</th></tr></thead><tbody><tr><td>0</td><td>Issuer Address</td><td>The Ethereum address of the credential issuer, i.e. who provided this attestation</td></tr><tr><td>1</td><td>Nullifier Secret</td><td>A secret used to create nullifiers</td></tr><tr><td>2</td><td>Issuer-defined field</td><td>Can be used for anything the issuer wants</td></tr><tr><td>3</td><td>Issuer-defined field</td><td>Can be used for anything the issuer wants</td></tr><tr><td>4</td><td>iat</td><td>Unix timestamp in seconds when the credential was issued at</td></tr><tr><td>5</td><td>Scope</td><td>Default to 0 (wildcard) indicating it can be used anywhere. This field is mostly deprecated and is not used anywhere.</td></tr></tbody></table>

Note that while credentials can only have 2 issuer- fields, nothing prevents these fields from representing more: they can be set to the hash of unlimited custom fields

#### Nullifier Secret

The *nullifier secret* is a value that only the user knows. Because it is pseudorandom, the preimage of the leaf cannot be guessed without at least brute-forcing the secret, which is infeasible at 16 bytes. Thus, even if the other fields are predictable, the credential will still be anonymous without knowing the secret. In this way, it acts as a pepper, obscuring the preimage from the publicly known digest.

It also has a particular property allowing for sybil resistance: credentials can be spent for a specific use case by publishing a salted hash of the secret pepper. We refer to this as a **hashbrown** because it is a mixture of salt & pepper\_.\_ Accurate computation of the hashbrown can be verified in a ZKP without revealing the pepper. Some use cases of involve anonymous reputation, anonymous voting, bot prevention, and fair airdrops. For example

> Whenever a user votes in an election with actionID `abc123`, they must publish `hash(abc123, secret)` . And they generate a proof that the result came from `abc123` and `secret` . This hash then gets added on-chain, so the user can never vote again without people seeing they're trying to "double-spend" their secret.

## Leaf Formats of Holonym Foundation Issuers

Holonym Foundation provides two issuers, though others are encouraged to create more issuers.

### Government ID Issuer

<table><thead><tr><th width="122">Index</th><th width="302">Name</th><th>Description</th></tr></thead><tbody><tr><td>0</td><td>Issuer Address</td><td>The Ethereum address of the credential issuer, i.e. who provided this attestation</td></tr><tr><td>1</td><td>Nullifier Secret</td><td>A secret used to create nullifiers</td></tr><tr><td>2</td><td>Country</td><td>This is a number corresponding to the user's country. Countries are numbered based on a custom accumulator scheme. This allows them to be used in efficient ZK arguments of presence in allowlists of countries.</td></tr><tr><td>3</td><td>Additional Info</td><td>Poseidon hash of: Name, city, state/province, street, zipcode</td></tr><tr><td>4</td><td>Time of credential issuance</td><td>Represented as seconds since 1900 (unix timestamp with -70yr offset)</td></tr><tr><td>5</td><td>Scope</td><td>0</td></tr></tbody></table>

### Phone Issuer

<table><thead><tr><th width="122">Index</th><th width="302">Name</th><th>Description</th></tr></thead><tbody><tr><td>0</td><td>Issuer Address</td><td>The Ethereum address of the credential issuer, i.e. who provided this attestation</td></tr><tr><td>1</td><td>Nullifier Secret</td><td>A secret used to create nullifiers</td></tr><tr><td>2</td><td>Phone Number</td><td>Phone number in E.164 but without the leading + so it can be converted to a field element. E.g., +15555555555 becomes 15555555555</td></tr><tr><td>3</td><td>Empty</td><td>0</td></tr><tr><td>4</td><td>iat</td><td>Day the credential was issued at, in days since 1900/01/01</td></tr><tr><td>5</td><td>Scope</td><td>0</td></tr></tbody></table>

### Biometrics Issuer

<table><thead><tr><th width="122">Index</th><th width="302">Name</th><th>Description</th></tr></thead><tbody><tr><td>0</td><td>Issuer Address</td><td>The Ethereum address of the credential issuer, i.e. who provided this attestation</td></tr><tr><td>1</td><td>Nullifier Secret</td><td>A secret used to create nullifiers</td></tr><tr><td>2</td><td>Group Name</td><td>1 (this maybe incremented)</td></tr><tr><td>3</td><td>Reference Hash</td><td>Hash of the reference id</td></tr><tr><td>4</td><td>iat</td><td>Day the credential was issued at, in days since 1900/01/01</td></tr><tr><td>5</td><td>Scope</td><td>0</td></tr></tbody></table>


# Hub

The contract where everything happenson-chain

Before a user can submit a proof on-chain, the Hub ensures&#x20;

1. An allowed verifier, either a server or smart contract, has verified a VOLE-based ZKP
2. This verifier has posted any necessary data off-chain so others can check its steps in verifying the proof
3. Any nullifier revealed by the ZKP has not been spent

Once a proof is on-chain, the Hub sends a non-transferable ERC721 representing a SBT. The Hub also allows reading the relevant metadata pertaining to the SBT, i.e. the public values of the corresponding proof, via the method `getSBT`.

{% hint style="danger" %}
**Attention** The Hub does not check any custom logic on the public values of the proof beyond the following two standard public input values: nullifier and expiry. While it checks the nullifier has not been used before and the expiry has, it cannot check custom logic. For example, some proofs require that you check the credential's issuer given in the public outputs is actually a trusted issuer of credentials. To do so, you must call `getSBT`and check the public value corresponding to the issuer address is valid.
{% endhint %}

For more information, you may look at the [source code](https://github.com/holonym-foundation/id-hub-contracts/blob/main/contracts/Hub.sol)


