skillZs
★ LIVE SKILL TAGS ★
>>> LIVE SKILLS INDEX <<<
* OPEN SOURCE *
NO LOGIN, NO TRACKING
※ REAL INSTALL DATA ※
← back to all skills
kali-decoder/midnight-skills120 installs

example-zk-loan-application

Build a zero-knowledge loan application on Midnight Network — privately evaluate credit score, income, and employment tenure inside a Compact circuit with Schnorr attestation, record only loan outcomes on-chain, and wire attestation API + CLI + optional Next.js frontend. Covers zkloan-credit-scorer.compact (witness-derived identity, tiered eligibility, PIN migration, admin governance), Jubjub Schnorr verification, attestation provider registration, and low-level deploy/call via 1AM wallet. Use for private credit scoring, attested witness data, lending dApps, or teaching disclose()/witness patterns with off-chain trusted signers. Triggers: ZK loan, credit scorer, attestation API, Schnorr signature Compact, requestLoan, private credit score, loan eligibility tiers, PIN identity, zkloan. Also use when extending leaderboard-dapp or locker-dapp wallet/provider patterns to multi-service monorepos.

How do I install this agent skill?

npx skills add https://github.com/kali-decoder/midnight-skills --skill example-zk-loan-application
view source ↗

Is this agent skill safe to install?

  • Gen Agent Trust Hubpass

    The skill provides a technical template and architectural guidance for building a zero-knowledge (ZK) loan application on the Midnight Network. It implements privacy-preserving logic, identity derivation from witnesses, and off-chain attestation. No malicious patterns or security risks were identified.

  • Socketpass

    No alerts

  • Snykwarn

    Risk: MEDIUM · 2 issues

What does this agent skill do?

Midnight Network ZK Loan Application

A privacy-preserving loan dApp where applicants prove eligibility inside a ZK circuit without revealing credit score, income, or tenure on-chain. A trusted attestation provider signs credit data off-chain; the contract verifies the Schnorr signature in-circuit and writes only the loan outcome (status + authorized amount) to the ledger.

What this skill produces:

  • contract/ — schnorr.compact + zkloan-credit-scorer.compact, witnesses, compile scripts
  • zkloan-credit-scorer-attestation-api/ — REST server that Schnorr-signs credit profiles
  • zkloan-credit-scorer-cli/ — deploy, register providers, request/respond to loans (headless wallet)
  • app/loan/ (optional) — Next.js + 1AM wallet UI (copy patterns from templates/leaderboard-dapp/)
  • lib/zkloan.ts — deploy, requestLoan, respondToLoan, admin calls, ledger decode
  • lib/midnight.ts — wallet session + patched indexer provider (copy from references/midnight-session.md)
  • public/zk/zkloan/ — ZK proving assets synced from contract build

Shared references (canonical provider + troubleshooting — do not duplicate in prompts):

  • references/midnight-session.md — createConnectedSession, indexer patch, deploy/call helpers
  • references/gotchas.md — preprod deploy hangs, GraphQL offset: null, ZK asset paths
  • references/versions.json — pinned @midnight-ntwrk/* versions

Primary references:

  • example-leaderboard-dapp/ / templates/leaderboard-dapp/ — Next.js + 1AM, low-level deploy/call, indexer reads
  • example-locker-dapp/ — witness private state, createUnprovenDeployTx + submitTxAsync
  • example-counter/ — headless CLI wallet, monorepo workspaces, vitest contract tests
  • compact/ — disclose(), witnesses, persistentHash, Map, Set, new type, pure circuits
  • security/ — never trust ownPublicKey() for caller identity; witness-derived keys
  • indexer/ — read public loan ledger without wallet

Key architecture notes:

  • Credit score, income, tenure, and attestation signature stay private (witness only)
  • Only loan status + authorized amount + derived user pubkey bytes appear on-chain
  • Never use ownPublicKey() for auth — it is prover-supplied and bypassable; derive identity from getUserSecret() witness + PIN via deriveUserPublicKey / deriveAdminPublicKey
  • Attestation binds to transientHash(userPubKeyBytes) inside evaluateApplicant
  • Eligibility tiers: ≥700 + ≥$2k + ≥24mo → $10k; ≥600 + ≥$1.5k → $7k; ≥580 → $3k; else rejected
  • requestLoan may return Proposed when amount exceeds tier max — user calls respondToLoan to accept/decline
  • PIN change migrates loans in batches of 5 via changePin + onGoingPinMigration ledger map
  • Use createUnprovenDeployTx + submitTxAsync — not deployContract() (hangs on preprod)
  • Node.js ≥ 22 required for @midnight-ntwrk/wallet-sdk-shielded (Iterator helpers on Map.values())

Workflow

When helping the user, follow this sequence:

  1. Monorepo — root package.json workspaces: contract, attestation-api, cli
  2. Contract — compile Compact (schnorr.compact + zkloan-credit-scorer.compact) + witnesses
  3. Attestation API — generate/load provider Jubjub keypair; sign credit profiles; expose REST endpoint
  4. Deploy — CLI or browser: low-level deploy, persist userSecretKey + contract address in private state
  5. Register provider — admin calls registerProvider(providerId, providerPk) on-chain
  6. Request loan — fetch attestation → populate private state → requestLoan(amount, secretPin)
  7. Respond — if status is Proposed, call respondToLoan(loanId, secretPin, accept)
  8. Read state — indexer GraphQL → decode loans nested map (public)
  9. Frontend (optional) — connect 1AM, deploy/join, loan form, loan table, admin panel

1) Monorepo Structure

zkloan-credit-scorer/
├── package.json                    # workspaces + pinned midnight-js 4.0.4 / ledger 8.0.3
├── .nvmrc                          # 22
├── contract/
│   ├── package.json
│   └── src/
│       ├── schnorr.compact
│       ├── zkloan-credit-scorer.compact
│       ├── witnesses.ts
│       ├── index.ts
│       └── managed/zkloan-credit-scorer/   # compiler output (gitignored)
├── zkloan-credit-scorer-attestation-api/
│   └── src/                        # Express/Fastify REST — sign credit profiles
├── zkloan-credit-scorer-cli/
│   └── src/                        # deploy, register-provider, request-loan, admin
└── zkloan-dapp/                    # optional Next.js frontend
    ├── lib/midnight.ts             # copy from templates/leaderboard-dapp
    ├── lib/zkloan.ts
    ├── app/loan/LoanClient.tsx
    ├── contract/                   # symlink or workspace import from ../contract
    ├── scripts/sync-zk-assets.mjs
    └── public/zk/zkloan/

Root package.json engines: "node": ">=22.0.0". Workspaces pin @midnight-ntwrk/compact-runtime: ^0.16.0, @midnight-ntwrk/midnight-js-*: 4.0.4, @midnight-ntwrk/ledger-v8: 8.0.3.


2) Compact Contract

Schnorr module (contract/src/schnorr.compact)

Verifies Jubjub Schnorr signatures inside the circuit. Temporary polyfill until jubjubSchnorrVerify ships in Compact Standard Library.

module schnorr {
  import CompactStandardLibrary;
  export struct SchnorrSignature { announcement: JubjubPoint; response: Field; }
  witness getSchnorrReduction(challengeHash: Field): [Field, Uint<248>];
  export circuit schnorrVerify<#n>(msg: Vector<n, Field>, signature: SchnorrSignature, pk: JubjubPoint): [] { /* ... */ }
  export pure circuit schnorrChallenge(...): Field { /* transientHash challenge */ }
}

JubjubPoint equality: compare jubjubPointX() / jubjubPointY() — struct == is reference equality and always fails for fresh points.

Main contract (contract/src/zkloan-credit-scorer.compact)

pragma language_version >= 0.22 && <= 0.23;
import CompactStandardLibrary;
import "schnorr" prefix Schnorr_;

export enum LoanStatus { Approved, Rejected, Proposed, NotAccepted }
export struct LoanApplication { authorizedAmount: Uint<16>; status: LoanStatus; }

export new type UserSecretKey = Bytes<32>;
export new type UserPublicKey = Bytes<32>;
export new type AdminPublicKey = Bytes<32>;

export ledger blacklist: Set<UserPublicKey>;
export ledger loans: Map<Bytes<32>, Map<Uint<16>, LoanApplication>>;
export ledger onGoingPinMigration: Map<Bytes<32>, Uint<16>>;
export ledger contractAdmin: AdminPublicKey;
export ledger providers: Map<Uint<16>, JubjubPoint>;

witness getAttestedScoringWitness(): [Applicant, Schnorr_SchnorrSignature, Uint<16>];
witness getUserSecret(): UserSecretKey;

export pure circuit deriveUserPublicKey(sk: UserSecretKey, pin: Uint<16>): UserPublicKey { /* persistentHash domain "zkloan:user:pk:v1" */ }
export pure circuit deriveAdminPublicKey(sk: UserSecretKey): AdminPublicKey { /* "zkloan:admin:pk:v1" */ }

export circuit requestLoan(amountRequested: Uint<16>, secretPin: Uint<16>): [] { /* ... */ }
export circuit respondToLoan(loanId: Uint<16>, secretPin: Uint<16>, accept: Boolean): [] { /* ... */ }
export circuit changePin(oldPin: Uint<16>, newPin: Uint<16>): [] { /* batched migration, 5 per tx */ }

// Admin (all guard with deriveAdminPublicKey(getUserSecret()) == contractAdmin)
export circuit blacklistUser(account: UserPublicKey): [] { /* ... */ }
export circuit registerProvider(providerId: Uint<16>, providerPk: JubjubPoint): [] { /* ... */ }
export circuit rotateAdmin(newAdmin: AdminPublicKey): [] { /* ... */ }

Privacy boundary:

DataVisibility
Credit score, income, tenurePrivate (witness)
Attestation signaturePrivate (ZK input)
User secret + PINPrivate (witness / circuit input)
Derived UserPublicKey (loan map key)Public (ledger)
Loan status + authorized amountPublic (ledger)
contractAdmin, providers, blacklistPublic (ledger)

Compile:

cd contract
npm run compact    # → src/managed/zkloan-credit-scorer/
npm run build
cd ..

For browser UI also run npm run sync:assets (copies keys/zkir → public/zk/zkloan/).


3) Witnesses

contract/src/witnesses.ts:

export type ZKLoanCreditScorerPrivateState = {
  creditScore: bigint;
  monthlyIncome: bigint;
  monthsAsCustomer: bigint;
  attestationSignature: SchnorrSignature;
  attestationProviderId: bigint;
  userSecretKey: Uint8Array;   // 32 bytes — authoritative caller identity
};

export const witnesses = {
  getAttestedScoringWitness: (ctx) => [ctx.privateState, [profile, sig, providerId]],
  getSchnorrReduction: (ctx, challengeHash) => [ctx.privateState, [q, r]],  // challenge / 2^248
  getUserSecret: (ctx) => [ctx.privateState, ctx.privateState.userSecretKey],
};

Before requestLoan, TypeScript must:

  1. Call attestation API with credit profile + user's derived pubkey hash
  2. Store returned signature + provider ID in private state alongside credit fields
  3. Ensure userSecretKey is 32 bytes (persist in localStorage or CLI keystore)

4) Attestation API

Trusted off-chain signer. Flow:

sequenceDiagram
  participant User
  participant API as Attestation API
  participant Wallet as 1AM / CLI
  participant Chain as Midnight Network

  User->>API: POST /attest { creditScore, income, tenure, userPubKeyHash }
  API->>API: Schnorr-sign msg = [score, income, tenure, userPubKeyHash]
  API-->>User: { signature, providerId }
  User->>Wallet: requestLoan(amount, pin) + private state with attestation
  Wallet->>Chain: ZK tx — evaluateApplicant verifies signature in-circuit
  Chain-->>User: Loan Proposed / Approved / Rejected on ledger

Implementation checklist:

  • Generate Jubjub provider keypair at startup (or load from env)
  • Compute challenge via contract's exported pureCircuits.schnorrChallenge (same hash as in-circuit)
  • Sign with provider secret key; return { announcement, response } + providerId
  • Admin must registerProvider(providerId, providerPk) on-chain before any loan succeeds
  • Bind attestation to transientHash(deriveUserPublicKey(secret, pin)) — same hash the circuit uses

Example endpoint shape:

// POST /attest
// body: { creditScore, monthlyIncome, monthsAsCustomer, userPubKeyHash: string }
// response: { providerId, signature: { announcement: { x, y }, response } }

Run locally (default port e.g. 3001):

cd zkloan-credit-scorer-attestation-api
npm run dev

5) CLI (headless wallet)

Mirror example-counter/ CLI patterns:

CommandPurpose
deploycreateUnprovenDeployTx → stores deployer as admin via constructor
register-provider <id>Admin: on-chain provider pubkey from attestation API
request-loan <amount> <pin>Fetch attestation → set private state → requestLoan
respond-loan <id> <pin> accept|declineAccept or decline Proposed loan
list-loansIndexer read of public loans map for derived user key
change-pin <old> <new>Repeat until onGoingPinMigration cleared
blacklist-user <userPk>Admin governance

Use @midnight-ntwrk/wallet-sdk-facade + level private state provider. Point at local proof server + preprod indexer from wallet config or .env.


6) TypeScript Integration (browser)

lib/zkloan.ts — same low-level pattern as templates/leaderboard-dapp/lib/leaderboard.ts:

FunctionPurpose
getOrCreateUserSecret()Persist 32-byte secret in localStorage
deriveUserPublicKey(session, pin)Call exported pure circuit off-chain
fetchAttestation(apiUrl, profile, userPubKeyHash)POST to attestation API
deployZkLoan(session)Deploy + seed private state with secret
registerProvider(session, addr, id, pk)Admin circuit call
requestLoan(session, addr, amount, pin, profile)Attest → update private state → requestLoan
respondToLoan(session, addr, loanId, pin, accept)Accept/decline proposed amount
fetchLoanState(queryUrl, addr, userPkBytes)Indexer poll + decode nested loans map
import { createUnprovenDeployTx, submitCallTxAsync, submitTxAsync } from '@midnight-ntwrk/midnight-js-contracts';
import { ContractState } from '@midnight-ntwrk/compact-runtime';
import { CompiledZKLoanContract, Contract, ledger } from '../contract/src/index';

export const ZK_PATH = '/zk/zkloan';
const PRIVATE_STATE_ID = 'zkloanPrivateState';

export async function requestLoan(session, contractAddress, amount, pin, attestation) {
  await session.providers.privateStateProvider.set(PRIVATE_STATE_ID, {
    ...attestation,
    userSecretKey: getOrCreateUserSecret(),
  });
  await submitCallTxAsync(session.providers, {
    compiledContract: CompiledZKLoanContract,
    contractAddress,
    circuitId: 'requestLoan',
    args: [BigInt(amount), BigInt(pin)],
    privateStateId: PRIVATE_STATE_ID,
  });
}

ZK asset path: /zk/zkloan (synced to public/zk/zkloan/).


7) Browser UI

Copy structure from templates/leaderboard-dapp/app/leaderboard/LeaderboardClient.tsx:

  1. Connect — detectWallet() → wallet.connect('preprod') → createConnectedSession(api, ZK_PATH)
  2. Deploy / Join — deploy new contract or paste existing address; persist in localStorage
  3. Admin panel (deployer only) — register attestation provider ID + pubkey from API
  4. Request loan — form: amount, secret PIN, credit fields → call attestation API → requestLoan
  5. My loans — decode indexer state for user's derived pubkey; show status badges (Approved / Proposed / Rejected)
  6. Respond — if Proposed, buttons to accept max eligible amount or decline
  7. Optional — PIN change wizard (call changePin repeatedly until migration complete)

Indexer reads work without wallet for public loan records — only writes need connection + attestation.


8) End-to-end local run

# Terminal 1 — proof server
docker run -d -p 6300:6300 midnightntwrk/proof-server:8.0.3 -- \
  midnight-proof-server --network preprod

# Terminal 2 — attestation API
cd zkloan-credit-scorer-attestation-api && npm run dev

# Terminal 3 — CLI or Next.js UI
cd zkloan-credit-scorer-cli   # or zkloan-dapp/
npm install
cd ../contract && npm run compact && npm run build && cd ..
npm run sync:assets           # UI only
npm run deploy                # CLI
npm run register-provider 1
npm run request-loan 5000 1234

Wallet: 1AM extension on Preprod, proof server http://localhost:6300, tNIGHT + tDUST from faucet.


9) Prerequisites

ComponentVersion
Node.js≥ 22 (required for shielded wallet SDK)
Compact compiler0.22–0.23 (pragma language_version)
Compact runtime0.16.0
Ledger8.0.3
midnight-js4.0.4
proof server8.0.3
DockerFor local proof server
node --version          # v22+
compact compile --version
compact update
docker run -d -p 6300:6300 midnightntwrk/proof-server:8.0.3 -- \
  midnight-proof-server --network preprod

10) Security checklist

  • Do not use ownPublicKey() for blacklist, admin, or user identity checks
  • Attestation provider private key lives only on attestation server — never in browser private state
  • disclose() marks witness values safe to cross into ledger reads; credit fields never get disclosed
  • Blacklist stores derived UserPublicKey, not wallet addresses
  • Re-deploy if domain-separator strings change — persistentHash preimages affect derived keys
  • Rate-limit attestation API; validate input ranges before signing

11) Production notes

  • Deploy attestation API behind HTTPS; rotate provider keys via rotateAdmin + re-register provider
  • Frontend: Vercel/Netlify build step runs npm run compact && npm run sync:assets
  • Store default contract address in env or localStorage
  • For mainnet, update network ID, indexer URLs, proof server via wallet getConfiguration()
  • Pin versions from references/versions.json; cross-check support matrix

Quick Commands

# Contract only
cd contract && npm install && npm run compact && npm run build

# Full monorepo (from root)
npm install
npm run validate:registry   # if added to workspace scripts
cd contract && npm run test:compile

# Optional browser UI (after adding zkloan-dapp from leaderboard template)
cd zkloan-dapp
npm install && npm run compact && npm run sync:assets && npm run dev

Open http://localhost:3000 → Connect 1AM → Deploy → Register provider → Request loan → View outcome on-chain.

Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.

<a href="https://skillzs.dev/skills/kali-decoder/midnight-skills/example-zk-loan-application">View example-zk-loan-application on skillZs</a>