Skip to main content
Creation CostSPL Token AccountCompressed Token
Per account~2,000,000 lamports~5,000 lamports
Privy handles user authentication and wallet management. You build transactions with tokens (SOL, SPL or compressed) and Privy signs them client-side:
  1. Authenticate with Privy
  2. Build unsigned transaction
  3. Sign transaction using Privy’s wallet provider
  4. Send signed transaction to RPC

What you will implement

SPLCompressed
Get BalancegetAccount()getCompressedTokenAccountsByOwner()
TransfertransferChecked()transfer()
CompressN/Acompress()
DecompressN/Adecompress()
Transaction HistorygetSignaturesForAddress()getCompressionSignaturesForOwner()
1

Prerequisites

npm install @lightprotocol/compressed-token@alpha \
            @lightprotocol/stateless.js@alpha
Connect to an RPC endpoint that supports ZK Compression (Helius, Triton)
import { createRpc } from "@lightprotocol/stateless.js";

import {
  transfer,
  compress,
  decompress,
  selectMinCompressedTokenAccountsForTransfer,
} from "@lightprotocol/compressed-token";

const rpc = createRpc(RPC_ENDPOINT);
Before we can compress or decompresss, we need:
  • An SPL mint with a token pool for compression. This token pool can be created for new SPL mints via createMint() or added to existing SPL mints via createTokenPool().
  • For compress() SPL tokens in an Associated Token Account, or
  • For decompress() compressed token accounts with sufficient balance.
import "dotenv/config";
import { Keypair } from "@solana/web3.js";
import { createRpc } from "@lightprotocol/stateless.js";
import { createMint } from "@lightprotocol/compressed-token";
import { homedir } from "os";
import { readFileSync } from "fs";

// devnet:
const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`;
// localnet:
// const RPC_URL = undefined;
const payer = Keypair.fromSecretKey(
    new Uint8Array(
        JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8"))
    )
);

(async function () {
    // devnet:
    const rpc = createRpc(RPC_URL);
    // localnet:
    // const rpc = createRpc();

    const { mint, transactionSignature } = await createMint(
        rpc,
        payer,
        payer.publicKey,
        9
    );

    console.log("Mint:", mint.toBase58());
    console.log("Tx:", transactionSignature);
})();
2

Full Code Examples

Find a complete example on Github: privy/nodejs-privy-compressed.
Send compressed tokens to another recipient, similar to SPL token transfers:
  1. Fetch compressed token accounts with getCompressedTokenAccountsByOwner
  2. Select sender accounts with selectMinCompressedTokenAccountsForTransfer
  3. Fetch a validity proof from your RPC provider to prove the account’s state correctness
  4. Build the transfer instruction and transaction
  5. Sign with Privy and send transaction
import 'dotenv/config';
import {PrivyClient} from '@privy-io/node';
import {createRpc, bn} from '@lightprotocol/stateless.js';
import {PublicKey, Transaction, ComputeBudgetProgram} from '@solana/web3.js';
import {CompressedTokenProgram, selectMinCompressedTokenAccountsForTransfer} from '@lightprotocol/compressed-token';

const transferCompressedTokens = async (
  fromAddress: string,
  toAddress: string,
  tokenMintAddress: string,
  amount: number,
  decimals: number = 6
) => {
  const connection = createRpc(process.env.HELIUS_RPC_URL!);

  const privy = new PrivyClient({
    appId: process.env.PRIVY_APP_ID!,
    appSecret: process.env.PRIVY_APP_SECRET!,
  });

  // Create public key objects
  const fromPubkey = new PublicKey(fromAddress);
  const toPubkey = new PublicKey(toAddress);
  const mintPubkey = new PublicKey(tokenMintAddress);
  const tokenAmount = bn(amount * Math.pow(10, decimals));

  // Get compressed token accounts (filter out null items from indexer)
  const accounts = await connection.getCompressedTokenAccountsByOwner(fromPubkey, {mint: mintPubkey});
  const validItems = (accounts.items || []).filter((item): item is NonNullable<typeof item> => item !== null);
  if (validItems.length === 0) {
    throw new Error('No compressed token accounts found');
  }

  // Select minimum accounts needed for transfer
  const [inputAccounts] = selectMinCompressedTokenAccountsForTransfer(validItems, tokenAmount);
  if (inputAccounts.length === 0) {
    throw new Error('Insufficient balance');
  }

  // Get validity proof to prove compressed token accounts exist in state tree.
  const proof = await connection.getValidityProof(inputAccounts.map(account => bn(account.compressedAccount.hash)));

  // Create transfer instruction
  const instruction = await CompressedTokenProgram.transfer({
    payer: fromPubkey,
    inputCompressedTokenAccounts: inputAccounts,
    toAddress: toPubkey,
    amount: tokenAmount,
    recentInputStateRootIndices: proof.rootIndices,
    recentValidityProof: proof.compressedProof,
  });

  // Create transaction
  const transaction = new Transaction();
  transaction.add(ComputeBudgetProgram.setComputeUnitLimit({units: 300_000}));
  transaction.add(instruction);

  // Get recent blockhash
  const {blockhash} = await connection.getLatestBlockhash();
  transaction.recentBlockhash = blockhash;
  transaction.feePayer = fromPubkey;

  // Sign with Privy
  const signResult = await privy.wallets().solana().signTransaction(process.env.TREASURY_WALLET_ID!, {
    transaction: transaction.serialize({requireAllSignatures: false}),
    authorization_context: {
      authorization_private_keys: [process.env.TREASURY_AUTHORIZATION_KEY!]
    }
  });
  const signedTx = (signResult as any).signed_transaction || signResult.signedTransaction;
  if (!signedTx) {
    throw new Error('Privy returned invalid response: ' + JSON.stringify(signResult));
  }
  const signedTransaction = Buffer.from(signedTx, 'base64');

  // Send transaction
  const signature = await connection.sendRawTransaction(signedTransaction, {
    skipPreflight: false,
    preflightCommitment: 'confirmed'
  });
  await connection.confirmTransaction(signature, 'confirmed');

  return signature;
};

export default transferCompressedTokens;```

Get balances

Fetch SPL and compressed token balances.
import {PublicKey} from '@solana/web3.js';
import {createRpc} from '@lightprotocol/stateless.js';
import {HELIUS_RPC_URL} from './config.js';

export async function getCompressedBalances(ownerAddress: string) {
  const rpc = createRpc(HELIUS_RPC_URL);
  const owner = new PublicKey(ownerAddress);

  // Get compressed token accounts (filter out null items from indexer)
  const compressedAccounts = await rpc.getCompressedTokenAccountsByOwner(owner);
  const validItems = (compressedAccounts.items || []).filter((item): item is NonNullable<typeof item> => item !== null);

  // Aggregate balances by mint
  const balances = new Map<string, bigint>();
  for (const account of validItems) {
    if (account.parsed) {
      const mint = account.parsed.mint.toBase58();
      const amount = BigInt(account.parsed.amount.toString());
      const current = balances.get(mint) || 0n;
      balances.set(mint, current + amount);
    }
  }

  return {
    tokens: Array.from(balances.entries()).map(([mint, amount]) => ({
      mint,
      amount: amount.toString(),
      accounts: validItems.filter(a => a.parsed?.mint.toBase58() === mint).length
    }))
  };
}

Get transaction history

Fetch compressed token transaction history for an owner, with optional detailed compression information.
import 'dotenv/config';
import {createRpc} from '@lightprotocol/stateless.js';
import {PublicKey} from '@solana/web3.js';

const getTransactionHistory = async (
  ownerAddress: string,
  limit: number = 10,
  includeDetails: boolean = false
) => {
  const connection = createRpc(process.env.HELIUS_RPC_URL!);

  const owner = new PublicKey(ownerAddress);

  // Get compression signatures for token owner
  const signatures = await connection.getCompressionSignaturesForTokenOwner(owner);

  if (signatures.items.length === 0) {
    return {
      count: 0,
      transactions: [],
    };
  }

  // Limit results
  const limitedSignatures = signatures.items.slice(0, limit);

  // Get detailed info if requested
  if (includeDetails && limitedSignatures.length > 0) {
    const transactions = await Promise.all(
      limitedSignatures.map(async (sig) => {
        const txInfo = await connection.getTransactionWithCompressionInfo(sig.signature);

        return {
          signature: sig.signature,
          slot: sig.slot,
          blockTime: sig.blockTime,
          timestamp: new Date(sig.blockTime * 1000).toISOString(),
          compressionInfo: txInfo?.compressionInfo ? {
            closedAccounts: txInfo.compressionInfo.closedAccounts.length,
            openedAccounts: txInfo.compressionInfo.openedAccounts.length,
          } : null,
        };
      })
    );

    return {
      count: signatures.items.length,
      transactions,
    };
  }

  // Return basic signature info
  const transactions = limitedSignatures.map((sig) => ({
    signature: sig.signature,
    slot: sig.slot,
    blockTime: sig.blockTime,
    timestamp: new Date(sig.blockTime * 1000).toISOString(),
  }));

  return {
    count: signatures.items.length,
    transactions,
  };
};

export default getTransactionHistory;