Skip to main content
Version: develop

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

TypeCapabilitiesUse case
ZkpReadAccountdecryptBalance viewing, history
ZkpAccountdecrypt + prove + signTransfers, 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

MethodKeys derived fromCSK signingBest for
createAccountFromMnemonicMnemonicLocal (derived)Standalone wallets -- start here
createAccountFromSeedSeed bytes (e.g. EIP-712 signature)Local (derived)Linked wallet flow
createAccount(esk, signer)ESK local, CSK externalDelegated to walletDApp integrations (MetaMask, Ledger)
createReadAccount(esk)ESK onlyN/APortfolio viewers, dashboards