Skip to main content
Version: 0.3.0

Getting Started

Set up the SDK, create an encrypted balance account, check your balance, and send a private transfer.

Prerequisites

  • Node.js 18+ or Bun
  • Testnet tokens in a standard EVM wallet

Get testnet tokens

A faucet at ebemt-faucet.vercel.app dispenses 100 of each test token (zkUSD, zkEUR, zkGBP, zkPLN) on Base Sepolia per request. Pass the EVM address of the wallet you'll use as your controller — not your zk1... address.

curl -X POST https://ebemt-faucet.vercel.app/api/mint \
-H 'content-type: application/json' \
-d '{"address":"0xYourControllerAddress"}'

Install

The SDK is published to GitHub Packages under the @cardinal-cryptography scope. GitHub Packages requires authentication for installs, so before bun add (or npm install) you need a .npmrc pointing the scope at the GitHub registry with a personal access token.

  1. Create a GitHub PAT with the read:packages scope.

  2. Add to ~/.npmrc (or a project-local .npmrc):

    @cardinal-cryptography:registry=https://npm.pkg.github.com
    //npm.pkg.github.com/:_authToken=YOUR_GITHUB_PAT
  3. Install:

    bun add @cardinal-cryptography/sdk viem

@cardinal-cryptography/sdk is dual-target: Node and browser are served by the same package via conditional exports. Node imports route to the Node entry; browser bundlers (Vite, webpack, etc.) route to the browser entry. You install one package regardless of where you run.

Create a runtime and account

The runtime provides two services that run locally on your machine:

  • Decryption -- reverses the on-chain encryption so you can read your balance
  • Proving -- generates zero-knowledge proofs that verify your transfers are valid without revealing amounts

An account holds your keys in memory and exposes decrypt, prove, and sign operations.

import { createRuntime, createFullClient } from '@cardinal-cryptography/sdk'
import { createPublicClient, http } from 'viem'
import { baseSepolia } from 'viem/chains'

const runtime = await createRuntime()

// Derives two keys from your mnemonic:
// - Encryption key (ESK): decrypts your balance, used as witness in ZK proofs
// - Controller key (CSK): authorizes transfers on-chain via EIP-712 signatures
const account = runtime.createAccountFromMnemonic('your mnemonic here')

console.log('Your ZK address:', account.zkpAddress) // zk1...

Your ZK address (zk1...) is your public identifier in the encrypted balance system. Share it with others so they can send you private transfers -- it doesn't reveal your keys or balance.

Create a client

The client connects your account to the chain. Gas is sponsored by default via a bundler and paymaster -- users don't need ETH.

const publicClient = createPublicClient({ chain: baseSepolia, transport: http() })

const client = await createFullClient({
client: publicClient,
zkpAccount: account,
})

The factory uses default bundler and paymaster endpoints. See Gas Sponsorship to customize these.

Register your EPK

The first chain interaction with a new account binds your ZK address to a controller EVM address. Every subsequent send (transfer, encrypted-to-public, ...) verifies the controller signature against this binding. One-time per (account, token).

await client.sendRegisterEpk({
token: 'zkUSD',
controller: account.controllerAddress,
})

Register before sharing your ZK address with anyone — your address can't receive tokens until the EPK is bound on-chain.

Check your balance

The SDK reads the encrypted balance from the contract, then decrypts it locally using your encryption key. No private data is sent to the network.

const balance = await client.getDecryptedBalance({ token: 'zkUSD' })
console.log(`Decrypted balance: ${balance}`)

Send an encrypted transfer

Send tokens privately to another ZK address. Under the hood, the SDK:

  1. Reads and decrypts your current balance
  2. Generates a ZK proof that the transfer is valid (the slowest step)
  3. Signs a controller authorization (EIP-712, similar to a permit signature)
  4. Submits the transaction
await client.sendEncryptedTransfer({
token: 'zkUSD',
amount: 50n,
to: 'zk1recipientAddress...',
})

Cleanup

When you're done, free the runtime resources:

runtime.destroy()

What's next?

  • Public Account -- send to encrypted addresses, register, auto-encrypt (no proving needed)
  • Read Balances -- decrypt balances and transaction history in detail
  • Encrypted Transfers -- step-by-step prepare/sign/send flow, progress callbacks
  • Account Creation -- create accounts from mnemonics, signatures, or external signers like MetaMask