Skip to main content
Version: 0.3.0

Architecture

The SDK has four layers. You primarily interact with the Client layer -- behind the scenes, it calls Account (which holds your keys), which uses Runtime (which runs the proving and decryption services), which talks to Chain (standard EVM).

Runtime (decryption + proving services)
|
Account (keys in closures: decrypt, prove, sign)
|
Client (viem client + action extensions)
|
Chain (EVM JSON-RPC)

Runtime

Provides the computationally heavy services: decryption and ZK proving. Both are WASM modules that lazy-load when first used. Created once, shared across accounts.

  • ReadRuntime -- decryption service only
  • ZkpRuntime -- decryption + proving services

@cardinal-cryptography/sdk provides the runtime factories. The same package serves Node and browser via conditional exports, each entry configured with the appropriate WASM build.

Account

Holds cryptographic keys in closures. Never exposes private keys.

  • ZkpReadAccount -- can decrypt ciphertexts
  • ZkpAccount -- can decrypt, generate ZK proofs, and sign controller authorizations

Accounts are created from a runtime. Multiple accounts can share one runtime.

Client

A viem client extended with ZKP action methods, scoped to one chain and one account. Clients use a bundler and paymaster by default -- gas is sponsored so users don't need ETH.

Best practice: create the client once with a hoisted account and reuse it across your application.

Three convenience factories:

FactoryWhat it's forBundler-backed
createEvmClientPublic account ops (register, send to encrypted, auto-encrypt)Yes
createDecryptClientRead + decrypt balances and historyNo (read-only)
createFullClientEverything: decrypt + encrypted transfersYes

Bundler + paymaster (the default)

Every write goes through the SharedAccount via an ERC-4337 bundler. Two consequences:

  • No ETH for users. A paymaster pays gas on every UserOp. End users hold tokens; they never need a separate gas balance.
  • No on-chain link between your ZKP account and the gas-payer. Every UserOp lands with msg.sender = SharedAccount. The controller key never holds funds, never appears as tx.origin. The paymaster pays from its own wallet — never from yours. Authorization is in the ZK proof + EIP-712 controller signature, not in who submitted the transaction.

This is the default for createEvmClient and createFullClient. See Gas Sponsorship for configuration.

Manual WalletClient path

Each action extension (zkpPublicActions, zkpEvmActions, zkpWalletActions, etc.) is a plain viem extension. You can extend a regular WalletClient and submit ZKP operations directly with a funded EOA -- bypassing the bundler. You forfeit gas sponsorship and the unlinkability property above. There's no convenience factory for it; assemble it yourself if you have a reason to.

Chain

Standard EVM JSON-RPC. The SDK reads from and writes to encrypted balance token contracts deployed on-chain.

Why this architecture?

The decryption and proving services are WASM modules -- loading them has a real cost, especially in browsers:

  • EVM-only operations (register, send to encrypted address) load no WASM at all -- pure RPC calls
  • Decrypt-only loads only the decryption WASM
  • Full loads both decryption and proving WASM

By separating Runtime → Account → Client, you control exactly what gets loaded. A portfolio dashboard doesn't need the prover. An onboarding page doesn't need either.

The account layer also provides security isolation: keys live in closures, separate from the network-facing client. The client never sees your private keys.