Inspiration

I work on NAWA, an AI platform that handles intake and screening for innovation programs. Screening always starts the same way: applicants send over personal stuff like their date of birth and past funding so we can check if they qualify. Then we're stuck storing and protecting all of it, even when the actual decision is just a yes or no. That never sat right with me, and it clashes with how NAWA is meant to work: the AI never rejects anyone, a human makes the call.

When I looked at Midnight, the zero-knowledge angle clicked. What if someone could prove they meet the rules without handing over the numbers at all?

What it does

Someone can prove they meet a program's requirements (old enough, under a funding cap) without showing their actual age or funding. The rules sit in public on-chain state, so anyone can check what applicants are being held to. The applicant's real numbers stay on their own device and never go on-chain.

The setup is simple. The program puts two public numbers on the ledger: a minimum age and a funding cap. The applicant keeps three things private that never leave their device: their real age, their prior funding, and a secret. The circuit then proves three things are true at once:

  • their age is at least the minimum,
  • their funding is at or under the cap, and
  • they haven't already used their one proof.

That last part uses a nullifier, which is just a one-way hash of the applicant's secret. Only the nullifier goes on-chain, and it gives away nothing about the secret. Once it's recorded, the same person can't prove eligibility again. And the whole time, nobody verifying ever sees the age, the funding, or the secret. A valid proof just says "I know values that pass the rules," and nothing else.

What comes out is a proof reference: the contract address plus the proof's transaction id. NAWA drops that into its audit log, so there's a verifiable record that the person cleared the bar, without NAWA ever holding the data behind it.

How I built it

The circuit lives in eligibility.compact, written in Compact. The state splits into a public part (minAge, maxPriorFunding, and a set of spent nullifiers) and private witnesses (age, funding, a secret). The proveEligibility circuit checks the two limits, builds a domain-separated nullifier with persistentHash, and blocks replays.

A Midnight.js driver deploys the contract with the public rules, generates the proof against the private inputs, and tells you eligible or not.

For the integration, the proof reference goes through NAWA's existing intake API (FastAPI and Postgres) into the audit log, and shows up in the Next.js intake console as a small panel a reviewer uses on an application. That's the whole "integrate Midnight" idea: an app that already exists picks up a real privacy guarantee.

Challenges I ran into

Compact treats everything as private by default, even constructor arguments, until you explicitly disclose it. Figuring out where the line should sit (rules public, applicant data private) took a few passes.

Versions were fiddly. The Compact compiler, compact-runtime, and the native onchain-runtime all have to match, and I ended up with two copies of the native runtime resolving at once, which threw StateValue errors on every call until I deduped them.

The public testnet fought me. Deploying to preview kept failing because the preview indexer drops its response partway through the deploy (their v4 indexer isn't fully ready on preview yet). The wallet funds fine and DUST generates fine, it's just the read-back that's flaky. So I verified everything on a local devnet instead, and left a one-command retry for when preview settles down.

What I learned

I learned how to turn a real access-control decision into a ZK circuit, and where to draw the public/private line so it stays both auditable and private. I also got a feel for how a Midnight app actually fits together: DUST as the fee token you generate from registered NIGHT, and how the wallet, indexer, and proof server play together around a deploy.

What's next

Get the on-chain deploy working on a public testnet once the indexer is stable (it's already wired up), and add more criteria beyond age and funding, like residency or program-specific gates, while keeping every applicant input private. Longer term, I want NAWA to make more of its intake calls on proofs instead of raw personal data.

Built With

Share this project:

Updates

Submission history