Overview
Verified Human is the credential the Billions Wallet issues after a user completes document-based verification, an NFC passport, Aadhaar, driving license, or national ID. 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 Verified Human credential from an issuer you accept. It does not check any field inside the credential. In particular, it does not establish a specific age, nationality or residence. 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 Verified Human 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 Verified Human 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 document 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 document-verified 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:circuitId: the circuit used to generate and verify the proof.allowedIssuers: the DIDs allowed to have issued the credential. Verified Human accepts proofs from either of Billions Network’s issuers listed above.context: the schema context for theBasicPersontype, hosted on IPFS.type: the credential type being requested, alwaysBasicPersonfor Verified Human.
Need a custom query for your specific requirements, whether that’s a variation on Verified Human, Proof of Uniqueness, or a combination of the two? Reach out to the Billions integration team to discuss it.
What this example verifies. It checks that the user holds a valid credential of this type from an accepted issuer. It does not test any field inside the credential. To ask a question such as “is the user at least 18?” or “is their country of residence the US?”, the credential must contain that field and the query must include a supported condition.
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
Example Repository
The steps below follow the Billions Wallet verifier example. Clone it to follow along, and setUSE_CASE=POVH in js/.env to select the Verified Human query above. The same backend verifies Proof of Uniqueness with USE_CASE=POU.
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, and NULLIFIER_SESSION_ID are read from configuration. You choose NULLIFIER_SESSION_ID, once per campaign; the Verified Human issuer DIDs and schema context are the same values shown in Verification Query Structure above.
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 Verified Human 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.
Verified Human
What the credential is, and how users claim it.