This is a submission for the Midnight Network "Privacy First" Challenge - Protect That Data prompt
Click the Disclosure Triangle icon / Collapsible Indicator (βΊ) below to see the details
What I Built
I built ZK Job Board, a privacy-first job marketplace where applicants prove they meet job requirements without revealing their personal attributes. Using Midnight's Compact language and MidnightJS SDK, applicants generate zero-knowledge proofs that validate skills, experience, and region without disclosing exact values. Employers get trustworthy eligibility checks, while applicants keep their data private.
Core Features:
- Anonymous Eligibility Proofs: Prove subset skills, minimum experience, and region membership without revealing specifics
- On-Chain Verification: Verifier contract integration with mock fallback for development
- Anti-Spam Protection: Per-job nullifiers prevent duplicate applications while preserving cross-job unlinkability
- Privacy-First UI: Clear indicators showing what stays private vs. what gets proven
Demo
GitHub Repository: https://github.com/depapp/zk-job-board
Page-by-Page Walkthrough
This section explains every page, its function, and how it supports privacy by design.
1. Home Page (/)

Purpose: Landing page introducing the concept of anonymous job applications
Key Actions: Navigate to "Browse Jobs" or "Post a Job"
Privacy Signals: Copy and visuals emphasize that proofs validate eligibility without exposing personal data
Midnight Integration: Educational introduction; no proof generation on this page
2. Browse Jobs (/jobs)

Purpose: List all open positions with summarized requirements
Key Actions: Select a job to view full details
Privacy Signals: Requirements are public; applicant data remains private
Midnight Integration: Displays policy summaries that become public inputs (policyHash) in the circuit
3. Job Detail (/job/:id)

Purpose: Show complete job policy including required skills, minimum experience, and allowed regions
Key Actions: Click "Apply to This Job" to start application
Privacy Signals: Requirements are transparent, but what you'll reveal is not
Midnight Integration: jobId and policyHash form part of the circuit's public inputs
4. Apply to Job (/job/:id/apply)

Purpose: Applicant prepares private attributes and generates ZK proof
Key Actions: Generate proof client-side, then submit application
Privacy Signals: UI marks private fields with π icons and explains that raw values never leave the device
Midnight Integration:
-
Real Mode (
VITE_MIDNIGHT_ENABLED=true):- Fetches zk-config for
job_eligibilitycircuit - Generates proof via MidnightJS
httpClientProofProvider - Optionally submits to on-chain verifier
- Fetches zk-config for
- Mock Mode: Deterministic proof generation for demo/testing
5. Proof Result (/proof-result)

Purpose: Confirmation that proof was generated and verified
Key Actions: View "What Was Proven" vs "What Remained Private"
Privacy Signals: Explicit list of public inputs (jobId, policyHash, nullifier) vs hidden attributes
Midnight Integration: Shows verification result (on-chain or mock) with transaction hash in real mode
6. Applicant Status (/status)


Purpose: Check application status using applicationId or nullifier reference
Key Actions: View historical submissions per job while preserving anonymity
Privacy Signals: Status checking without identity revelation; nullifier provides rate-limiting
Midnight Integration: Real deployments derive status from on-chain events; mock mode uses localStorage
7. Employer New Job (/employer/new)

Purpose: Create job policy with required skills, minimum experience, and allowed regions
Key Actions: Submit job β policyHash computed and stored
Privacy Signals: Employers never see raw applicant attributes, only validity results
Midnight Integration: Policy becomes circuit parameterization; policyHash embedded in proofs
8. Employer Applications (/employer/job/:id/applications)

Purpose: View verified submissions for a specific job
Key Actions: Inspect application validity, timestamps, and anonymized IDs
Privacy Signals: No visibility into applicant skills/years/regionβonly constraint satisfaction
Midnight Integration: Read-only view of verified results (on-chain or mock storage)
9. Review Application (/employer/application/:applicationId)

Purpose: Inspect single application's verification outcome
Key Actions: Approve, shortlist, or reject based on proof validity
Privacy Signals: No PII; only proof validity and job policy reference shown
Midnight Integration: Links proof result to job policy without deanonymizing applicant
10. Privacy Page (/privacy)

Purpose: Educational page explaining zero-knowledge proofs in this context
Key Actions: Learn what's shared vs. protected, understand nullifiers
Privacy Signals: Teaches users to trust the process and clarifies tradeoffs
Midnight Integration: Conceptual explanation with links to technical documentation
How I Used Midnight's Technology
This is How I Used Midnight's Technology:
1. Compact Circuit Design
The core privacy logic lives in circuits/job_eligibility.cmp:
circuit JobEligibility {
// Public inputs (visible to verifier)
public jobId: Field
public policyHash: Field
public nullifier: Field
// Private inputs (never revealed)
private skillsBitset: Field[32]
private experienceYears: Field
private regionIndex: Field
private secret: Field
// Constraints enforce:
// 1. Skills subset check (applicant β required)
// 2. Experience threshold (years β₯ minimum)
// 3. Region membership (region β allowed)
// 4. Nullifier derivation (prevents duplicates)
}
2. MidnightJS SDK Integration
The app/src/lib/midnight.ts file orchestrates proof generation:
// Dual-mode design for flexibility
if (VITE_MIDNIGHT_ENABLED === 'true') {
// Real Midnight Network integration
const { httpClientProofProvider } = await import('@midnight-ntwrk/midnight-js-http-client-proof-provider');
const { FetchZkConfigProvider } = await import('@midnight-ntwrk/midnight-js-fetch-zk-config-provider');
// Generate real proofs via SDK
const proof = await proofProvider.prove('job_eligibility', witness, zkConfig);
} else {
// Mock mode for development/demo
return generateMockProof(publicInputs, privateInputs);
}
3. Build & Deployment Scripts
-
Circuit Compilation (
scripts/compile-circuits.ts): Compiles Compact code to proving/verification keys inartifacts/zk/ -
Verifier Deployment (
scripts/deploy-verifier.ts): Deploys on-chain verifier and persists contract address
Data Protection as a Core Feature
Privacy isn't an afterthoughtβit's the foundation of every design decision:
What Stays Private:
- Exact Skills: Verifier never sees your full skill list, only that you have the required subset
- Precise Experience: Your exact years remain hidden; only proof of meeting minimum is revealed
- Specific Location: Your exact region stays private; only membership in allowed regions is proven
- Personal Identity: No PII is ever exposed or stored
What Gets Proven:
- β "I have all required skills" (without listing other skills)
- β "I have enough experience" (without revealing exact years)
- β "I'm in an allowed region" (without specifying which one)
- β "This is my only application to this job" (via nullifier)
UI Privacy Reinforcement:
- π Lock icons mark all private data fields
- Clear explanations at each step about what remains hidden
- Dedicated
/privacypage educating users on ZK benefits - Success page explicitly lists "What Was Protected" vs "What Was Proven"
Set Up Instructions / Tutorial
This comprehensive tutorial supports both mock mode (no credentials needed) and real Midnight integration.
Prerequisites
- Node.js 18+ and npm
- Git
- Modern web browser
Step 1: Clone and Install
git clone https://github.com/depapp/zk-job-board.git
cd zk-job-board
npm install
Step 2: Configure Environment
cp .env.example .env.local
Option A: Mock Mode (Default - No Credentials Needed)
Leave .env.local as-is or set:
VITE_MIDNIGHT_ENABLED=false
Option B: Real Midnight Integration
VITE_MIDNIGHT_ENABLED=true
VITE_MIDNIGHT_RPC_URL=https://testnet.midnight.network/rpc
VITE_MIDNIGHT_NETWORK_ID=testnet-02
VITE_MIDNIGHT_API_KEY=your-api-key
VITE_PROOF_SERVER_URL=http://localhost:6300
VITE_VERIFIER_ADDRESS= # Set after deployment
π‘ Get testnet credentials at midnight.network
Step 3: Compile Circuit
npm run compile-circuits
This compiles circuits/job_eligibility.cmp and generates artifacts in artifacts/zk/.
Step 4: Deploy Verifier (Real Mode Only)
npm run deploy-verifier
Deploys verifier contract and saves address to .env.local.
Step 5: Run the Application
npm run dev
# Visit http://localhost:5173
Step 6: Try the Complete Flow
As an Employer:
- Navigate to "Submit Job" (
/employer/new) - Create a job with requirements:
- Required Skills: Select from allowlist
- Min Experience: Set years
- Allowed Regions: Choose regions
- Submit and note the Job ID
As an Applicant:
- Browse jobs (
/jobs) - Select a job and click "Apply"
- Generate mock credentials that meet requirements
- Click "Generate Proof"
- Submit application
- View "Your Privacy Was Protected" confirmation
Verify Privacy:
- Check the Privacy page (
/privacy) to understand the system - Try applying twice to same job (nullifier prevents it)
- Apply to different jobs (nullifiers don't link identity)
Step 7: Understanding Console Logs
The app provides detailed logging for educational purposes:
// Mock mode
[Midnight] SDK loading skipped (VITE_MIDNIGHT_ENABLED is not true)
[Midnight] Using mock proof generation
// Real mode
[Midnight] Loading SDK modules...
[Midnight] Connected to testnet-02
[Midnight] Using real proof generation via SDK
[Midnight] Proof verified on-chain at 0x...
Step 8: Troubleshooting
| Issue | Solution |
|---|---|
| SDK modules failed to load | Run npm install again |
| ZK config not available | Run npm run compile-circuits
|
| Verifier address not configured | Run npm run deploy-verifier or use mock mode |
| Connection failed | Check API key and network connectivity |
| Proof generation fails | App auto-falls back to mock mode |
Step 9: Advanced Customization
Adding New Skills:
Edit config/allowlist.skills.json:
{
"skills": ["YourNewSkill", ...]
}
Modifying Circuit:
- Edit
circuits/job_eligibility.cmp - Recompile:
npm run compile-circuits - Redeploy:
npm run deploy-verifier
Switching Modes:
Toggle VITE_MIDNIGHT_ENABLED and restart dev server.
Step 10: Production Deployment
# Build for production
npm run build
# Preview production build
npm run preview
# Deploy to any static hosting (Vercel, Netlify, etc.)
Architecture Overview
βββββββββββββββ ββββββββββββββββ βββββββββββββββ
β Employer ββββββΆβ Job Policy ββββββΆβ On-Chain β
β UI β β Creation β β Storage β
βββββββββββββββ ββββββββββββββββ βββββββββββββββ
β
βΌ
βββββββββββββββ ββββββββββββββββ βββββββββββββββ
β Applicant ββββββΆβ ZK Proof Gen ββββββΆβ Verifier β
β UI β β (Midnight) β β Contract β
βββββββββββββββ ββββββββββββββββ βββββββββββββββ
Key Design Decisions:
- Dual-Mode Operation: Enables both development (mock) and production (real Midnight) workflows
- Client-Side Proof Generation: Sensitive data never leaves user's device
- Nullifier Design: Per-job nullifiers prevent spam while maintaining cross-job unlinkability
- Bitset Encoding: Efficient skill matching using bitwise operations in circuit
ZK Job Board demonstrates how Midnight Network enables practical privacy-preserving applications. By making privacy the default rather than an option, we can build systems that respect user data while maintaining functionality and trust.
Top comments (4)
Hello Depa, I am Mark from Belgium. I run the Cardaspians SPO on Cardano ticker CASP already for 6 years. We are planning on building an job recruiter / seeker platform comparable to nowjobs.be/
I was thinking to build this on midnight so I came across your POC. Are you interested in talking with us about the idea to launch a prototype on mainnet so we can attract some investors. We are a team of 5 multi-displined professionals. We have one developer already and I think you would perfectly complement him.
Hoping to have a call soon.
Best regards,
Mark
Some comments may only be visible to logged-in visitors. Sign in to view all comments.