DEV Community

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

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
};
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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          │
    └──────────────────────────────┴──────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Top comments (0)