Private Payments Tutorial

This tutorial helps you build a private token transfer and is a great introduction to Light. If you need any help integrating, join us in our Discord.
At a high level, here's what you'll accomplish in this tutorial:
  1. 1.
    Create a new Light user
  2. 2.
    Top up the user's shielded balance with SOL.
  3. 3.
    Transfer SOL privately to another Light user by calling one of Light's built-in native Private Solana Programs
We'll cover what to do after that in our next tutorial.

Step 1: Create a user

Install zk.js in your project directory. If you don't have an existing project, set up a new project first.
yarn add @lightprotocol/zk.js
While we assume a node environment for this tutorial, zk.js also works in browser environments.
After installing zk.js, import it into your project. We also want to configure anchor for testing locally and initialize a local TestRelayer instance, like below:
import * as light from '@lightprotocol/zk.js'
import * as anchor from "@coral-xyz/anchor";
// Configure testing environment
let rpcUrl = "";
const provider = anchor.AnchorProvider.local(
const main = async () => {
// Replace this with your user's Solana wallet
const solanaWallet = anchor.web3.Keypair.generate()
// We want to shield SOL, so let's airdrop some public test tokens
// to the user's Solana wallet.
await light.airdropSol({
connection: provider.connection,
recipientPublicKey: solanaWallet.publicKey,
lamports: 1e9,
// The relayer that will execute the transfer later
const testRelayer = new TestRelayer({
relayerPubkey: solanaWallet.publicKey,
relayerRecipientSol: solanaWallet.publicKey,
relayerFee: new BN(100_000),
payer: solanaWallet,
Now, initialize a Provider:
const provider = await light.Provider.init({
wallet: solanaWallet,
relayer: testRelayer,
The Provider stores fundamental information for you, such as the RPC and relayer URLs. We should provide at least the user's wallet, the relayer, and our default confirmConfig, and we can keep the default values for the remaining fields.
Next, we'll add the provider to a user instance. We want to use the user instance later to fetch our balance and execute payments.
const user = await light.User.init({ provider });
Before we get to the next step, let's take a quick look at why using the user class is so powerful.
When we initialize the user , it automatically derives the user's decryption keys from the provided wallet and caches its shielded balance.
Shielded balances are a specific example of private state.
Private state differs from Solana's regular public state in that private state is encrypted and hence "owned" by one user or multiple users (via shared secrets) that can decrypt the state.
The user class also exposes a range of convenient higher-level methods, such as shield and transfer.

Step 2: Shielding SOL

After the code we just added, we call the shield method and provide the amount and token we want to shield. In this case, we shield 0.5 SOL.
await user.shield({
publicAmountSol: '0.5',
token: 'SOL',
// This should now be 0.5 SOL. This also caches the latest balance in your local user instance so you can use it later.
console.log(await user.getBalance())
By shielding tokens, Light locks them in a global escrow and emits a note representing ownership of the tokens. This note is encrypted to the user's encryption key and, together with the user's other notes, makes up the user's total private balance.

Step 3: Transferring SOL

To execute a private transfer, we first create another Light account to which we want to send the tokens. We then call the transfer method of our sender's user instance to transfer 0.1 SOL.
// Create a random recipient publicKey
const testRecipientPublicKey = new light.account().getPublicKey();
// Execute the transfer
const response = await user.transfer({
amountSol: '0.1',
token: 'SOL',
recipient: testRecipientPublicKey,
// We can check the transaction that gets executed on-chain and won't
// see any movement of tokens, whereas our user's private balance changed!
console.log(await user.getBalance())
The important code to notice is that the transfer recipient is not a public Solana address but another Light user. The transfer gets executed as a private state transition; our user's original ownership note (UTXO) is nullified, and a new UTXO gets emitted and encrypted to the recipient's encryption public key.
Private token transfers are a common private state transition, and the shield and transfer methods that we just used are high-level wrappers around it. Underneath, the method calls one of Light's built-in native Private Solana Programs, which specifies this type of state transition and its constraints. We've built several such 'System Programs' so you can access the most common private state transitions easily without having to build your own PSP.
Feel free to look at our open-source demo to see how everything looks when put together: (it also has all the latest changes!)

Next up

  • After you've sent your first private transfer, you'll probably want to create more complex payment flows. For this, check out the zk.js reference!
  • You can also build your own Private Solana Program to support custom state transitions! Be it for public on-chain games with private state, encrypted order books, or something else entirely. Follow the custom PSP tutorial, or jump directly into one of our custom PSP reference implementations to get started!