Back to Portfolio Get in touch

Case Study

BlindRoute: proving a delivery happened without saying what was delivered

A delivery escrow on Midnight where the courier releases the payment by proving, in zero knowledge, that they know a secret — without that secret, the buyer's address, or anyone's identity ever touching the ledger. The contract is four circuits and a clock; the hard part was deciding exactly which facts the public chain is allowed to learn.

Role

Solo Developer

Timeline

Jul – Aug 2026

Network

Midnight Preview

Tests

25 cases in CI

Midnight Compact Zero-Knowledge Proofs TypeScript Vite Lace Wallet
View live demo View source

The problem

Escrow for a physical delivery needs an answer to one question: did the package arrive? On a public blockchain, the usual answer is to publish the evidence — a tracking number, a delivery hash, a courier receipt. That works, and it leaks.

Anyone reading the ledger can now see a buyer's purchasing pattern, their delivery cadence, and enough metadata to infer a great deal about them. The centralized alternative isn't better: logistics providers cache plaintext addresses, which turns every one of them into a breach waiting to happen. Both approaches treat "verify the delivery" and "reveal the delivery" as the same operation.

They aren't. That's the whole thesis of this project: verification and visibility are separable, and zero-knowledge proofs are how you separate them.

Drawing the line between public and private

The design work here wasn't writing circuits — it was deciding, field by field, what the ledger is allowed to know. Too much on-chain and the privacy claim is theatre; too little and neither party can prove the contract behaved correctly.

Public on the ledger

Escrow state (EMPTY / LOCKED / RELEASED), the payment amount, a commitment hash, derived public keys for payer and courier, a shared tick clock, and the refund deadline.

Why: either party — or an outside observer — must be able to confirm the escrow exists, holds what it claims, and resolved legitimately.

Private, never transmitted

The delivery-proof secret (standing in for a GPS ping, drop-off code, or recipient signature) and each party's identity secret key. Generated in the browser, held in memory for the session only.

Why: this is precisely the data the contract exists to keep off-chain. It reaches no server, no wallet call, and no DOM node.

What an observer sees is an escrow appear with an amount and a hash, then later flip to RELEASED alongside a valid proof. What they cannot recover at any point is the secret behind that hash, or any link between a derived public key and a real person. I enforced the same boundary in the UI as in the contract: the private secret is never assigned to an element or written to a log line — only the public commitment and transaction results are ever rendered.

Four circuits and a clock

lockEscrow — the customer commits

Publishes the amount, a hash commitment of the delivery secret, and a refund deadline expressed in ticks. The secret itself stays in the browser; only its commitment goes on-chain.

releaseEscrow — the zero-knowledge step

The courier proves they know a value that hashes to the recorded commitment, without disclosing it. This is the circuit that makes the whole thing work — and it is noticeably slower than the others, because its proving key is roughly 5MB against ~150KB for the lock circuit. Proof generation happens locally, so that cost is paid on the courier's machine, not by a server that would have to be trusted.

refundEscrow — the escape hatch

The first version had a real flaw: if the courier simply never released, the customer's funds were locked forever. Adding a refund path meant introducing a shared tick counter as an on-chain clock — once it passes the deadline set at lock time, and only for the original payer, the funds can be reclaimed.

Twenty-five test cases cover this: circuit logic, the full state machine including double-lock and double-release rejections, the refund path (too-early rejection, successful refund after the deadline, refusing anyone who isn't the payer, re-locking afterwards), and a privacy test asserting no public circuit call exposes or mutates private state. All of it runs in CI on every push, against a pinned Compact compiler version.

Working on unfinished infrastructure

Building on a young network means the ground moves. Three decisions came directly from that.

Deployed to Preview, not Preprod

Preprod's indexer and dust-sponsor infrastructure proved unstable at submission time. I confirmed the substitution with the organizers rather than shipping something that looked deployed but couldn't be demonstrated.

Let the wallet do the hard parts

I wrote a Node CLI first, and its wallet sync hit an SDK bug where progress never advanced past zero while leaking memory. Rather than fight someone else's sync implementation, the browser app delegates sync, balancing, signing and proving to Lace entirely. The CLI stays in the repo, documented as the alternate path, with its limitation stated plainly.

Treat error messages as a feature

Most failures here are environmental, not logical — a popup blocker eating the signing window, a competing wallet extension, dust not yet accumulated. Eleven of the tests cover error handling alone: distinguishing a user dismissing a prompt from a genuine failure, and unwrapping nested error shapes into something a human can act on. On infrastructure this raw, a clear error is worth more than another feature.

What I'd do differently

Model the failure path before the happy path

I built lock-and-release first and treated "courier never shows up" as an edge case to handle later. It isn't an edge case — it's the scenario escrow exists for. Retrofitting the refund meant adding a clock to a contract that hadn't been designed to need one. The timeout should have been in the state machine from the first sketch.

Check proving cost per circuit early

The 30× difference in proving key size between release and lock is a UX fact, not a footnote — it's the difference between an instant action and one that needs a progress indicator and an explanation. I discovered it by feel, late. Measuring each circuit's proving cost as it's written would have shaped the interface sooner.

Write the privacy table on day one

The public/private field table became the most useful artifact in the repo — it's what makes the privacy claim auditable instead of aspirational. I wrote it while documenting, but it would have been a better design tool than a summary, because every "should this be on-chain?" question was already answered by it.

Want something like this built?

I build full-stack and on-chain products end to end — contracts, frontend, deployment, and the careful thinking about what a system should and shouldn't reveal.