Inspiration

The people we most want to trust — pilots before a red-eye, surgeons at 3 AM, dispatchers running a shift with a hundred lives on the frequency — are the ones we currently trust on the honor system. There's no cheap way to check, before someone takes the controls, whether they're actually fit to be there right now.

Cognitive screening exists, but the data it produces is deeply intimate — reaction time, tremor, speech prosody, memory recall. It's health data by any reasonable definition. That leaves you in a bind: the verdict (cleared / not cleared) has to be verifiable by a third party — an employer, an auditor, the person themselves — but the raw scores are nobody else's business.

Every architecture we looked at made you pick a side. A database Dusk controls isn't proof of anything. A public chain like Polygon fixes tamper-proofing but leaks the pattern — every wallet, every timestamp, every write is publicly linkable. Anyone watching the chain can see who's being screened, how often, and start drawing conclusions before they've seen a single number.

Midnight is the one architecture where "publicly verifiable" and "privately held" stop being in tension. That was the inspiration — building something that couldn't exist on any other chain.

What it does

Dusk is a cognitive readiness screening platform for high-stakes roles. An operator (a flight surgeon, a shift supervisor) runs a short battery of tests with a candidate — a spiral-drawing tremor assay via webcam, a locate-and-click reaction task, a memory/sequence cognitive test, and a short speech sample for prosody analysis. The system produces a per-domain breakdown (motor, cognitive, speech) and a composite readiness score, then a verdict: cleared, review recommended, or not fit for duty.

The verdict is anchored on Midnight via a Compact smart contract:

  • Only a SHA-256 fingerprint of the result payload and the boolean pass/fail flag are written on-chain.
  • The write itself is authorized inside a zero-knowledge circuit — the operator's secret key is a witness that the circuit uses to prove ownership, but the key itself never appears on-chain.
  • The raw scores, biometric features, audio, and video never leave Dusk's servers.

The subject leaves with a Digital Readiness Passport — a shareable, QR-scannable page an employer can independently verify: "yes, this exact result was recorded on Midnight, on this date, unaltered" — without Dusk, or Midnight, ever exposing what's actually inside it.

How we built it

Compact contract (dusk.compact) — one owner-gated write circuit (storeAssessment) and public read circuits (verifyAssessment, getAssessment, assessmentExists). Ownership is a ZK check: assert(owner == publicKey(ownerSecretKey())) where ownerSecretKey() is a witness function. The chain sees publicKey(sk), never sk.

Docker Compose runs the actual Midnight network locally — three services, one command:

  • Node (midnight-node) — consensus, block production
  • Indexer (indexer-standalone) — a queryable event stream so the wallet can sync without replaying the whole chain
  • Proof server (proof-server) — generates the ZK proofs; computationally heavy, kept local rather than round-tripped

Bridge (midnight/bridge/server.mjs) — a small Node.js service that owns the wallet keys, drives the ZK proving via @midnight-ntwrk/wallet-sdk and midnight-js-contracts, and exposes exactly four HTTP routes:

Why a bridge at all: Midnight's SDK is JavaScript-only, and Dusk's backend is Python. Rather than rewrite FastAPI in Node just to sign one kind of transaction, the bridge isolates the entire Midnight surface behind plain HTTP.

Python client (backend/blockchain/midnight_client.py) — a thin HTTP client wrapping the bridge, deliberately keeping the exact public shape (store_on_chain, verify_on_chain, get_assessment_record, hash_assessment, typed exceptions for AlreadyAnchoredError / InsufficientFundsError / PendingConfirmationError) that main.py already expected. Result: nothing else in the FastAPI app had to change to move onto Midnight.

Frontend (React / Vite) — the screening flow, the Results page with the ZK-verified badge, and the shareable Passport route (/passport/:assessmentId) that anyone with the ID can open — including a scannable QR code.

Challenges we ran into

Error 170: InvalidDustSpendProof — the whole hackathon almost died on this one. Every deploy attempt against Midnight — Preview, PreProd, local devnet — bounced back with 1010 Invalid Transaction: Custom error: 170. It was a documented, deterministic, on-chain rejection with no simple published cause. We chased it for hours: version-matched every @midnight-ntwrk/* package against Midnight's own reference examples, matched the ledger and proof-server versions, ruled out fees, ruled out the indexer being behind chain tip, ruled out our own dust-registration code, even swapped in the bboard contract as a diagnostic. All failed identically.

The actual root cause was one line missing from our bridge boot: we were only waiting for the wallet's dust balance to become non-zero before deploying, not for the full syncWallet() isStrictlyComplete() check across shielded / dust / unshielded streams. On local devnet this returned true within ~400ms (a positive balance appears fast), but the wallet's local ledger view wasn't genuinely caught up to chain tip until ~30s in — and any ZK proof generated against a not-fully-synced local state is structurally invalid, exactly the way error 170 describes it. Once we compared our boot sequence line-by-line against Midnight's own example-hello-world/hw.test.ts, the missing syncWallet(walletFacade, 2000, 60*60_000) call became obvious. Deploy succeeded 22 seconds after we added it.

Dual WASM instances of onchain-runtime-v3 — after the deploy fix, /store calls failed with expected instance of StateValue. Root cause: two physically separate copies of the onchain-runtime-v3 WASM module were being loaded (nested inside compact-runtime at v3.0.0 and midnight-js-protocol at v3.1.0). WASM classes from one module instance don't satisfy the type checks of another. Fixed by pinning onchain-runtime-v3 in overrides and manually symlinking the two nested copies to a single canonical directory.

The password validator — midnight-js-utils rejects any private-storage password that doesn't use at least 3 of {uppercase, lowercase, digit, special}. Our raw hex owner-key was only 2 classes. Fixed in one line.

Getting to a real reference — the ecosystem's most-cited example (example-bboard) turned out to be the older pattern. The actually current reference is example-hello-world, and finding that was the turning point.

What we learned

  • Public verifiability and privacy aren't opposites on Midnight — that reframe changes what kinds of civic-infrastructure products are worth building.
  • Selective disclosure via ZK circuits is a fundamentally different primitive from encryption-at-rest or off-chain storage. On Midnight, "we can prove this to you without showing it to you" is a first-class contract-level concept, not a bolt-on.
  • Compact's witness/disclose split is elegant — you write ordinary-looking imperative code and the compiler correctly separates what stays private from what's revealed on-chain. disclose() is an explicit act, so it's very hard to accidentally leak.
  • Sync semantics matter deeply for ZK. The strict-sync bug we hit taught us that on a ZK chain, "your wallet sees a positive balance" and "your wallet is provably in sync with chain tip" are genuinely different states — and only the second one is safe to prove against.
  • The right architecture for a Python backend + a JS-only SDK is a small HTTP bridge, not a rewrite. Isolates the SDK's operational complexity behind four routes, and the rest of your stack stays language-native.

What's next

  • Move the on-chain timestamp field from a counter to a proper Midnight block-time input so passports display real dates without needing a backend fallback.
  • Delegate NIGHT for DUST generation externally (1AM / Lace Carbon) so we can deploy the same contract to Midnight Preview and PreProd for public verification.
  • A per-operator ZK identity so multiple flight surgeons or shift supervisors can each sign screenings from the same organization, without any of them being able to impersonate another — a natural fit for Compact's witness pattern.
  • Cross-organization verification: an airline should be able to check that a candidate's readiness passport is genuine without contacting Dusk, and without seeing anything except the verdict.

Built With

Share this project:

Updates

Submission history