Account Creation
ZKP accounts hold encryption and signing keys in closures -- private keys are never exposed. There are several ways to create one depending on your use case.
Two keys per account
Each account derives two independent keys from the same seed:
- ESK (Encryption Secret Key) -- decrypts your balances and serves as the witness in ZK proofs. Uses the Grumpkin curve (optimized for ZK circuits).
- CSK (Controller Spending Key) -- authorizes transfers on-chain via EIP-712 signatures. Uses secp256k1 (the EVM curve), so standard wallets and hardware signers can sign.
Your ZK address (zk1...) is derived from the ESK. Share it to receive encrypted transfers. See Keys & Addresses for the full derivation details.
From a mnemonic (simplest)
All keys derived from a BIP-39 mnemonic. Best for standalone wallets.
import { createRuntime } from '@cardinal-cryptography/sdk'
const runtime = await createRuntime()
const account = runtime.createAccountFromMnemonic('abandon abandon ... about')
// account.zkpAddress — your ZK address (zk1...), share this to receive transfers
// account.controllerAddress — EVM address derived from CSK, used for on-chain authorization
// account.decrypt() — decrypt ElGamal ciphertexts using ESK
// account.prove() — generate ZK proofs using ESK as witness
// account.signControllerAuth() — sign EIP-712 authorization using CSK
From EIP-712 signature (linked wallet) or from seed
Derive keys from arbitrary seed bytes — typically an EIP-712 wallet signature, which links the ZKP account to an existing wallet. No mnemonic needed.
const signature = await walletClient.signTypedData({ /* linking message */ })
const account = runtime.createAccountFromSeed(signature)
With an external signer (MetaMask, hardware wallet)
Delegate controller signing to an external wallet (MetaMask, Ledger, etc.). The ESK is still derived locally from your mnemonic or signature -- only the CSK signing is delegated.
This means proof generation and decryption happen locally, but the EIP-712 controller authorization is signed by the external wallet.
import { eskFromMnemonic } from '@cardinal-cryptography/sdk'
// ESK is still derived locally -- only the controller signing is delegated
const esk = eskFromMnemonic('your mnemonic here')
const account = runtime.createAccount(esk, {
getAddress: () => walletClient.account.address,
signTypedData: (args) => walletClient.signTypedData(args),
})
The signer object must implement getAddress() and signTypedData() -- viem's WalletClient satisfies this interface.
Multiple accounts from one seed
All factories that take a mnemonic or seed accept an optional accountIndex (default 0) for HD-style derivation. The same mnemonic + a different index yields an independent ZK account — different ESK, different CSK, different zk1... address.
const main = runtime.createAccountFromMnemonic(mnemonic) // index 0
const savings = runtime.createAccountFromMnemonic(mnemonic, 1)
const trading = runtime.createAccountFromMnemonic(mnemonic, 2)
Works the same for createAccountFromSeed, createReadAccountFromMnemonic, createReadAccountFromSeed, and the lower-level eskFromMnemonic / cskFromMnemonic / eskFromSeed / cskFromSeed helpers. Each derived account still needs its own on-chain EPK registration before receiving transfers.
Read-only account (decrypt only)
For viewing balances without signing or proving. No prover needed -- use a read-only runtime (solver only).
import { createReadRuntime } from '@cardinal-cryptography/sdk'
const runtime = await createReadRuntime()
const account = runtime.createReadAccount(esk)
// or: runtime.createReadAccountFromMnemonic('...')
// or: runtime.createReadAccountFromSeed(sig)
Account type summary
| Type | Capabilities | Use case |
|---|---|---|
ZkpReadAccount | decrypt | Balance viewing, history |
ZkpAccount | decrypt + prove + sign | Transfers, full wallet |
First chain interaction: register the EPK
A freshly-derived account exists only locally. Before sharing your ZK address with anyone, register the EPK on-chain — the contract uses this binding to route incoming encrypted transfers, and to verify the controller signature on outgoing ones:
await client.sendRegisterEpk({
token: 'zkUSD',
controller: account.controllerAddress,
})
Which creation method to use
| Method | Keys derived from | CSK signing | Best for |
|---|---|---|---|
createAccountFromMnemonic | Mnemonic | Local (derived) | Standalone wallets -- start here |
createAccountFromSeed | Seed bytes (e.g. EIP-712 signature) | Local (derived) | Linked wallet flow |
createAccount(esk, signer) | ESK local, CSK external | Delegated to wallet | DApp integrations (MetaMask, Ledger) |
createReadAccount(esk) | ESK only | N/A | Portfolio viewers, dashboards |