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 Vision: Privacy at Scale
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.
The Problem: The "Glue Code" Friction
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:
- 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.
The Midnight SDK Generator solves this by deriving a production-ready SDK directly from your contract's metadata.
The Privacy Contract (salary.compact)
Everything starts with the contract. Using Compact, we define what data is public and what logic is private.
The Ledger
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.
export ledger {
total_salary_sum: Uint64,
employee_count: Uint64
};
The "Submit" Circuit
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.
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;
}
The "Benchmark" Circuit (Anonymity Check)
This is where the privacy guarantee is enforced. The circuit checks the employee_count before performing the comparison.
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;
}
Generating the SDK
Once the contract is written, we use the SDK Generator to bridge the gap between Compact and TypeScript.
1. Compile to Metadata
First, use the compact compiler to generate a structured JSON representation of your contract.
compact compile ./contracts/salary.compact
2. Generate the SDK
Once you have your salary.structure.json, use the generator to create your type-safe SDK:
midnight-sdk-gen ./contracts/salary.structure.json --output ./src/sdk/SalarySDK.ts
Type Mapping Support
The generator automatically maps Compact types to their most appropriate TypeScript equivalents, so you never have to guess:
| Compact Type | TypeScript Type |
|---|---|
Boolean |
boolean |
Uint<N> |
bigint |
Bytes<N> |
Uint8Array |
Maybe<T> |
`T |
{% raw %}Vector<N, T>
|
T[] |
The Application Layer (pulse.ts)
With the generated SDK, building the CLI tool is straightforward. We use an Agentic Pattern to simulate multiple participants.
1. Initializing Providers
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.
const env = process.env.MIDNIGHT_ENV || "simulated";
const providers = await getProviders(env);
2. Deploying the Contract
The team leader (or a smart contract factory) deploys the instance using the generated deploy method.
import { SalarySDK } from "./sdk/SalarySDK";
const sdk = await SalarySDK.deploy(providers);
const contractAddress = sdk.handle.address;
3. Orchestrating the Users
We loop through our simulated agents (Alice, Bob, Carol, Dave, Eve). Each agent joins the contract and submits their salary.
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);
}
4. Running the Benchmark
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.
const isAbove = await carolSdk.is_above_benchmark(carol.witness);
StateObserver.displayResult("Carol", isAbove);
Observability (The Pulse)
One of the biggest challenges in ZK development is "blind debugging." To solve this, i built a StateObserver to visualize the delta of privacy.
Side-by-Side Verification
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.
--- [Alice] Status ---
┌──────────────────────────────┬──────────────────────────────┐
│ PUBLIC STATE │ PRIVATE STATE │
├──────────────────────────────┼──────────────────────────────┤
│ Total Sum: 92,000 │ My Salary: 92,000 │
│ Headcount: 1 │ ZK-Proof: VALID │
└──────────────────────────────┴──────────────────────────────┘
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.
Conclusion: The New Way to Build ZK
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.
Key Takeaways:
- 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.
See the Pulse in 60 seconds
Clone the repository and run the multi-agent simulation on your own machine:
git clone https://github.com/Kanasjnr/midnight-pulse-sdk-demo
cd midnight-pulse-sdk-demo
npm install && make run
Top comments (0)