Tools: Zero-Knowledge, Zero Friction: Automating DApp Development on Midnight

Tools: Zero-Knowledge, Zero Friction: Automating DApp Development on Midnight

The Vision: Privacy at Scale

The Problem: The "Glue Code" Friction

The Privacy Contract (salary.compact)

The Ledger

The "Submit" Circuit

The "Benchmark" Circuit (Anonymity Check)

Generating the SDK

1. Compile to Metadata

2. Generate the SDK

Type Mapping Support

The Application Layer (pulse.ts)

1. Initializing Providers

2. Deploying the Contract

3. Orchestrating the Users

4. Running the Benchmark

Observability (The Pulse)

Side-by-Side Verification

Conclusion: The New Way to Build ZK

Key Takeaways:

See the Pulse in 60 seconds This article provides a step by step deep dive into Midnight Pulse, a collaborative analytics tool built to showcase the power of the Midnight SDK Generator. We'll walk through the entire lifecycle: from defining a privacy preserving smart contract in Compact to building a multi user CLI application in TypeScript. The goal of Midnight Pulse is to allow a team to compute a "salary pulse" a benchmark average without any individual ever revealing their sensitive data to anyone else (not even a central server). We solve this using Zero-Knowledge (ZK) proofs and a strict Anonymity Threshold ($N \ge 5$). The logic is simple: the application won't let you see the results until at least 5 people have joined. Developing on Midnight involves interacting with ZK-circuits and a private ledger. Manually writing the TypeScript glue code to call these circuits and read ledger state is: The Midnight SDK Generator solves this by deriving a production-ready SDK directly from your contract's metadata. Everything starts with the contract. Using Compact, we define what data is public and what logic is private. We store the aggregate metrics (Sum and Headcount) on the Shielded Ledger. This means the data is encrypted on-chain, and only the contract logic can update it. When a user submits their salary, they don't send the value in the clear. Instead, they run a circuit that privately adds their value to the aggregate. This is where the privacy guarantee is enforced. The circuit checks the employee_count before performing the comparison. Once the contract is written, we use the SDK Generator to bridge the gap between Compact and TypeScript. First, use the compact compiler to generate a structured JSON representation of your contract. Once you have your salary.structure.json, use the generator to create your type-safe SDK: The generator automatically maps Compact types to their most appropriate TypeScript equivalents, so you never have to guess: With the generated SDK, building the CLI tool is straightforward. We use an Agentic Pattern to simulate multiple participants. We first set up the Midnight network context (Wallet, Proof Server, Indexer). Our providers.ts bridge allows us to toggle between Fast Simulation and the Local Docker Network. The team leader (or a smart contract factory) deploys the instance using the generated deploy method. We loop through our simulated agents (Alice, Bob, Carol, Dave, Eve). Each agent joins the contract and submits their salary. Finally, Carol checks if she is above the average. Because all 5 agents have now submitted, the $N \ge 5$ check in the circuit passes successfully. One of the biggest challenges in ZK development is "blind debugging." To solve this, i built a StateObserver to visualize the delta of privacy. The terminal output provides a side-by-side view. As you can see below, the Public State tracks the group metrics, while the Private State remains strictly isolated in the user's local witness. This allows the developer to verify that privacy is actually being maintained—Alice can see the total sum grow, but she never sees Bob's individual contribution. Midnight Pulse proves that building ZK applications doesn't have to be hard. By using Compact for privacy logic and the SDK Generator for the application layer, we can build sophisticated, privacy preserving systems with standard TypeScript skills. Clone the repository and run the multi-agent simulation on your own machine: Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Code Block

Copy

export ledger { total_salary_sum: Uint64, employee_count: Uint64 }; export ledger { total_salary_sum: Uint64, employee_count: Uint64 }; export ledger { total_salary_sum: Uint64, employee_count: Uint64 }; export circuit submit_salary(salary: Uint64): [] { const current_sum = ledger.total_salary_sum; const current_count = ledger.employee_count; ledger.total_salary_sum = current_sum + salary; ledger.employee_count = current_count + 1; } export circuit submit_salary(salary: Uint64): [] { const current_sum = ledger.total_salary_sum; const current_count = ledger.employee_count; ledger.total_salary_sum = current_sum + salary; ledger.employee_count = current_count + 1; } export circuit submit_salary(salary: Uint64): [] { const current_sum = ledger.total_salary_sum; const current_count = ledger.employee_count; ledger.total_salary_sum = current_sum + salary; ledger.employee_count = current_count + 1; } export circuit is_above_benchmark(my_salary: Uint64): Boolean { const count = ledger.employee_count; const total = ledger.total_salary_sum; // The Privacy Gate: Revert if less than 5 people have joined check(count >= 5) "Threshold Error: N < 5 contributors"; const average = total / count; return my_salary > average; } export circuit is_above_benchmark(my_salary: Uint64): Boolean { const count = ledger.employee_count; const total = ledger.total_salary_sum; // The Privacy Gate: Revert if less than 5 people have joined check(count >= 5) "Threshold Error: N < 5 contributors"; const average = total / count; return my_salary > average; } export circuit is_above_benchmark(my_salary: Uint64): Boolean { const count = ledger.employee_count; const total = ledger.total_salary_sum; // The Privacy Gate: Revert if less than 5 people have joined check(count >= 5) "Threshold Error: N < 5 contributors"; const average = total / count; return my_salary > average; } compact compile ./contracts/salary.compact compact compile ./contracts/salary.compact compact compile ./contracts/salary.compact midnight-sdk-gen ./contracts/salary.structure.json --output ./src/sdk/SalarySDK.ts midnight-sdk-gen ./contracts/salary.structure.json --output ./src/sdk/SalarySDK.ts midnight-sdk-gen ./contracts/salary.structure.json --output ./src/sdk/SalarySDK.ts const env = process.env.MIDNIGHT_ENV || "simulated"; const providers = await getProviders(env); const env = process.env.MIDNIGHT_ENV || "simulated"; const providers = await getProviders(env); const env = process.env.MIDNIGHT_ENV || "simulated"; const providers = await getProviders(env); import { SalarySDK } from "./sdk/SalarySDK"; const sdk = await SalarySDK.deploy(providers); const contractAddress = sdk.handle.address; import { SalarySDK } from "./sdk/SalarySDK"; const sdk = await SalarySDK.deploy(providers); const contractAddress = sdk.handle.address; import { SalarySDK } from "./sdk/SalarySDK"; const sdk = await SalarySDK.deploy(providers); const contractAddress = sdk.handle.address; for (const agent of agents) { // Join the same shared contract address using the generated SDK const agentSdk = await SalarySDK.join(contractAddress, agent.providers); // Submit salary privately via the ZK circuit // The SDK handles all witness and proof management await agentSdk.submit_salary(agent.witness); // Log progress using side-by-side observability const publicState = await agentSdk.providers.publicDataProvider.queryContractState( contractAddress, ); StateObserver.displayPulse(agent, publicState); } for (const agent of agents) { // Join the same shared contract address using the generated SDK const agentSdk = await SalarySDK.join(contractAddress, agent.providers); // Submit salary privately via the ZK circuit // The SDK handles all witness and proof management await agentSdk.submit_salary(agent.witness); // Log progress using side-by-side observability const publicState = await agentSdk.providers.publicDataProvider.queryContractState( contractAddress, ); StateObserver.displayPulse(agent, publicState); } for (const agent of agents) { // Join the same shared contract address using the generated SDK const agentSdk = await SalarySDK.join(contractAddress, agent.providers); // Submit salary privately via the ZK circuit // The SDK handles all witness and proof management await agentSdk.submit_salary(agent.witness); // Log progress using side-by-side observability const publicState = await agentSdk.providers.publicDataProvider.queryContractState( contractAddress, ); StateObserver.displayPulse(agent, publicState); } const isAbove = await carolSdk.is_above_benchmark(carol.witness); StateObserver.displayResult("Carol", isAbove); const isAbove = await carolSdk.is_above_benchmark(carol.witness); StateObserver.displayResult("Carol", isAbove); const isAbove = await carolSdk.is_above_benchmark(carol.witness); StateObserver.displayResult("Carol", isAbove); --- [Alice] Status --- ┌──────────────────────────────┬──────────────────────────────┐ │ PUBLIC STATE │ PRIVATE STATE │ ├──────────────────────────────┼──────────────────────────────┤ │ Total Sum: 92,000 │ My Salary: 92,000 │ │ Headcount: 1 │ ZK-Proof: VALID │ └──────────────────────────────┴──────────────────────────────┘ --- [Alice] Status --- ┌──────────────────────────────┬──────────────────────────────┐ │ PUBLIC STATE │ PRIVATE STATE │ ├──────────────────────────────┼──────────────────────────────┤ │ Total Sum: 92,000 │ My Salary: 92,000 │ │ Headcount: 1 │ ZK-Proof: VALID │ └──────────────────────────────┴──────────────────────────────┘ --- [Alice] Status --- ┌──────────────────────────────┬──────────────────────────────┐ │ PUBLIC STATE │ PRIVATE STATE │ ├──────────────────────────────┼──────────────────────────────┤ │ Total Sum: 92,000 │ My Salary: 92,000 │ │ Headcount: 1 │ ZK-Proof: VALID │ └──────────────────────────────┴──────────────────────────────┘ git clone https://github.com/Kanasjnr/midnight-pulse-sdk-demo cd midnight-pulse-sdk-demo npm install && make run git clone https://github.com/Kanasjnr/midnight-pulse-sdk-demo cd midnight-pulse-sdk-demo npm install && make run git clone https://github.com/Kanasjnr/midnight-pulse-sdk-demo cd midnight-pulse-sdk-demo npm install && make run - Error-prone: Manual type mapping can lead to runtime failures. - Maintenance-heavy: Every contract change requires a manual update to the SDK. - Boilerplate-intensive: Setting up contract stubs and providers involves repetitive code. - Zero Maintenance: Changing your contract automatically updates your SDK types. - Type Safety: No more runtime errors from incorrect type mapping. - Developer Velocity: Focus on your dApp logic, not the cryptographic plumbing.