NightShift: Private Payroll with Public Proof
About the project
NightShift is a privacy-preserving payroll system built on the Midnight Network. It lets an employer create a job offer whose salary is sealed on-chain, lets a worker accept that offer by proving they received the correct private terms, and gives auditors a public employment history without exposing the worker's salary or payment amount.
The central idea is simple:
A worker should be able to prove that they were paid correctly without publishing what they earn.
NightShift records the existence and outcome of a payroll relationship—not the sensitive financial details inside it. A public observer can see a pseudonymous worker, approved hours, pay periods, confirmation status, and contribution-verification status. They cannot read the salary, the payment amount, or the value derived from either.
The project is a multi-application monorepo containing:
- an employer application for hiring, approving hours, and paying workers;
- a worker application for accepting offers, confirming payments, and proving contributions;
- an auditor application that reads public employment history without a wallet or login;
- a NestJS backend for directory, timesheet, notification, and reporting workflows;
- a shared
PayrollApicontract that allowed the user interfaces to be built before the blockchain integration was complete; and - a Compact smart contract with six circuits:
hire,acceptHire,approveHours,confirmPayment,proveContribution, andendEmployment.
What inspired us
We started from a real-world weakness in payroll: a salary agreement is usually private, but the evidence that it was honored is controlled by the employer. A worker may have a payslip, a spreadsheet, or an HR portal entry, yet those records are easy to alter, difficult to carry between jobs, and often unavailable when a dispute matters most.
The consequences are practical. A worker may need to demonstrate stable employment to a lender or future employer. A regulator or social-security fund may need to verify that contributions were calculated correctly. A journalist or auditor may need to establish that a recurring employment relationship existed. None of those parties should need to learn the worker's exact salary.
We took inspiration from several familiar systems:
- sealed employment offers, where the agreed terms should not be mutable after acceptance;
- payslips and contribution records, where the fact of payment matters but the record should be independently verifiable;
- real-world sealed tenders, where commitments prevent a party from changing its number after seeing someone else's;
- zero-knowledge qualification systems, where a verifier learns whether a condition holds but not the private facts behind it; and
- public audit trails, where anyone can inspect the outcome without receiving permission from the party being audited.
The privacy design also benefited from studying what privacy blockchains do not hide. We initially had to distinguish carefully between private content and public participation. Calling a contract circuit, the contract address, and transaction timing remain observable. The product therefore makes a narrower and stronger promise: financial content is sealed, while the existence and timing of payroll activity are public.
The product model
When an employer hires a worker, the salary is combined with a fresh random salt and committed on-chain:
$$ C = \operatorname{persistentCommit}(\text{rate}, \text{salt}) $$
The commitment is public, but the rate and salt remain private. Because the salt makes guessing impractical and the commitment is binding, the employer cannot later claim that a different salary was agreed.
For an hourly worker, the expected payment is checked inside a zero-knowledge circuit:
$$ \text{amountReceived} = \text{approvedHours} \times \text{sealedRate} $$
For a contribution declaration, the circuit checks the corresponding relationship without exposing the rate:
$$ \text{declared} = \frac{\text{sealedRate} \times \text{contributionRate}}{100} $$
Only the result is recorded. A correct payment becomes confirmed; a missed or incorrect payment remains unconfirmed. That failure state is intentional: the system cannot turn an underpayment into a correct payment merely because an employer controls the database.
The public ledger deliberately contains no field for salary or payment amount. This absence is part of the data model, not just a UI decision. The shared types define Amount as bigint to prevent floating-point rounding errors, while PeriodRecord contains status, approved hours, and contribution verification—but nowhere to place a private amount.
How we built it
1. We began with the problem and a shared contract
The first commits established the NightShift idea, repository structure, engineering rules, and a task board. Before attempting a full blockchain application, we defined the PayrollApi interface and implemented MockPayrollApi. This let the employer, worker, and auditor experiences evolve against a stable contract while the smart-contract integration was still being researched.
That separation became one of the most important architectural choices. The applications did not need to know whether they were talking to an in-memory implementation or the real Midnight implementation. When the real API arrived, the applications could be re-pointed instead of rewritten.
2. We split the work into independent tracks
The team worked in parallel: contract circuits and arithmetic, wallet/payment integration, worker UI, employer UI/backend, and auditor/testing. The repository's task documents made the integration gate explicit:
$$ \text{hire} \rightarrow \text{accept} \rightarrow \text{approve} \rightarrow \text{pay} \rightarrow \text{confirm} \rightarrow \text{audit} $$
This was more than project management. It forced us to define what “done” meant across the whole system rather than celebrating an isolated compiling component.
3. We built and verified the Compact contract incrementally
The contract work progressed circuit by circuit. hire, acceptHire, approveHours, and confirmPayment were compiled and documented first; proveContribution and endEmployment completed the lifecycle. Witnesses were implemented and proven with local smoke runs, and the final build generated real prover and verifier keys for all six circuits.
The contract also adopted per-deployment identity spaces. Rather than treating a wallet address as a universal identity, the design derives a pseudonymous contract identity from a device-held secret and a domain separator. This keeps the identity meaningful inside NightShift without making it a person's name or directly reusing their wallet identity across applications.
4. We connected Midnight.js and the wallets
The integration layer added providers, deployment utilities, encoding helpers, local-secret handling, period-key derivation, headless wallet support, and MidnightPayrollApi. The employer and worker applications were wired to the DApp Connector flow, while the auditor used public/indexer reads and did not require a wallet.
The employer app can hire workers, approve hours, and initiate a payment. The worker app can inspect and accept a sealed offer, confirm a received payment, and submit a contribution proof. The auditor app reconstructs worker-period history from the public ledger and highlights confirmed versus unconfirmed periods.
5. We added the operational layer
The NestJS backend provides the conventional application workflows around the chain: worker directory management, five-state timesheet handling, notifications, and reporting. It also exposes on-chain confirmation information without making the backend the source of truth for private payroll facts.
The local Docker devnet and deployment scripts made it possible to test the full lifecycle locally. Full proving and verification assets were checked into the contract build output so that the deployed application exercised actual ZK artifacts rather than a mock proof path.
The challenges we faced—and how we overcame them
Privacy claims were easy to overstate
Our early research exposed a dangerous misconception: a value is not private just because a variable is named private, and disclose() is not an encryption operation. Values written into ordinary maps and sets are public. We corrected the design around persistentCommit, wrote down the visibility model, and adopted a rule that every privacy claim had to be checked against the actual ledger boundary.
The result was a more honest product story: amounts stay sealed, but circuit calls and timing remain visible. We also documented that pseudonymous histories are linkable within the contract because that linkability is what makes an employment history useful.
Compact arithmetic did not behave like ordinary application arithmetic
The arithmetic spike found that Compact has no ordinary division operator. That mattered for contribution proofs. We isolated the arithmetic question early, adjusted the circuit formulation, and recorded edge cases instead of letting an untested formula hide inside the main contract.
This taught us to treat small language experiments as security work. A payroll formula that is “obviously correct” in TypeScript is not automatically correct inside a ZK circuit.
Wallet identity and payment identity were not interchangeable
One integration failure came from treating a worker key as though it were a shielded payment address. The worker key is a contract identity; it is not the address required to receive a shielded transfer. We corrected the payment path to obtain and use the proper shielded address and kept the two identity concepts separate.
We also learned that the DApp Connector exposes wallet capabilities, not seed material. The integration therefore stopped trying to derive secrets from connector internals and moved secret custody into the appropriate local private-state path.
DUST came from the wrong key
Deployment initially failed because DUST was derived using the wrong key family. The failure looked like a deployment problem, but the underlying issue was a mismatch between wallet identity, Zswap keys, and DUST keys. By testing against the real local devnet and following the installed Midnight.js types, we corrected the key selection and got deployment working.
Windows and Docker added environmental traps
The devnet failures were eventually traced to one Docker issue appearing in two forms. Running the node with the wrong user prevented the expected files and permissions from behaving correctly. The fix was to run the node as UID 0 in the local compose setup and document the two Windows-specific traps so that the next developer would not repeat the same investigation.
Two WebAssembly copies broke the real API
Once the UI was connected to the real contract, the “hire” path still failed. The cause was not the circuit itself: two copies of the WebAssembly runtime had entered the dependency tree. We added dependency-tree checks and a deduplication script, then repaired the dependency graph so every part of the application used the same runtime instance.
The ledger is hashed, so history recovery needed a design
A worker's history could not simply be read as a friendly list from a private database. We implemented period-key derivation and recovery logic that reconstructs the public history from hashed ledger state. This preserved the auditor's no-wallet property while still allowing the worker application to present a useful timeline.
Proving keys had to match the current contract
After contract changes, stale full-ZK proving keys became a real risk. The team regenerated proving and verifier keys from the current contract and completed a compact:full build. The lesson was operational but fundamental: cryptographic artifacts are part of the versioned product, not disposable build clutter.
Parallel branches created integration pressure
The project contains several merges because the work was intentionally parallel. We used status documents, circuit maps, shared interfaces, and explicit merge gates to make the branches converge. A conflict-resolution commit and a final dashboard merge were not merely bookkeeping; they were the points where isolated assumptions had to agree in one runnable system.
What we learned
The biggest lesson was that privacy is a data-flow property. It is not enough to use a privacy-oriented blockchain or generate a proof. We had to ask, for every value:
- Where is it created?
- Which machine knows it?
- Does it cross a server, wallet, ledger, or public return boundary?
- What exactly does an observer learn from the resulting transaction?
We also learned that a good privacy product needs a good failure product. NightShift does not only show successful payroll. It makes an underpayment visibly fail the confirmation proof and keeps that period unconfirmed. The public failure signal is useful precisely because the private cause—the salary and payment amount—does not become public.
Finally, the process reinforced the value of building against interfaces, testing risky assumptions early, and keeping limitations visible. The mock API accelerated the UI work; the local devnet exposed integration truths; the full-ZK build prevented us from confusing a demo stub with a functioning proof system.
The final story we would tell in a demo
An employer hires a pseudonymous worker and seals a rate such as 5,000 units. The chain shows only a commitment. The worker accepts by supplying the private rate and salt; if the employer sent a different number from the one committed, acceptance fails.
For the next pay period, the employer approves hours and sends the payment. The worker confirms it. The contract proves the relationship privately and records only confirmed. An auditor, with no wallet connected, can see that the period was confirmed.
Then the employer sends an underpayment. The worker tries to confirm it, and the proof is rejected. The auditor sees an unconfirmed period, but still does not see the salary or amount. That moment captures the purpose of NightShift: public accountability without public exposure.
Honest limitations and future work
NightShift does not hide everything. Contract calls, timing, approved hours, and pseudonymous participation are observable. A worker's confirmations are linkable inside the contract. The system also cannot force a worker to confirm, detect cash payments made outside the agreed workflow, or create institutional adoption by itself.
The next steps would be production-grade salt recovery, DUST sponsorship so workers can transact without pre-funding, stronger end-to-end tests across the three applications, public testnet deployment, and a more complete contribution/authority model. Those extensions should preserve the same rule that shaped the first version: disclose only what the product can justify disclosing.
How the commit history shaped the project
The repository history shows the project evolving in distinct phases:
- Foundation: the NightShift idea, repository setup, engineering rulebook, task board, mock API, and contribution-proof examples.
- Parallel product work: employer and worker applications, auditor views, shared design tokens, backend modules, wallet/payment experiments, and the initial merge of the application branches.
- Contract convergence: incremental circuit compilation, witness implementation, contract documentation, the circuit map, and the completion of all six circuits.
- Real integration:
MidnightPayrollApi, provider and deployment code, local devnet support, full proving/verifier keys, and the merge of the employer wallet wiring and NestJS backend. - Live debugging: DUST key correction, Docker identity fixes, shielded-address correction, WASM dependency deduplication, and ledger-history recovery.
- Final convergence: wiring all three apps to the real contract, turning the worker application into a real devnet worker, and merging the dashboard compile changes.
Important merge points included the wallet/payment merges, the contract circuit merges, the MidnightPayrollApi integration merge, the full compact:full proving-key merge, the employer/backend merge, the live-tested deployment fixes, and the final dashboard merge. Together they tell the real development story: NightShift became reliable through repeated integration, not by pretending that the first design worked perfectly.
NightShift is ultimately an experiment in making trust inspectable. The employer keeps salary details private, the worker keeps portable evidence, and the auditor gets a public answer to the question that matters: did the agreed payroll condition hold?
Built With
- docker
- midnight
- react
- typescript
- vite

Log in or sign up for Devpost to join the conversation.