Additional Steps#
This section covers advanced topics including requesting different types of proofs, testing your integration, and understanding what you’ve built.
Requesting Additional Proofs#
Beyond age verification, you can request various identity attributes. There are two main types of statements:
Range Proofs (
AttributeInRange) - Prove a value is within a range WITHOUT revealing it (zero-knowledge)Reveal Attributes (
RevealAttribute) - Ask user to share the actual value
Age Verification (Zero-Knowledge)#
What it does: User proves they’re 18+ without revealing their birthdate.
Already implemented in our tutorial! Here’s how it works:
{
type: "AttributeInRange", // Range proof (zero-knowledge)
attributeTag: "dob", // Date of birth
lower: "19000101", // Must be after Jan 1, 1900
upper: getDobUpperBound(), // Must be before the age cutoff
}
Result: Verifier knows user is 18+, but never sees the actual birthdate.
Verify EU Citizenship (Zero-Knowledge)#
Use case: Content restricted to EU residents only.
Add to src/lib/config.ts:
// List of EU country codes (ISO 3166-1 alpha-2)
const EU_COUNTRIES = [
"AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR",
"DE", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL",
"PL", "PT", "RO", "SK", "SI", "ES", "SE",
];
export function getEUResidencyStatement() {
return [
{
type: "AttributeInSet" as const,
attributeTag: "countryOfResidence",
set: EU_COUNTRIES,
},
];
}
Use in verifier-service.ts:
statements: [
{
type: "AttributeInRange",
attributeTag: "dob",
lower: "19000101",
upper: dobUpperBound,
},
...getEUResidencyStatement(), // Add EU check
],
Result: Verifier knows user lives in the EU, but doesn’t know which specific country.
Reveal National ID Number#
Use case: KYC (Know Your Customer) for financial services - you need the actual ID.
Add to src/lib/config.ts:
export function getNationalIdRevealStatement() {
return [
{
type: "RevealAttribute" as const,
attributeTag: "nationalIdNo",
},
];
}
Use in verifier-service.ts:
statements: [
{
type: "AttributeInRange",
attributeTag: "dob",
lower: "19000101",
upper: dobUpperBound,
},
...getNationalIdRevealStatement(), // User will share their ID number
],
Result: User’s actual national ID number is revealed and included in the verified proof.
Warning
Privacy Note: Only request revealed attributes when absolutely necessary. Zero-knowledge proofs are always preferable.
Verify Document Type#
Use case: Only accept passport holders (not national ID cards).
export function getPassportOnlyStatement() {
return [
{
type: "AttributeInSet" as const,
attributeTag: "idDocType",
set: ["0"], // 0 = passport, 1 = national ID card
},
];
}
Reveal Name for Personalization#
Use case: Personalized greeting without full KYC.
export function getNameRevealStatements() {
return [
{
type: "RevealAttribute" as const,
attributeTag: "firstName",
},
{
type: "RevealAttribute" as const,
attributeTag: "lastName",
},
];
}
Use in verifier-service.ts:
statements: [
{
type: "AttributeInRange",
attributeTag: "dob",
lower: "19000101",
upper: dobUpperBound,
},
...getNameRevealStatements(), // Get first and last name
],
How to access revealed attributes:
After verification succeeds, the revealed values are in the proof:
// In your verify API route
const result = await verifyPresentation({...});
// Access revealed attributes from the credential subject
const credentialSubject = result.verificationAuditRecord.presentation
.verifiableCredential[0].credentialSubject;
// credentialSubject.statement contains the revealed attributes
const statements = credentialSubject.statement;
// Find revealed attributes
statements.forEach(stmt => {
if (stmt.type === "AttributeValue") {
console.log(`${stmt.attributeTag}: ${stmt.attributeValue}`);
// Example output: "nationalIdNo: N-1234"
// Example output: "firstName: John"
}
});
Best Practices#
✅ DO:
Use zero-knowledge proofs whenever possible (AttributeInRange, AttributeInSet)
Only reveal attributes when absolutely necessary
Clearly explain to users what you’re requesting and why
Store revealed attributes securely (encrypt, minimize retention)
❌ DON’T:
Request more attributes than you need
Use RevealAttribute for age verification (use AttributeInRange instead)
Store sensitive revealed data indefinitely
Share revealed attributes with third parties without consent
Testing Your Integration#
Start Everything#
# Terminal 1: Start verifier service
docker compose up concordium-verifier
# Terminal 2: Start your app
npm run dev
Test the Flow#
Click “Verify Age with Concordium ID”
SDK modal appears with QR code
Scan QR with Concordium ID app
Approve the age proof request in the app
Your web app receives the proof
Verifier service validates it
User gains access
Full Verification Flow:
sequenceDiagram
autonumber
participant Browser
participant YourAPI
participant Verifier
participant Wallet
participant Chain
Browser->>Browser: User clicks "Verify Age"
Browser->>YourAPI: POST /api/verification/create
YourAPI->>Verifier: POST /create-verification-request
Verifier->>Chain: Anchor VRA (tx hash stored)
Verifier-->>YourAPI: Return VPR + audit record ID
YourAPI-->>Browser: Return sessionId + VPR
Browser->>Browser: SDK shows QR code modal
Wallet->>Wallet: User scans QR code
Browser->>Wallet: WalletConnect session established
Browser->>Wallet: Send VPR (age ≥ 18 request)
Wallet->>Wallet: User approves
Wallet->>Wallet: Generate ZK proof locally
Wallet-->>Browser: Send VP (verifiable presentation)
Browser->>YourAPI: POST /api/verification/verify
YourAPI->>Verifier: POST /verify (VP + VPR + audit ID)
Verifier->>Verifier: Validate ZK proof
Verifier->>Chain: Anchor VAA (verification audit)
Verifier-->>YourAPI: "verified" + tx hash
YourAPI-->>Browser: Success
Browser->>Browser: Grant access
Debug Common Issues#
Issue: “Module not found” error with SDK
Solution: Remove --webpack flag from package.json:
"dev": "next dev" // not "next dev --webpack"
Issue: QR code not showing
Solution: Check browser console for WalletConnect Project ID errors. Ensure .env.local is loaded.
Issue: Verification fails
Solution: Check verifier service logs:
docker logs concordium-verifier
Issue: Session persists across page reloads
Solution: Clear WalletConnect IndexedDB in browser console:
indexedDB.deleteDatabase("WALLET_CONNECT_V2_INDEXED_DB");
What You’ve Built#
✅ Docker-based verifier service running locally
✅ WalletConnect integration for mobile wallet pairing
✅ Zero-knowledge age verification without revealing DOB
✅ On-chain proof anchoring for audit trails
✅ Reusable verification hook for any React app
What You Learned#
This tutorial taught you:
How zero-knowledge proofs work in practice
How to integrate blockchain identity verification
How to structure a Next.js app with proper SSR/CSR separation
How WalletConnect enables mobile wallet pairing
How to request different types of identity proofs
Warning
⚠️ Before going to production, consider adding security features and an audit