TypeScript SDK for interacting with the ZK Payroll smart contracts.
npm install @zk-payroll/sdkThe SDK provides configuration presets for common environments to simplify initialization:
import { PayrollService, ConfigPresets } from "@zk-payroll/sdk";
// Initialize config for a specific environment
const config = ConfigPresets.testnet()
.withContractId("CCONTRACT_ID...")
.withProofConfig({
wasmUrl: "https://cdn.example.com/payroll_circuit.wasm",
zkeyUrl: "https://cdn.example.com/payroll_circuit.zkey",
})
.build(); // Validates required fields
// Initialize service
const service = new PayrollService(config);
// Process a private payment
await service.processPayment(
"G...", // Recipient Stellar address
1000n // Amount
);The SDK enforces strict schema validation for all configuration parameters (network, rpcUrl/networkUrl, contractId/contractIds, proofConfig, retryPolicy, and featureFlags) before operations run.
import { ConfigPresets, validateConfig, assertValidConfig } from "@zk-payroll/sdk";
// Using ConfigPresets for a minimal valid setup:
const config = ConfigPresets.testnet()
.withContractId("CAKZGMMMJOHMSZ5V3DYKCUDNTIWBG57MAMFJDSVICNWUNVXLX6EZN3NC")
.build();
// Direct schema validation utility
const validation = validateConfig(config);
if (!validation.isValid) {
console.error("Config errors:", validation.errors);
}
// Asserts configuration validity or throws structured ValidationError
assertValidConfig(config);The ConfigBuilder and assertValidConfig fail fast with structured ValidationError (code: CONFIG_VALIDATION_ERROR) if required fields are missing or malformed:
// Throws ValidationError: "Configuration validation failed:\n- contractId is malformed: invalid_id"
ConfigPresets.testnet().withContractId("invalid_id").build();For safe retries, pass an idempotencyKey when processing a payment.
import { PayrollService, createPaymentIdempotencyKey } from "@zk-payroll/sdk";
const idempotencyKey = createPaymentIdempotencyKey({
recipient: "G...",
amount: 1000n,
asset: "native",
});
await service.processPayment({
recipient: "G...",
amount: 1000n,
asset: "native",
idempotencyKey,
});- Typed Contract Clients: Fully typed client wrappers for PayrollRegistry, SalaryCommitment, ProofVerifier, and PaymentExecutor contracts.
- ZK Proof Generation: Client-side proof generation using snarkjs for privacy.
- Caching: Built-in caching for proofs and circuit artifacts.
- Error Handling: Robust error typing and management.
- Mock Testing Environment: Comprehensive testing utilities for unit tests without a live network.
- Setup Checklist Generator: Generates a pre-payroll integration checklist covering config, network, contracts, treasury, proofs, wallet, and test fixtures.
Use this section to pick the right environment before you wire the SDK into a product. The package supports browsers (wallets, dashboards) and Node.js backends (workers, automation), but secrets and signing paths differ.
| Concern | Browser (frontend) | Backend (Node worker / service) |
|---|---|---|
| Wallet signing | User wallets (Freighter, Albedo) via adapters | Server-held keys from a secrets manager — not browser extensions |
| Proof generation | On-device with snarkjs; prefer Web Workers so the UI stays responsive | On the worker/process for batch payroll; good for heavy or unattended jobs |
| Secrets / witnesses | Never embed Stellar secret keys (S…) or long-lived note secrets in frontend code, env shipped to the client, or localStorage |
Load signer material from env / KMS / secrets manager; never log full witnesses |
| Next.js / SSR | Import SDK only in Client Components ("use client") |
Do not import the browser wallet path in Server Components or Route Handlers for UI signing |
| Best fit | Employee/employer dashboards, interactive connect + sign | Payroll automation, queues, retries, multi-payment workers |
Supported runtime versions: Runtime Support Matrix.
Interactive apps should connect a wallet, generate or request proofs without blocking the main thread when possible, and keep sensitive material off the client bundle.
// Frontend / Next.js Client Component — "use client"
import { FreighterAdapter } from "@zk-payroll/sdk";
const wallet = new FreighterAdapter();
if (wallet.isAvailable()) {
const publicKey = await wallet.connect();
// Build XDR with public flows; sign via the adapter — never with a hardcoded secret key
// const signed = await wallet.signTransaction(xdr);
}- Prefer Worker-based proof generation for multi-second circuit work.
- For App Router apps, follow Next.js Integration (client boundary rules).
- Wallet details: Wallet Adapters.
Backend services own automation: queue jobs, generate proofs on the server, sign with keys that never leave the host, and submit to RPC.
// Node worker — secrets from environment / secrets manager only
import { PayrollService, ConfigPresets } from "@zk-payroll/sdk";
const config = ConfigPresets.testnet()
.withContractId(process.env.CONTRACT_ID!)
.withProofConfig({
wasmUrl: process.env.WASM_URL!,
zkeyUrl: process.env.ZKEY_URL!,
})
.build();
const service = new PayrollService(config);
// Signer secret: process.env / KMS — never commit S… keys or put them in browser env- End-to-end worker prototype: Backend Worker Quickstart.
- Production patterns (config, secret handling, retries): Backend Integration Guide.
| Do | Don't |
|---|---|
Keep note secret / nullifier inputs in memory only as long as needed for proving |
Log witnesses, proofs with private inputs, or secret keys |
Use HTTPS CDN or authenticated artifact hosts for .wasm / .zkey |
Ship production signing keys in frontend env (NEXT_PUBLIC_*, Vite VITE_*, etc.) |
| Rotate and scope backend signer keys; prefer HSM/KMS where possible | Reuse a single hot key across untrusted multi-tenant frontends |
Proof APIs and witness shapes: ZK Proof Generation.
- Browser and Server Usage Guide
- Runtime Support Matrix
- Wallet Adapters
- ZK Proof Generation
- Worker Proof Generation
- Next.js Integration
- Backend Worker Quickstart
- Backend Integration Guide
The SDK includes production-ready ZK proof generation using snarkjs:
import { SnarkjsProofGenerator, MemoryCacheProvider } from "@zk-payroll/sdk";
// Configure circuit artifacts
const config = {
wasmUrl: "https://cdn.example.com/payroll_circuit.wasm",
zkeyUrl: "https://cdn.example.com/payroll_circuit.zkey",
artifactCacheTTL: 86400, // 24 hours
};
// Create generator with caching
const cache = new MemoryCacheProvider<string>();
const generator = new SnarkjsProofGenerator(config, cache);
// Generate proof
const witness = {
recipient: "GDZQHV...",
amount: 1000000n,
nullifier: 123456789n,
secret: 987654321n,
};
const proof = await generator.generateProof(witness);See ZK Proof Generation Guide for detailed documentation.
Teams building internal payroll automation workers can follow the Backend Worker Quickstart for a practical end-to-end prototype covering setup, polling, retries, and event handling.
The SDK includes a powerful mock testing environment for writing unit tests:
import { MockContractEnvironment, MockPayrollContract } from "@zk-payroll/sdk";
const mockEnv = new MockContractEnvironment();
mockEnv.expectInvoke("deposit").toReturn("tx_hash_123");
const mockContract = new MockPayrollContract(mockEnv);
const txHash = await mockContract.deposit(1000n);See the Testing Guide for complete documentation.
Runnable examples covering two core use cases are in the examples/ directory.
Each example works out of the box in demo mode (no Stellar node required) and switches to a
live network automatically when the relevant environment variables are set.
examples/employee-onboarding.ts
Shows how to onboard a new employee: verify they have no existing payroll account, fund it with an initial allocation, and confirm the deposit was recorded.
npx tsx examples/employee-onboarding.tsShows how to run a full private payroll batch: configure SnarkjsProofGenerator with circuit
artifacts and caching, wire up PayrollService, process multiple payments, and report results.
npx tsx examples/payroll-execution.tsCopy the environment variable template and fill in your values to run against a live network:
cp examples/.env.example examples/.env
# edit examples/.env
source examples/.env && npx tsx examples/payroll-execution.tsSee examples/.env.example for all available variables.
The SDK provides typed client wrappers for the core ZK Payroll contracts. Each client exposes typed methods that encode arguments and decode responses automatically.
import { PayrollRegistryClient, rpc } from "@zk-payroll/sdk";
const server = new rpc.Server("https://soroban-testnet.stellar.org");
const client = new PayrollRegistryClient(server, "CCONTRACT_ID...");
// Register a payroll relationship
await client.register(
{ employer: "G...", employee: "G...", salary: 1000n, token: "C...", metadata: "engineering" },
signer
);
// Query a registry entry
const entry = await client.getRegistry("G...", "G...", signer);
console.log(entry.salary, entry.active);
// List employees
const employees = await client.getEmployees("G...", 0, 10, signer);
// Check if a registry exists
const exists = await client.registryExists("G...", "G...", signer);import { SalaryCommitmentClient } from "@zk-payroll/sdk";
const client = new SalaryCommitmentClient(server, "CCONTRACT_ID...");
// Commit to a salary amount (hidden via hash)
await client.commit(
{ employer: "G...", employee: "G...", commitmentHash: "abcd...", cycleId: 1n },
signer
);
// Retrieve a commitment
const commitment = await client.getCommitment("G...", "G...", 1n, signer);
// Batch commit multiple salaries
await client.batchCommit("G...", [
{ employee: "G...1", commitmentHash: "abcd", cycleId: 1n },
{ employee: "G...2", commitmentHash: "ef01", cycleId: 1n },
], signer);
// Verify a commitment against a ZK proof
const isValid = await client.verifyCommitment("G...", "G...", 1n, proof, signer);
// Reveal the actual salary
await client.revealSalary("G...", "G...", 1n, 1000n, signer);Consumers rarely pass payroll data in exactly one shape (different key names, extra whitespace, comma-formatted amounts, mixed-case addresses). normalizePayrollPayload converts any of these variations into the SDK's canonical entry shape before validation, proof preparation, or transaction building:
import { normalizePayrollPayload } from "@zk-payroll/sdk";
const { entries, issues } = normalizePayrollPayload({
entries: [
{ employee_id: " E-42 ", wallet: "gabc...", asset: "xlm", amount: "1,000.50" },
],
});
// entries[0] => { employeeId: "E-42", walletAddress: "GABC...", asset: "native", amount: "1000.50", source: {...} }
// Required fields (employeeId, walletAddress, asset, amount) are never silently dropped —
// missing/unparseable data shows up as an indexed issue instead, with the original
// input still reachable via entries[issue.index].source.raw for clear validation errors.
if (issues.length > 0) {
console.log(issues);
}See the Payload Normalization Guide for the full field-by-field normalization rules.
The SDK automatically validates batch payroll payloads before submitting them to contracts, preventing empty batches, duplicate recipients, and invalid amounts:
import { BatchPayloadBuilder, validateBatchPayload, PayrollValidation } from "@zk-payroll/sdk";
const entries = [
{ recipient: "GABC...", amount: 1000n, asset: "native" },
{ recipient: "GDEF...", amount: 2000n, asset: "native" },
];
// Validate entries before building
const errors = validateBatchPayload(entries);
if (errors.length === 0) {
const payload = new BatchPayloadBuilder().addMany(entries).build();
// Safe to submit payload.entries
} else {
// Structured errors returned for UI display (code, message, field, index)
console.log(errors);
}import { ProofVerifierClient } from "@zk-payroll/sdk";
const client = new ProofVerifierClient(server, "CCONTRACT_ID...");
// Verify a ZK proof on-chain
const valid = await client.verify(
{ pi_a: ["1","2"], pi_b: [["3","4"],["5","6"]], pi_c: ["7","8"], publicSignals: ["sig1"] },
["input1"],
1, // verification key ID
signer
);
// Add a new verification key
const vkId = await client.addVerificationKey("aabbcc...", "groth16 key", signer);
// Get active verification key
const activeId = await client.getActiveVerificationKeyId(signer);
// Get verification key info
const info = await client.getVerificationKeyInfo(1, signer);import { PaymentExecutorClient } from "@zk-payroll/sdk";
const client = new PaymentExecutorClient(server, "CCONTRACT_ID...");
// Execute an immediate payment
const result = await client.execute(
{ recipient: "G...", amount: 1000n, asset: "C...", memo: "salary" },
signer
);
console.log("Transaction:", result.txHash);
// Schedule a future payment
const scheduled = await client.schedule(
{ recipient: "G...", amount: 500n, asset: "C...", executeAt: 1700000000, memo: "bonus" },
signer
);
console.log("Payment ID:", scheduled.paymentId);
// Cancel a scheduled payment
await client.cancel(scheduled.paymentId, signer);
// Get pending payments
const payments = await client.getPendingPayments("G...", 0n, 20, signer);To catch configuration integration problems (such as misconfigured RPC endpoints, invalid contract IDs, or missing/unreachable circuit artifacts) before starting runtime work, the SDK provides the validateEnvironment helper:
import { validateEnvironment } from "@zk-payroll/sdk";
const clientConfig = {
networkUrl: "https://soroban-testnet.stellar.org",
contractId: "CCONTRACT_ID...",
};
const proofConfig = {
wasmUrl: "https://cdn.example.com/payroll_circuit.wasm",
zkeyUrl: "https://cdn.example.com/payroll_circuit.zkey",
};
const result = await validateEnvironment(clientConfig, proofConfig);
if (!result.isValid) {
console.error("Environment check failed!");
for (const diagnostic of result.diagnostics) {
if (diagnostic.status === "error") {
console.error(`- [${diagnostic.component}] ${diagnostic.message}`);
}
}
} else {
console.log("Environment is ready!");
}validateEnvironment returns a SanityCheckResult containing:
isValid: boolean-trueif all validations pass with no errors.diagnostics: DiagnosticEntry[]- List of diagnostics for each checked component.
Each DiagnosticEntry contains:
component: "rpc" | "contract" | "artifacts"- The checked component.status: "success" | "warning" | "error"- The validation status.message: string- Actionable diagnostic message explaining the result.error?: Error- The caught error object, if any.details?: Record<string, unknown>- Extra context (e.g. network passphrases or RPC response details).
Before running payroll, use generateSetupChecklist to generate an integration checklist covering config, network, contracts, treasury, proofs, wallet, and test fixtures:
import { generateSetupChecklist } from "@zk-payroll/sdk";
const checklist = generateSetupChecklist(config, {
expectedNetworkPassphrase: "Test SDF Network ; September 2015",
rpcReachable: true, // result of validateEnvironment(config)
networkPassphrase: "Test SDF Network ; September 2015",
contractDeployed: true, // result of validateEnvironment(config)
treasury: { treasuryAddress: "G...", funded: true },
wallet: { name: "Freighter", isAvailable: true, isConnected: true, network: "testnet" },
testFixturesAvailable: true,
});
if (!checklist.isReady) {
for (const blocker of checklist.blockers) {
console.error(`[${blocker.category}] ${blocker.message} → ${blocker.remediation}`);
}
}Each check returns pass, warn, or fail with an actionable remediation.
See the Setup Checklist Guide for full details.
To troubleshoot issues without exposing private payroll data, capture an audit-safe snapshot of SDK configuration and runtime state:
import { createDebugSnapshot } from "@zk-payroll/sdk";
const { snapshot, redactedFieldCount, redactedKeys } = await createDebugSnapshot({
config: client.getConfig(),
state: { pendingPayments, draft, signerSecret: signer.secret() },
});
console.log(`Redacted ${redactedFieldCount} fields: ${redactedKeys.join(", ")}`);
console.log(JSON.stringify(snapshot)); // safe to attach to a support ticketThe snapshot is always JSON-serializable (BigInt, dates, cycles handled),
redacts sensitive payroll fields recursively, and carries an integrity hash
verifiable via verifyDebugSnapshot. See the Audit-Safe Debug Snapshot guide.
To diagnose slow RPC or API paths, wrap your rpc.Server with createTimedRpcServer and read back the timing metadata — without changing any existing response behavior:
import { rpc } from "@stellar/stellar-sdk";
import { createTimedRpcServer } from "@zk-payroll/sdk";
const server = createTimedRpcServer(new rpc.Server(rpcUrl));
// Pass `server` to the SDK wherever it accepts an rpc.Server.
// Existing behavior is unchanged.
// ... run payroll operations ...
const stats = server.getNetworkTimingStats();
if (stats.byOperation.simulateTransaction?.avgDurationMs > 1500) {
console.warn("simulateTransaction is slow; consider a closer RPC endpoint");
}HTTP(S) artifact fetches can be timed per-request with timeAxiosRequest or
globally (opt-in) with installAxiosTiming. Timing metadata is attached to
responses as a non-enumerable symbol and never includes payloads or private
payroll values. See the Network Request Timing guide.
The SDK provides a centralised AssetRegistry that maps asset identifiers to labels,
decimal precision, and display behaviour. Applications can extend it with any custom
Soroban token.
import { AssetRegistry, formatAmount, parseAmount } from "@zk-payroll/sdk";
// Use a built-in asset (native XLM, USDC, EUROC ship pre-registered)
const xlm = AssetRegistry.getOrThrow("native");
formatAmount(10_000_000n, xlm); // "1.0000000 XLM"
// Register a custom Soroban token
AssetRegistry.register({
id: "CTOKEN_CORP123",
symbol: "CORP",
label: "Corp Company Token",
decimals: 7,
});
const corp = AssetRegistry.getOrThrow("CTOKEN_CORP123");
parseAmount("500.00 CORP", corp); // 3_500_000_000nBefore submitting any amount to a contract, batch builder, or commitment hash, normalize
it into the asset's canonical smallest-unit bigint. normalizeCanonicalAmount accepts
any of the loose shapes payroll data arrives in (string, number, bigint) and either
an asset id string (resolved via the registry) or an AssetMetadata object:
import {
normalizeCanonicalAmount,
tryNormalizeCanonicalAmount,
RoundingMode,
} from "@zk-payroll/sdk";
// Throwing variant — canonical { amount, decimals, assetSymbol, assetId, wasRounded, original }
const { amount, assetSymbol, wasRounded } = normalizeCanonicalAmount(
" $1,000.50 XLM ", // formatted string with currency symbol + whitespace
"native", // asset id resolved via AssetRegistry
{ rounding: RoundingMode.HALF_UP }
);
// amount => 10_005_000_000n (canonical stroops)
// assetSymbol => "XLM"
// wasRounded => false (input has 2 decimals, XLM has 7 — no rounding)
// bigint inputs are already canonical (matches formatAmount) — no double-scaling
normalizeCanonicalAmount(10_005_000_000n, "native").amount; // 10_005_000_000n
// Non-throwing variant — discriminated { ok, value | error }
const result = tryNormalizeCanonicalAmount(input, "USDC");
if (!result.ok) {
console.warn(result.error.code, result.error.message);
} else {
submit(result.value.amount);
}Use normalizeCanonicalAmount instead of manually scaling strings, so submissions always
carry the canonical precision the contract expects.
See the Multi-Asset Guide for the full API reference, isolation patterns for tests, and rules for extending the registry in production.
The SDK includes a comprehensive, typed employee eligibility evaluation engine to inspect employee records before payroll resolution. It returns typed, machine-readable reason codes that make it easy for dashboards and backends to explain blocked recipients without exposing private payroll amounts.
import {
evaluateEmployeeEligibility,
evaluateBatchEligibility,
filterEligibleEmployees,
formatEligibilityReport,
EligibilityReasonCode,
EmployeeRegistry,
} from "@zk-payroll/sdk";
// Single employee evaluation
const result = evaluateEmployeeEligibility({
employeeId: "EMP-001",
recipient: "GA...",
salary: 5000000000n,
asset: "native",
status: "suspended",
});
if (!result.isEligible) {
console.log(result.primaryReasonCode); // "EMPLOYEE_SUSPENDED"
console.log(result.reasons[0].action); // "Resolve the administrative suspension prior to releasing payroll disbursements."
}
// Batch evaluation with privacy-safe diagnostic summary for dashboards
const batch = evaluateBatchEligibility([
{ employeeId: "EMP-001", recipient: "GA...", salary: 5000000000n, asset: "native" },
{ employeeId: "EMP-002", recipient: "", salary: 2000000000n, asset: "native" },
]);
const report = formatEligibilityReport(batch);
// All salaries and secrets remain redacted in logs/telemetry:
console.log(report.reasonSummary); // { MISSING_RECIPIENT_ADDRESS: 1 }| Reason Code | Category | Description | Suggested Remediation |
|---|---|---|---|
MISSING_RECIPIENT_ADDRESS |
Identity | Destination Stellar address is missing or empty | Provide a valid Stellar G... address |
INVALID_RECIPIENT_ADDRESS |
Identity | Address format is malformed or invalid | Correct public key address format |
MISSING_EMPLOYEE_ID |
Identity | Employee record identifier is missing | Assign a unique employee ID |
DUPLICATE_EMPLOYEE_ID |
Identity | Duplicate employee ID within payroll batch | Deduplicate employee records in batch |
DUPLICATE_RECIPIENT_ADDRESS |
Identity | Duplicate destination address within batch | Ensure unique recipient per payout batch |
INACTIVE_EMPLOYEE_STATUS |
Status | Account is flagged as inactive | Activate employee in HR/payroll system |
EMPLOYEE_SUSPENDED |
Status | Account is currently suspended | Resolve administrative suspension |
EMPLOYEE_TERMINATED |
Status | Employee is terminated / offboarded | Settle through severance workflows |
EFFECTIVE_DATE_FUTURE |
Status | Employment start date is in the future | Schedule payout for on/after effective date |
EFFECTIVE_DATE_EXPIRED |
Status | Contract period ended before payroll cycle | Renew contract or adjust cycle bounds |
ZERO_OR_NEGATIVE_SALARY |
Compensation | Payout amount is zero or negative | Set positive non-zero payout amount |
SALARY_EXCEEDS_MAX_LIMIT |
Compensation | Salary exceeds configured maximum ceiling | Split transaction or request limit elevation |
SALARY_BELOW_MIN_LIMIT |
Compensation | Salary is below minimum payout threshold | Combine into next cycle or verify amount |
MISSING_ASSET_IDENTIFIER |
Compensation | Asset identifier is missing | Specify token address or "native" |
UNSUPPORTED_ASSET |
Compensation | Asset is not permitted in configuration | Use an approved asset token |
COMPLIANCE_BLOCKED |
Compliance | Failed KYC/AML verification or policy hold | Complete identity verification |
PAYROLL_LOCKED |
Compliance | Administrative or security hold active | Release lock in dashboard |
SANCTION_LISTED |
Compliance | Recipient matches prohibited sanctions list | Escalate to compliance/legal team |
REGISTRY_RECORD_NOT_FOUND |
Registry | No on-chain registry record found | Register employee with contract first |
REGISTRY_RECORD_DEACTIVATED |
Registry | On-chain registry record is deactivated | Reactivate registry entry on-chain |
REGISTRY_SALARY_MISMATCH |
Registry | Resolution salary differs from on-chain | Update registry salary on-chain |
REGISTRY_TOKEN_MISMATCH |
Registry | Payment token differs from on-chain token | Use registered token contract |
CUSTOM_INELIGIBILITY_RULE |
Custom | Custom user-defined eligibility rule failed | Review custom business policy criteria |
- Terminology Guide - Canonical names for concepts shared across contracts, SDK, and dashboard
- Runtime Support Matrix - Supported Node.js and browser versions
- Browser and Backend Usage - Where to run the SDK, wallets, proofs, and secrets
- Payload Normalization - Canonicalizing payroll payloads before validation
- API Reference - Complete API documentation
- Error Handling - Public error hierarchy and recovery patterns
- ZK Proof Generation - Detailed proof generation guide
- Versioning & Compatibility - SDK semantic versioning and contract compatibility matrix
- SDK Migration Cookbook - Step-by-step upgrade checklist and migration patterns
- Troubleshooting - Solutions for common CI, dependency, and environment issues
# Install dependencies
npm install
# Build
npm run build
# Run tests
npm test
# Lint
npm run lintHaving trouble? See the Troubleshooting Guide.