Inspiration
Almost every signup form asks for more than it needs. A rental application that only needs to know "are you 18?" ends up storing your exact date of birth, forever, in a database you don't control. That gap between what a check actually requires and what gets collected is exactly what Midnight's zero-knowledge tooling is built to close. I picked the "Integrate Midnight" track because it forces an honest comparison: not a hypothetical privacy app, but a real before-and-after on a form pattern everyone has filled out.
What it does
ProofPass upgrades a rental application's age check. The "before" panel is a standard form: enter your date of birth, it gets stored, in full, on the server. The "after" panel does the same job with a Midnight zero-knowledge proof instead: you enter your date of birth locally, a circuit proves your age falls within the landlord's required range (e.g. 18+, or an 18–65 bracket for something like an event ticket), and only a pass/fail boolean plus a pseudonymous applicant ID ever reach the chain. Your birth date is read once, used to build the proof, and discarded and it's never stored, logged, or disclosed. The check is exact to the day: someone who turns 18 today passes, someone who turns 18 tomorrow does not.
How I built it
Contract (Compact):
proofpass.compactdeclares two private witnesses (localBirthDate,localSecretKey) and one circuit,proveEligibility. Dates are encoded as plainYYYYMMDDintegers, which makes exact-to-the-day age arithmetic possible with plain integer subtraction. No floating point, no date library, no leap-year edge cases beyond one documented, accepted approximation (Feb 29 vs. a non-leap cutoff year).Witnesses & private state: birth date and a locally-generated secret key never leave the process; only the circuit's
disclose()d outputs (a boolean and apersistentHash-derived pseudonymous ID) touch the ledger.CLI: an interactive demo playing both roles: applicant (prove) and verifier (read back the result).
Web demo: a small Express server keeps one contract connection alive and exposes
/api/prove+/api/statusbehind a static before/after page, plus a fourth panel walking through what a real company would actually have to do to adopt this (deploy their own contract instance, swap the frontend step, swap the backend check), because the honest answer is "real work," not "flip a switch."Local devnet: node + indexer + proof server via Docker Compose, so the whole thing runs and proves end-to-end without touching a public testnet.
Challenges I ran into
The hardest bug was not in my code; it was in the scaffold. create-mn-app's pinned dependency (@midnight-ntwrk/[email protected]) has a loose semver range on a WASM binding (onchain-runtime-v3@^3.0.0), while a sibling package pins it exactly at 3.0.0. Once 3.1.0 was published, npm install silently resolved two separate copies of that binding into the tree. Every stateful circuit call then failed deep inside the SDK with expected instance of StateValue, because an object built by one WASM module's class fails an instanceof-style check in the other module's copy of the same class. I isolated it by reproducing the exact same failure on a completely untouched hello-world template, which ruled out my contract and pointed straight at the dependency tree. Fixed with an overrides pin plus a full clean reinstall (an incremental install patches versions in place without actually deduplicating the files on disk). Getting exact date-of-birth precision without pulling in a date library was its own small puzzle. The YYYYMMDD-as-integer trick turned out to be the cleanest fit for Compact's plain arithmetic.
Accomplishments that I am proud of
A real, working zero-knowledge proof pipeline, verified on-chain with automated tests covering both the happy path and the exact day-level boundary (turn 18 today: pass; turn 18 tomorrow: fail).
Found and fixed a dependency bug that would block any team using this scaffold's default setup and documented it clearly enough that it's a fix, not just a workaround.
A demo that's honest about scope: the page does not just show a working proof, it tells what integrating this into a real product actually requires.
What I learned
How Compact's "private by default, explicit disclose()" model actually forces good privacy design, one cannot accidentally leak a witness value, the compiler stops you. Also a lot about how young, fast-moving toolchains fail: version-string checks can pass while the underlying binaries are still incompatible, and the fix is dependency hygiene, not more code.
What's next for ProofPass
Real wallet integration (Lace) so proving happens in the applicant's own browser instead of a shared demo server; a second circuit (income or credential threshold) to make the "this pattern generalizes" claim concrete instead of asserted; and moving past the current single-session contract to a per-applicant record so a verifier can look up more than the most recent proof.
Built With
- compact
- css
- docker
- docker-compose
- express.js
- git
- github
- html
- javascript
- leveldb
- midnight
- node.js
- typescript
- websocket
- zero-knowledge-proofs
- zk-snarks
Log in or sign up for Devpost to join the conversation.