Overview
Proof of Uniqueness is the credential the Billions Wallet issues after a user completes their one-time face scan. Any backend can request proof of this credential from a user who holds it, then confirm the proof on its own servers before granting access, distributing a reward, or unlocking a feature. This guide covers the full verification flow: requesting the credential, the user completing it in the Billions Wallet, and confirming the resulting proof on your backend. It confirms that the user holds a valid Proof of Uniqueness credential from an issuer you accept. It does not check any field inside the credential. See what this example verifies.This is a reference example to get you running locally and to show the shape of the integration. For a production rollout, reach out to the Billions integration team — they’ll provide the issuer and schema values your verifier needs, and can walk you through rate limits and the persistent storage your deployment requires.
Verification Flow
A verification request is a standard DID-authentication request with one addition: ascope naming the exact credential and issuer you’ll accept as proof.
1
User requests verification
Once a user is signed into your app, your frontend asks the backend for a verification request scoped to that user’s session.
2
Backend builds a scoped authorization request
The backend generates an authorization request and attaches a proof request to it, naming the Proof of Uniqueness credential it’s asking the user to present.
3
User opens the Billions Wallet
The request is encoded into a Universal Link. Opening it in the Billions Wallet shows the user exactly what’s being asked: proof that they hold a genuine Proof of Uniqueness credential.
4
Wallet generates the proof
If the user holds the credential, the wallet builds a zero-knowledge proof against it and signs a token, without exposing the underlying credential data.
5
Wallet posts to your callback
The wallet sends the signed token to your backend’s callback endpoint, tagged with the session ID from Step 1.
6
Backend verifies and checks for replay
Your backend verifies the token against the request it stored, then checks the proof’s nullifier, a privacy-preserving identifier unique to that person, to confirm the same human hasn’t already verified through a different session.
7
Session marked verified
Once the nullifier check passes, mark the session verified in your store. From here you can gate access, unlock a reward, or anything else that depends on the user being a real, unique human.
Verification Query Structure
This is the structure of the verification query used to make a verification request, naming the credential you’re asking the user to present:id: the session ID for this verification request.allowedIssuers: the DID allowed to have issued the credential.context: the schema context for the credential type.type: the credential type being requested, alwaysUniquenessCredentialfor PoU.
allowedIssuers and context are specific to your integration. Reach out to the Billions integration team to get these values.Session ID and nullifier session ID
Each verification uses two different IDs. Don’t mix them up:
Pick one
nullifierSessionId per campaign, as a positive integer, and keep it fixed. The same person then always produces the same nullifier, so a second attempt is caught. A different value gives different nullifiers, so one person can’t be linked across your campaigns. Changing it mid-campaign lets everyone verify again.
Setup Instructions
Prerequisites
- Public URL for callbacks (use ngrok for local development)
- A Verifier DID
- The Proof of Uniqueness issuer DID and schema context, from the Billions integration team
Example Repository
The steps below follow the Billions Wallet verifier example. Clone it to follow along. It contains an Express backend (js/), the ZK circuit keys it depends on (keys/), and a static demo frontend (static/).
1. Server and Request Handler
Sets up the server and the endpoint that builds a scoped verification request for each new session.HOST_URL, AUDIENCE_DID, ALLOWED_ISSUER, the schema context, and NULLIFIER_SESSION_ID are read from configuration. The first four come from onboarding. You choose NULLIFIER_SESSION_ID, once per campaign.
Get Your Verifier DID: Sign in to your Billions Wallet and copy your profile DID to use during setup. This is your
AUDIENCE_DID — it represents the entity doing the verification, which in this case is your application.Set
NULLIFIER_SESSION_ID once per campaign, as a positive integer, and don’t change it after the first verification. If it changes, every user gets a new nullifier, and people who already verified can verify again. See Session ID and nullifier session ID.2. Verification Callback
Verifies the proof the wallet sends back, and enforces that a given nullifier, a given real person, can only complete this verification once.userVerificationMap before marking a session verified is what stops the same human from claiming a reward twice under two different accounts.
Testing Steps
- Start the server:
node index.js - Visit
http://localhost:8080, the example ships a static demo page with a “Verify with Billions” button wired to the flow above. - Open the generated link in the Billions Wallet and complete verification with a Proof of Uniqueness credential.
- Poll
GET /api/status/<sessionId>(or watch the demo page) until the status flips tosuccess.
Troubleshooting
The callback endpoint never fires
The callback endpoint never fires
HOST_URL must be a publicly reachable URL, not localhost. Use ngrok for local development, and confirm the callback URI includes the sessionId query parameter.'No valid proof found in response'
'No valid proof found in response'
The wallet returned a proof, but not one matching the
circuitId and id your backend is looking for. This usually means the id on the proofRequest doesn’t match the sessionId used to build the callback URI. They must be the same value.'This person has already verified'
'This person has already verified'
The nullifier from this proof already has a
verified: true record, meaning this same human has completed verification through a different session already. This is the anti-Sybil check working as intended. If you see this during testing, the same person verified twice under the same NULLIFIER_SESSION_ID, because their nullifier doesn’t change between sessions. To test again, use a separate NULLIFIER_SESSION_ID for testing, or clear your stored nullifiers.Verification fails with a state resolution error
Verification fails with a state resolution error
The
resolvers map only knows about the networks it’s configured for (billions:main, privado:main). Confirm your RPC and contract configuration is correct, and that the RPC endpoint is reachable from your server.Learn More
How Verification Works
The request, the wallet, the signed token, and the nullifier check, in plain terms.
Credentials
What each credential proves, and how users claim it.
Proof of Uniqueness
What the credential is, and how users claim it.