Introduction**
Identity is everywhere in Web3. Whether you're onboarding users to a DeFi protocol, verifying age for a content platform, or running KYC for a regulated service, you need to answer one fundamental question: who is this person, and can I trust what they claim about themselves?
The problem is that most solutions today force you into one of two bad options.
**
Option A: Centralized identity.** You hand everything to a third party a government database, an OAuth provider, a KYC vendor. It works, but now you've introduced a single point of failure, a honeypot for attackers, and a privacy nightmare for your users. Data breaches are not a matter of if, but when.
***_Option B: _On-chain identity.* You put credentials on a public blockchain. Transparent, decentralized and completely exposed. Anyone can see that your wallet is over 18, holds a certain accreditation, or passed KYC. That's not privacy. That's surveillance with extra steps.
Neither option is good enough for real-world applications that need both compliance and user trust.
This is exactly the gap Midnight is designed to fill. Midnight is a data protection blockchain that lets users prove things about themselves without revealing the underlying data. You can prove you're over 18 without showing your birthdate. You can prove you're an accredited investor without disclosing your net worth. You get compliance and privacy at the same time.
In this tutorial, we'll build a complete privacy-preserving identity system using Decentralized Identifiers (DIDs) and Verifiable Credentials (VCs) on Midnight. By the end, you'll have:
- A working Compact smart contract for credential issuance and verification
- TypeScript utilities for DID creation and management
- Three complete use cases: age verification, accredited investor verification, and KYC/AML
- A clear understanding of how zero-knowledge proofs enable selective disclosure Let's get into it.
**
What Are DIDs and Verifiable Credentials?
**
Before writing any code, let's get the foundational concepts clear.
Decentralized Identifiers (DIDs)
A DID is a globally unique identifier that you control no central authority issues it or can revoke it without your consent. It looks something like this:
Breaking it down:
- did - the URI scheme
- midnight-the DID method (specifies how to resolve and interact with this DID)
- The unique identifier string - cryptographically derived from your key pair
Every DID has a DID Document associated with it a JSON-LD document that describes the entity: their public keys, authentication methods, and service endpoints. Think of it as a decentralized business card that only the owner can update.
**
Verifiable Credentials (VCs)
**
A Verifiable Credential is a tamper-proof digital statement made by one party (the issuer) about another party (the holder). A VC might say:
"peter is over 18" : signed by Verified KYC Inc.
It follows the W3C Verifiable Credentials Data Model and contains three key parts:
Credential metadata: issuer DID, issuance date, expiry date
Credential subject: the actual claims about the holder
Proof: a cryptographic signature that proves the credential hasn't been tampered with
{
"@context": [
"https://www.w3.org/2018/credentials/v1"
],
"type": ["VerifiableCredential", "AgeCredential"],
"issuer": "did:midnight:issuer123",
"issuanceDate": "2024-01-15T00:00:00Z",
"credentialSubject": {
"id": "did:midnight:holder456",
"ageOver": 18
},
"proof": {
"type": "Ed25519Signature2020",
"verificationMethod": "did:midnight:issuer123#keys-1",
"proofValue": "z58DAdFfa9..."
}
}
**
The Three-Party Trust Model
**
The system always involves three roles:
- Issuer — A trusted entity that creates and signs credentials (e.g., a KYC provider, a government body, a university)
- Holder — The user who receives, stores, and controls their credentials (e.g., Alice)
- Verifier — A service that needs to confirm a claim (e.g., a DeFi protocol, a content platform)
The critical insight is that the Holder controls what they share. With selective disclosure on Midnight, Alice can present a zero-knowledge proof to the DeFi app that proves she satisfies the "over 18" condition without handing over her full credential, her actual birthdate, or any other personal data.
**
Why Midnight for Identity?
**
You could implement DIDs on Ethereum, Solana, or other chains. So why Midnight?
**
Selective Disclosure by Default
**
On most public blockchains, if you put identity data on-chain, it's visible to everyone. Even with off-chain storage, the verification transaction itself leaks metadata. Midnight's architecture, built around zero-knowledge proofs using the Compact language, lets you prove specific attributes without revealing anything else. This isn't a bolt-on feature it's fundamental to how the chain operates.
**
Solving Privacy vs. Compliance Together
**
Regulated industries need auditability. A financial regulator might need to verify that KYC was performed. Midnight supports viewing keys that can grant an auditor or regulator selective read access to specific data without making that data public to everyone. You get a compliance trail and user privacy simultaneously.
**
Compact : Built for Privacy Logic
**
Compact is Midnight's smart contract language, designed specifically to express privacy-preserving logic. You define the verification rules; the zero-knowledge proof machinery handles the cryptographic heavy lifting. You don't need a PhD in cryptography to build private applications.
**
Technical Setup
**
In this tutorial, you’ll build a complete DID system on Midnight.
Prerequisites
Before you begin, ensure your environment is ready:
- Midnight Compact Compiler (v0.30.0+)
- Midnight SDK (v4.0.2+)
- Docker (for running the proof server)
- Node.js (v22+)
**
- Project Initialization ** Midnight apps combine:
Compact smart contracts→ on-chain privacy logic
TypeScript/JavaScript → off-chain orchestration
Start by scaffolding your project:
bash
npx @midnight-ntwrk/create-mn-app ./my-did-tutorial
cd my-did-tutorial
json
Ensure your package.json includes:
"dependencies": {
"@midnight-ntwrk/compact-js": "2.5.0",
"@midnight-ntwrk/compact-runtime": "0.15.0",
"@midnight-ntwrk/midnight-js-http-client-proof-provider": "4.0.2"
}
**
Creating and Managing DIDs on Midnight
**
Before we issue any credentials, we need identities one for the issuer and one for the holder. A DID on Midnight is derived from an Ed25519 key pair.
**
Generating a DID**
import { createHash, randomBytes } from 'crypto';
const DID_METHOD = 'midnight';
export interface DIDKeyPair {
did: string;
publicKey: string;
privateKey: string;
}
/**
* Simulates the Compact derive_pk logic.
* In a real DApp, this would use persistentHash from the Midnight SDK.
*/
function deriveCompactPk(privateKeyHex: string): string {
const prefix = Buffer.alloc(32);
prefix.write("midnight:pk:", 0, "utf8"); // 12 bytes + 20 zeros
const sk = Buffer.from(privateKeyHex, 'hex');
// Simulation of persistentHash using sha256 for this demo
return createHash('sha256')
.update(Buffer.concat([prefix, sk]))
.digest('hex');
}
export function generateDID(): DIDKeyPair {
const privateKeyBytes = randomBytes(32);
const privateKey = privateKeyBytes.toString('hex');
// Use the Compact-compatible derivation
const publicKey = deriveCompactPk(privateKey);
const didIdentifier = `z${publicKey.slice(0, 44)}`;
const did = `did:${DID_METHOD}:${didIdentifier}`;
return { did, publicKey, privateKey };
}
export interface DIDDocument {
'@context': string[];
id: string;
verificationMethod: Array<{
id: string;
type: string;
controller: string;
publicKeyHex: string;
}>;
authentication: string[];
assertionMethod: string[];
}
export function createDIDDocument(keyPair: DIDKeyPair): DIDDocument {
const keyId = `${keyPair.did}#ke
ys-1`;
return {
'@context': [
'https://www.w3.org/ns/did/v1',
'https://w3id.org/security/suites/ed25519-2020/v1'
],
id: keyPair.did,
verificationMethod: [{
id: keyId,
type: 'Ed25519VerificationKey2020',
controller: keyPair.did,
publicKeyHex: keyPair.publicKey
}],
authentication: [keyId],
assertionMethod: [keyId]
};
}
What's private here: The private key never leaves the holder's device. The DID and public key are designed to be shared they're how others verify signatures. The DID Document itself contains no personal data.
**
Registering a DID On-Chain
**
Once created, you register a commitment to the DID Document on Midnight's ledger. Crucially, you don't put the document itself on-chain only a hash of it.
//1. Register DID
case '1': {
console.log('\n Generating DID keypair...');
const keyPair = generateDID();
const didDoc = createDIDDocument(keyPair);
// Encode arguments for the registerDID circuit
const didId = toBytes32Hash(keyPair.did); // did_id: sha256(did string)
const docCommitment = toBytes32Hash( // document_commitment: sha256(did document JSON)
JSON.stringify(didDoc)
);
const controllerPk = hexToBytes32(keyPair.publicKey); // controller_pk: derived 32-byte pubkey
console.log(`\n DID : ${keyPair.did}`);
console.log(` Public Key : ${keyPair.publicKey}`);
console.log('\n Submitting registerDID transaction (this may take 30–60 seconds)...');
try {
const tx = await deployed.callTx.registerDID(didId, docCommitment, controllerPk);
session[keyPair.did] = keyPair;
saveSession(session);
console.log(`\n DID registered on-chain!`);
console.log(` DID : ${keyPair.did}`);
console.log(` Transaction ID : ${tx.public.txId}`);
console.log(` Block height : ${tx.public.blockHeight}`);
console.log(`\n Private key saved to .did-session.json (chmod 600). Back it up!\n`);
} catch (error: any) {
const msg = error?.message ?? String(error);
if (msg.includes('DID already exists')) {
console.error('\n This DID is already registered on-chain.\n');
} else {
console.error('\n Failed:', msg, '\n');
}
}
break;
}
**
Updating a DID Document
**
When a holder needs to rotate keys or update service endpoints, they submit a new document commitment authenticated by their existing private key.
// Update DID Document
case '3': {
const didInput = await rl.question('\n Enter the DID to update: ');
const didTrimmed = didInput.trim();
// Lookup keypair from session
const kp = session[didTrimmed];
if (!kp) {
console.log('\n No keypair found for this DID in this session.');
console.log(' You must have registered the DID in this session to update it.\n');
break;
}
const newDocContent = await rl.question(' Enter a note to embed in the updated document (or press Enter to skip): ');
const updatedDoc = {
...createDIDDocument(kp),
updated: new Date().toISOString(),
...(newDocContent.trim() ? { note: newDocContent.trim() } : {}),
};
const didId = toBytes32Hash(didTrimmed);
const newCommitment = toBytes32Hash(JSON.stringify(updatedDoc));
console.log('\n Submitting updateDocument transaction...');
try {
const tx = await deployed.callTx.updateDocument(didId, newCommitment);
console.log(`\n DID Document updated on-chain!`);
console.log(` Transaction ID : ${tx.public.txId}`);
console.log(` Block height : ${tx.public.blockHeight}\n`);
} catch (error: any) {
const msg = error?.message ?? String(error);
if (msg.includes('Not authorized')) {
console.error('\n Authorization failed -- controller key mismatch.\n');
} else {
console.error('\n Failed:', msg, '\n');
}
}
break;
}
**
Smart Contract: DID Registry (Compact)
**
This contract stores DID document commitments and public keys on-chain:
compact
pragma language_version >= 0.22 && <= 0.23;
import CompactStandardLibrary;
export ledger did_registry: Map, DIDEntry>;
export { registerDID, updateDocument }
circuit registerDID(
did_id: Bytes<32>,
document_commitment: Bytes<32>,
controller_pk: Bytes<32>
): [] {
const d_id = disclose(did_id);
const d_doc = disclose(document_commitment);
const d_pk = disclose(controller_pk);
assert(!did_registry.member(d_id), "DID already exists");
did_registry.insert(d_id, DIDEntry {
document_commitment: d_doc,
controller_pk: d_pk
});
}
plaintext
Important Note
To generate Zero-Knowledge artifacts (ZKIR + keys):
- Circuits must be exported.
- They must interact with ledger structures (Map, Cell, Counter)
**
Issuing Verifiable Credentials
**
Now that identities exist, the issuer can create credentials for holders. The issuance flow has two parts: off-chain credential creation and on-chain commitment registration.
Step 1: Create the Credential (Off-Chain)
The issuer creates and signs the credential document. The full credential including the holder's birthdate or financial status stays off-chain, held by the user.
import { DIDKeyPair } from './did.js';
import { createHash, randomBytes } from 'crypto';
export interface Credential {
id: string;
type: string[];
issuer: string;
holder: string;
issuanceDate: string;
claims: any;
commitment: string;
salt: string;
}
/**
* Issues an Age Credential for a holder.
* Note: In a production ZK system, the commitment would be generated using a
* SNARK-friendly hash like Poseidon or persistentHash.
*/
export function issueAgeCredential(
issuer: DIDKeyPair,
holderDid: string,
dob: string,
threshold: number
): Credential {
const salt = randomBytes(32).toString('hex');
// Create a commitment of the private data (DOB + salt)
const dobNum = parseInt(dob.replace(/-/g, '')); // YYYYMMDD
const dobBuffer = Buffer.alloc(32);
dobBuffer.writeUInt32BE(dobNum, 28); // Standard Field padding
const saltBuffer = Buffer.from(salt, 'hex');
const commitment = createHash('sha256')
.update(Buffer.concat([dobBuffer, saltBuffer]))
.digest('hex');
return {
id: `vc:age:${randomBytes(8).toString('hex')}`,
type: ['VerifiableCredential', 'AgeCredential'],
issuer: issuer.did,
holder: holderDid,
issuanceDate: new Date().toISOString(),
claims: {
dateOfBirth: dob,
ageThreshold: threshold
},
commitment,
salt
};
}
/**
* Issues an Accredited Investor Credential.
*/
export function issueInvestorCredential(
issuer: DIDKeyPair,
holderDid: string,
date: string
): Credential {
const salt = randomBytes(32).toString('hex');
// Commitment that the user is "accredited"
const status = 1; // 1 for ACTIVE/Accredited
const statusBuffer = Buffer.alloc(32);
statusBuffer.writeUInt8(status, 31);
const saltBuffer = Buffer.from(salt, 'hex');
const commitment = createHash('sha256')
.update(Buffer.concat([statusBuffer, saltBuffer]))
.digest('hex');
return {
id: `vc:investor:${randomBytes(8).toString('hex')}`,
type: ['VerifiableCredential', 'InvestorCredential'],
issuer: issuer.did,
holder: holderDid,
issuanceDate: new Date().toISOString(),
claims: {
status: 'accredited',
accreditationDate: date
},
commitment,
salt
};
}
plaintext
**
Step 2: Register the Commitment (On-Chain)
**
The issuer registers a cryptographic commitment to the credential on-chain a hash that allows future verification without revealing the credential's contents.
Compact
pragma language_version >= 0.22 && <= 0.23;
export { issueCredential, revokeCredential }
import CompactStandardLibrary;
include 'types';
export ledger credential_ledger: Map<Bytes<32>, IssuanceEntry>;
witness issuer_secret_key(): Bytes<32>;
circuit derive_issuer_pk(sk: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([ pad(32, "midnight:pk:"), sk ]);
}
circuit issueCredential(
holder_did_hash: Bytes<32>,
schema_id: Bytes<32>,
credential_commitment: Bytes<32>,
issuer_pk: Bytes<32>
): [] {
const d_holder = disclose(holder_did_hash);
const d_schema = disclose(schema_id);
const d_commitment = disclose(credential_commitment);
const d_issuer = disclose(issuer_pk);
const issuance_id = persistentHash<Vector<4, Bytes<32>>>([
d_holder, d_schema, d_commitment, d_issuer
]);
assert(!credential_ledger.member(issuance_id), "Credential already issued");
credential_ledger.insert(issuance_id, IssuanceEntry {
holder_did_hash: d_holder,
schema_id: d_schema,
credential_commitment: d_commitment,
issuer_pk: d_issuer,
status: CredentialStatus.ACTIVE
});
}
circuit revokeCredential(
issuance_id: Bytes<32>
): [] {
const d_id = disclose(issuance_id);
const entry = credential_ledger.lookup(d_id);
const derived_pk = derive_issuer_pk(issuer_secret_key());
assert(entry.issuer_pk == derived_pk, "Only the issuer can revoke this credential");
credential_ledger.insert(d_id, IssuanceEntry {
holder_did_hash: entry.holder_did_hash,
schema_id: entry.schema_id,
credential_commitment: entry.credential_commitment,
issuer_pk: entry.issuer_pk,
status: CredentialStatus.REVOKED
});
}
plaintext
Privacy guarantee at this step: The on-chain commitment reveals nothing about the credential contents. Without the salt (held only by the user), an observer cannot reverse-engineer the original data from the commitment hash.
**
Selective Disclosure: Proving Attributes Without Revealing Data
**
Midnight DID Showcase: Privacy-Preserving Identity Verification
This report demonstrates the three primary Use Cases implemented in the tutorial project. Each case showcases how Midnight's Zero-Knowledge technology enables compliance without compromising user privacy.
Use Case 1: Age Verification (Prove 18+ Without Revealing Birthdate)
Alice wants to access a service requiring her to be 18+. She provides a ZK proof that her birthdate is early enough, without ever disclosing the actual date.
**
The Compact Circuit**
Pure circuit logic in verifier.compact that compares numerical dates:
compact
export pure circuit verifyAge(
secret_dob: Uint<64>,
secret_salt: Bytes<32>,
threshold_dob: Uint<64>,
expected_commitment: Bytes<32>
): [] {
const d_threshold = disclose(threshold_dob);
const d_expected = disclose(expected_commitment);
const computed_commitment = computeAgeCommitment(secret_dob, secret_salt);
assert(computed_commitment == d_expected, "Age commitment mismatch");
// Numerical check: older persons have LOWER YYYYMMDD values
assert(secret_dob <= d_threshold, "User does not meet the age requirement");
}
plaintext
TypeScript Usage Generating the proof in prove-age.ts:
typescript
// Define the 18+ threshold dynamically
const eighteenYearsAgo = new Date();
eighteenYearsAgo.setFullYear(eighteenYearsAgo.getFullYear() - 18);
const ThresholdDob = BigInt(eighteenYearsAgo.toISOString().split('T')[0].replace(/-/g, ''));
// Verify Age (Pure Circuit)
await verifierInstance.circuits.verifyAge(
context as any,
SecretDob, // Private witness
UserSalt, // Private witness
ThresholdDob, // Public input
PublicCommitment // Public input
);
plaintext
Use Case 2: Accredited Investor Verification (Prove Net Worth >= $1M)
Regulated DeFi pools require accreditation. Alice proves her wealth meets the threshold without revealing her actual bank balance or net worth.
Credential Issuance
Issuing the credential in credentials.ts involves hashing the secret net worth value:
typescript
export function issueInvestorCredential(
issuer: DIDKeyPair,
holderDid: string,
date: string,
netWorth: number
): Credential {
const salt = randomBytes(32).toString('hex');
const netWorthBuffer = Buffer.alloc(32);
netWorthBuffer.writeBigUInt64BE(BigInt(netWorth), 24);
const saltBuffer = Buffer.from(salt, 'hex');
const commitment = createHash('sha256')
.update(Buffer.concat([netWorthBuffer, saltBuffer]))
.digest('hex');
return {
id: `vc:investor:${randomBytes(8).toString('hex')}`,
type: ['VerifiableCredential', 'InvestorCredential'],
claims: { status: 'accredited', accreditationDate: date, netWorth },
commitment,
salt
};
}
Verification Circuit
The verifier only learns if the threshold is met:
compact
export pure circuit verifyAccredited(
secret_net_worth: Uint<64>,
secret_salt: Bytes<32>,
threshold_net_worth: Uint<64>,
expected_commitment: Bytes<32>
): [] {
const d_threshold = disclose(threshold_net_worth);
const d_expected = disclose(expected_commitment);
const computed_commitment = computeInvestorCommitment(secret_net_worth, secret_salt);
assert(computed_commitment == d_expected, "Investor commitment mismatch");
assert(secret_net_worth >= d_threshold, "User net worth is below the threshold");
}
plaintext
Use Case 3: Privacy-Preserving KYC/AML
Compliance doesn't require "data honeypots." Alice proves she is from a non-sanctioned country and that her ID is valid, while providing a unique identity hash to prevent multiple accounts.
_
KYC Credential Schema_
Defined in kyc-credential.json
:
j
son
{
"type": "object",
"properties": {
"countryCode": { "type": "string", "description": "ISO 3166-1 alpha-2" },
"expiryDate": { "type": "integer", "description": "YYYYMMDD format" },
"idHash": { "type": "string", "description": "Hash of ID document" }
},
"required": ["countryCode", "expiryDate", "idHash"]
}
plaintext
KYC Verification Circuit
The circuit in verifier.compact performs three checks simultaneously:
compact
export pure circuit verifyKYC(
country_code: Bytes<32>,
expiry_date: Uint<64>,
id_hash: Bytes<32>,
secret_salt: Bytes<32>,
threshold_expiry: Uint<64>,
expected_commitment: Bytes<32>
): [] {
const computed_commitment = computeKYCCommitment(country_code, expiry_date, id_hash, secret_salt);
assert(computed_commitment == disclose(expected_commitment), "KYC commitment mismatch");
// 1. Sanctioned country check (Example: "RU")
assert(country_code != pad(32, "RU"), "Country is sanctioned");
// 2. Expiry check
assert(expiry_date >= disclose(threshold_expiry), "ID document is expired");
// 3. Identity Uniqueness (Allows platform to check for duplicates without seeing passport number)
disclose(id_hash);
}
plaintext
Platform View
When verification succeeds in prove-kyc.ts, the platform sees only compliance outcomes:
bash
╔════════════════ PLATFORM VIEW ═══════════════╗
║ [Proof]: VALID ║
║ [Claims]: Nationality OK, Valid and Unique ║
║ [Unique ID Hash]: ab78f2d5... ║
╚══════════════════════════════════════════════╝
plaintext
Summary: Midnight's DID approach ensures that personal PII never touches the ledger. Only the proof of compliance is shared, preventing centralized data honeypots and protecting user privacy.
Testing Guide
This guide provides the steps to run and verify the selective disclosure scenarios implemented in this tutorial.
- Start the Local Proof Server The proof server handles ZK-SNARK generation locally. It must be running for any of the scripts to work.
bash
docker run -p 6300:6300 midnightntwrk/proof-server:8.0.3 midnight-proof-server -v
plaintext
- Compile the Compact Contracts Compile the verifier circuits to generate the required ZK artifacts (ZKIR, circuit logic, and SDK bindings).
bash
npm run compile:verifier
plaintext
This generates:
contracts/managed/verifier/compiler/ -- ZK circuit representations
contracts/managed/verifier/contract/ -- TypeScript SDK bindings
plaintext
- Run the Age Verification Example Alice proves she is 18+ without revealing her birthdate.
bash
npx tsx scripts/prove-age.ts
plaintext
Expected output:
text
╔══════════════════════════════════════════════╗
║ Interactive Age ZK Proof Demo ║
╚══════════════════════════════════════════════╝
Enter your secret Birthdate (YYYY-MM-DD): 1995-05-15
[Local Storage] Secret Birthdate recorded as: 1995-05-15
[DApp] Issuing an Age VC to Alice...
[Crypto] Computing matching persistentHash commitment...
[Policy] Proving Birthdate is BEFORE 2008-04-17 (18+ requirement)...
Generating cryptographic proof (this keeps your DOB hidden)...
ZK Proof Generated Successfully!
plaintext
You have proved to the verifier that you are older than the threshold
WITHOUT disclosing your actual birthdate (1995-05-15).
4. Run the Accredited Investor Example
Alice proves she meets the $1,000,000 net worth threshold without revealing her actual balance.
bash
npx tsx scripts/prove-investor.ts
plaintext
Expected output:
text
╔══════════════════════════════════════════════╗
║ Interactive Investor ZK Proof Demo ║
╚══════════════════════════════════════════════╝
Enter your secret Net Worth (USD): 1250000
[Local Storage] Secret Net Worth recorded as: $1,250,000
[DApp] Issuing an Investor VC to Alice...
[Crypto] Computing matching persistentHash commitment...
[Policy] Proving Net Worth is >= $1,000,000...
Generating cryptographic proof (this keeps your Net Worth hidden)...
ZK Proof Generated Successfully!
plaintext
You have proved to the verifier that you meet the $1,000,000 threshold WITHOUT disclosing your actual net worth ($1,250,000).
**
5. Run the KYC/AML Example
**
Alice proves she is not from a sanctioned country and her ID is valid.
bash
npx tsx scripts/prove-kyc.ts
plaintext
Expected output:
plaintext
text
╔══════════════════════════════════════════════╗
║ Interactive KYC ZK Proof Demo ║
╚══════════════════════════════════════════════╝
Enter your Country Code (e.g., US, CH): CH
Enter ID Expiry Date (YYYYMMDD): 20301231
Enter Passport Number (Secretly hashed locally): P12345678
[DApp] Issuing a KYC VC to Alice...
[Crypto] Computing matching persistentHash commitment...
[Policy] Proving Nationality NOT "RU" and Expiry >= 20240417...
Generating ZK Proof (PII stays on your device)...
ZK Proof Generated Successfully!
╔════════════════ PLATFORM VIEW ═══════════════╗
║ [Proof]: VALID ║
║ [Claims]: Nationality OK, Valid and Unique ║
║ [Unique ID Hash]: ab78f2d5... ║
╚══════════════════════════════════════════════╝
**
Reality: **The platform never saw your passport number or actual country!
Note: If any proof generation fails, ensure your Docker container is running and that your inputs satisfy the logic (e.g., birthdate makes you 18+, country code is not "RU").
**
What to Build Next
**
You now have the foundational pattern for privacy-preserving identity on Midnight. Here's where to take it further:
Extend the credential types. The same pattern works for professional licenses, healthcare credentials, educational certificates, and membership verification. Each needs a new schema and a corresponding Compact verification circuit.
Integrate with DeFi protocols. **The verifyAgeClaim, verifyInvestorClaim, and verifyKYCStatus functions can gate access to pools, trading features, or governance participation without collecting user data.
**Add viewing key support. Midnight's viewing keys let you grant auditors selective read access to credential metadata, satisfying regulatory requirements without exposing data publicly.
Adapt for other use cases:
- Healthcare access control (prove insurance coverage without revealing diagnosis history)
- Voting eligibility (prove citizenship and age without a public identity trail)
- Professional licensing (prove bar membership or medical licensure for marketplace access)
**
Summary
**
By combining Decentralized Identifiers with Verifiable Credentials and Midnight's zero-knowledge proof infrastructure, you can build identity systems that are:
- Decentralized — no central authority controls the credentials
- Private — users prove claims without revealing underlying data
- Compliant — viewing keys satisfy audit requirements
- Interoperable — following W3C standards means credentials work across ecosystems
The patterns in this tutorial credential issuance, commitment registration, selective disclosure proof generation, and on-chain verification are the building blocks for every real-world identity use case: KYC/AML for DeFi, age verification for content platforms, professional licensing for marketplaces, healthcare access control, and more.
The code is yours to take and adapt. Start with the age verification example, get it running locally, then build from there. Share your implementations and questions in the Midnight Developer Forum. https://forum.midnight.network/
Resources
Midnight Documentation -https://docs.midnight.network/
Compact Smart Contracts Reference-https://docs.midnight.network/compact
W3C DID Core Specification-https://www.w3.org/TR/did-core/
W3C Verifiable Credentials Data Model-https://www.w3.org/TR/vc-data-model/
Midnight Developer Forum-https://forum.midnight.network/

Top comments (2)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.