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.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.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 walk through the POU 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, and the schema context are read from configuration, using the values provided during onboarding.
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.Generate
nullifierSessionId fresh for every request, as shown here. Computing it once at server startup and reusing it across sessions breaks the anti-replay check every subsequent verification depends on.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 unexpectedly during testing, it likely means you’re reusing the same PoU credential across test sessions, which is expected behavior, not a bug.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
Proof of Uniqueness
What the credential is, and how users claim it.