> ## Documentation Index
> Fetch the complete documentation index at: https://luminouslabs-cc5545c6-indexing-tokens.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Rent-Free SPL Accounts with Privy

> Integrate compressed tokens with Privy embedded wallets for rent-free SPL token accounts and transfers.

| Creation Cost   | SPL Token Account    | Compressed 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

<table>
  <thead>
    <tr>
      <th style={{ width: "20%" }} />

      <th>SPL</th>
      <th>Compressed </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>[**Transfer**](#full-code-examples)</td>
      <td>transferChecked()</td>
      <td>transfer()</td>
    </tr>

    <tr>
      <td>[**Compress**](#full-code-examples)</td>
      <td>N/A</td>
      <td>compress()</td>
    </tr>

    <tr>
      <td>[**Decompress**](#full-code-examples)</td>
      <td>N/A</td>
      <td>decompress()</td>
    </tr>

    <tr>
      <td>[**Get Balance**](#get-balances)</td>
      <td>getAccount()</td>
      <td>getCompressedTokenAccountsByOwner()</td>
    </tr>

    <tr>
      <td>[**Transaction History**](#get-transaction-history)</td>
      <td>getSignaturesForAddress()</td>
      <td>getCompressionSignaturesForOwner()</td>
    </tr>
  </tbody>
</table>

<Tabs>
  <Tab title="Guide">
    <Steps>
      <Step>
        ### Prerequisites

        ```bash theme={null}
        npm install @lightprotocol/compressed-token \
                    @lightprotocol/stateless.js
        ```

        Connect to an RPC endpoint that supports ZK Compression (Helius, Triton)

        ```typescript theme={null}
        import { createRpc } from "@lightprotocol/stateless.js";

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

        const rpc = createRpc(RPC_ENDPOINT);
        ```

        <Accordion title="Setup test mint">
          <Info>
            Before we call compress or decompresss, we need:

            * An SPL mint with a interface PDA for compression. This PDA can be created for new SPL mints via [`createMint()`](/compressed-tokens/guides/create-mint-with-token-pool) or added to existing SPL mints via [`createTokenPool()`](/compressed-tokens/guides/add-token-pools-to-mint-accounts).
            * For `compress()` SPL tokens in an Associated Token Account, or
            * For `decompress()` compressed token accounts with sufficient balance.
          </Info>

          ```typescript theme={null}
          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);
          })();
          ```
        </Accordion>
      </Step>

      <Step>
        ## Full Code Examples

        <Info>
          Find complete examples on Github: [nodejs](https://github.com/Lightprotocol/examples-zk-compression/tree/main/privy/nodejs-privy-compressed) and [react](https://github.com/Lightprotocol/examples-zk-compression/tree/main/privy/react-privy-compressed).
        </Info>

        <Tabs>
          <Tab title="Compressed Transfer">
            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

            <Tabs>
              <Tab title="Node.js">
                ````typescript theme={null}
                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;```
                ````
              </Tab>

              <Tab title="React">
                ```typescript theme={null}
                import { useState } from 'react';
                import { PublicKey, Transaction, ComputeBudgetProgram } from '@solana/web3.js';
                import { CompressedTokenProgram, selectMinCompressedTokenAccountsForTransfer } from '@lightprotocol/compressed-token';
                import { bn, createRpc } from '@lightprotocol/stateless.js';
                import type { ConnectedStandardSolanaWallet } from '@privy-io/js-sdk-core';
                import type { SignTransactionResult } from '@privy-io/react-auth/solana';

                export interface TransferParams {
                  ownerPublicKey: string;
                  mint: string;
                  toAddress: string;
                  amount: number;
                  decimals?: number;
                }

                export interface TransferArgs {
                  params: TransferParams;
                  wallet: ConnectedStandardSolanaWallet;
                  signTransaction: (args: {
                    transaction: Buffer;
                    wallet: ConnectedStandardSolanaWallet;
                    chain: string;
                  }) => Promise<SignTransactionResult>;
                }

                export function useTransfer() {
                  const [isLoading, setIsLoading] = useState(false);

                  const transfer = async (args: TransferArgs): Promise<string> => {
                    setIsLoading(true);

                    try {
                      const { params, wallet, signTransaction } = args;
                      const { ownerPublicKey, mint, toAddress, amount, decimals = 6 } = params;

                      // Create RPC connection
                      const rpc = createRpc(import.meta.env.VITE_HELIUS_RPC_URL);

                      const owner = new PublicKey(ownerPublicKey);
                      const mintPubkey = new PublicKey(mint);
                      const recipient = new PublicKey(toAddress);
                      const tokenAmount = bn(Math.floor(amount * Math.pow(10, decimals)));

                      // Get compressed token accounts
                      const accounts = await rpc.getCompressedTokenAccountsByOwner(owner, { mint: mintPubkey });
                      if (!accounts.items || accounts.items.length === 0) {
                        throw new Error('No compressed token accounts found');
                      }

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

                      // Get validity proof
                      const proof = await rpc.getValidityProof(
                        inputAccounts.map((account) => bn(account.compressedAccount.hash))
                      );

                      // Build transfer instruction
                      const transferIx = await CompressedTokenProgram.transfer({
                        payer: owner,
                        inputCompressedTokenAccounts: inputAccounts,
                        toAddress: recipient,
                        amount: tokenAmount,
                        recentInputStateRootIndices: proof.rootIndices,
                        recentValidityProof: proof.compressedProof,
                      });

                      // Build transaction
                      const { blockhash } = await rpc.getLatestBlockhash();
                      const transaction = new Transaction();
                      transaction.add(ComputeBudgetProgram.setComputeUnitLimit({ units: 300_000 }));
                      transaction.add(transferIx);
                      transaction.recentBlockhash = blockhash;
                      transaction.feePayer = owner;

                      // Serialize unsigned transaction
                      const unsignedTxBuffer = transaction.serialize({ requireAllSignatures: false });

                      // Sign with Privy
                      const signedTx = await signTransaction({
                        transaction: unsignedTxBuffer,
                        wallet,
                        chain: 'solana:devnet',
                      });

                      // Send transaction
                      const signedTxBuffer = Buffer.from(signedTx.signedTransaction);
                      const signature = await rpc.sendRawTransaction(signedTxBuffer, {
                        skipPreflight: false,
                        preflightCommitment: 'confirmed',
                      });

                      return signature;
                    } finally {
                      setIsLoading(false);
                    }
                  };

                  return { transfer, isLoading };
                }
                ```
              </Tab>
            </Tabs>
          </Tab>

          <Tab title="Compress SPL to Recipient">
            Convert SPL to compressed tokens and send to a recipient in one instruction.

            <Tabs>
              <Tab title="Node.js">
                ```typescript theme={null}
                import 'dotenv/config';
                import {PrivyClient} from '@privy-io/node';
                import {createRpc, bn, selectStateTreeInfo} from '@lightprotocol/stateless.js';
                import {PublicKey, Transaction, ComputeBudgetProgram} from '@solana/web3.js';
                import {getAssociatedTokenAddressSync, getAccount} from '@solana/spl-token';
                import {CompressedTokenProgram, getTokenPoolInfos, selectTokenPoolInfo} from '@lightprotocol/compressed-token';

                const compressTokens = 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 source token account and verify balance
                  const ownerAta = getAssociatedTokenAddressSync(mintPubkey, fromPubkey);
                  const ataAccount = await getAccount(connection, ownerAta);
                  if (ataAccount.amount < BigInt(tokenAmount.toString())) {
                    throw new Error('Insufficient SPL balance');
                  }

                  // Get state tree to store compressed tokens
                  //  Get token pool info. Stores SPL tokens in interface PDA when compressed.
                  const stateTreeInfos = await connection.getStateTreeInfos();
                  const selectedTreeInfo = selectStateTreeInfo(stateTreeInfos);
                  const tokenPoolInfos = await getTokenPoolInfos(connection, mintPubkey);
                  const tokenPoolInfo = selectTokenPoolInfo(tokenPoolInfos);

                  // Create compress instruction
                  const instruction = await CompressedTokenProgram.compress({
                    payer: fromPubkey,
                    owner: fromPubkey,
                    source: ownerAta,
                    toAddress: toPubkey,
                    mint: mintPubkey,
                    amount: tokenAmount,
                    outputStateTreeInfo: selectedTreeInfo,
                    tokenPoolInfo,
                  });

                  // 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 compressTokens;
                ```
              </Tab>

              <Tab title="React">
                ```typescript theme={null}
                import { useState } from 'react';
                import { PublicKey, Transaction, ComputeBudgetProgram } from '@solana/web3.js';
                import { getAssociatedTokenAddressSync, getAccount } from '@solana/spl-token';
                import { bn, createRpc, selectStateTreeInfo } from '@lightprotocol/stateless.js';
                import { CompressedTokenProgram, getTokenPoolInfos, selectTokenPoolInfo } from '@lightprotocol/compressed-token';
                import type { ConnectedStandardSolanaWallet } from '@privy-io/js-sdk-core';
                import type { SignTransactionResult } from '@privy-io/react-auth/solana';

                export interface CompressParams {
                  ownerPublicKey: string;
                  toAddress: string;
                  mint: string;
                  amount: number;
                  decimals?: number;
                }

                export interface CompressArgs {
                  params: CompressParams;
                  wallet: ConnectedStandardSolanaWallet;
                  signTransaction: (args: {
                    transaction: Buffer;
                    wallet: ConnectedStandardSolanaWallet;
                    chain: string;
                  }) => Promise<SignTransactionResult>;
                }

                export function useCompress() {
                  const [isLoading, setIsLoading] = useState(false);

                  const compress = async (args: CompressArgs): Promise<string> => {
                    setIsLoading(true);

                    try {
                      const { params, wallet, signTransaction } = args;
                      const { ownerPublicKey, toAddress, mint, amount, decimals = 6 } = params;

                      // Create RPC connection
                      const rpc = createRpc(import.meta.env.VITE_HELIUS_RPC_URL);

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

                      // Get source token account and verify balance
                      const ownerAta = getAssociatedTokenAddressSync(mintPubkey, ownerPubkey);
                      const ataAccount = await getAccount(rpc, ownerAta);
                      if (ataAccount.amount < BigInt(tokenAmount.toString())) {
                        throw new Error('Insufficient SPL balance');
                      }

                      // Get state tree to store compressed tokens
                      // Get token pool info. Stores SPL tokens in interface PDA when compressed.
                      const stateTreeInfos = await rpc.getStateTreeInfos();
                      const selectedTreeInfo = selectStateTreeInfo(stateTreeInfos);
                      const tokenPoolInfos = await getTokenPoolInfos(rpc, mintPubkey);
                      const tokenPoolInfo = selectTokenPoolInfo(tokenPoolInfos);

                      // Create compress instruction
                      const instruction = await CompressedTokenProgram.compress({
                        payer: ownerPubkey,
                        owner: ownerPubkey,
                        source: ownerAta,
                        toAddress: toPubkey,
                        mint: mintPubkey,
                        amount: tokenAmount,
                        outputStateTreeInfo: selectedTreeInfo,
                        tokenPoolInfo,
                      });

                      // Build transaction
                      const { blockhash } = await rpc.getLatestBlockhash();
                      const transaction = new Transaction();
                      transaction.add(ComputeBudgetProgram.setComputeUnitLimit({ units: 300_000 }));
                      transaction.add(instruction);
                      transaction.recentBlockhash = blockhash;
                      transaction.feePayer = ownerPubkey;

                      // Serialize unsigned transaction
                      const unsignedTxBuffer = transaction.serialize({ requireAllSignatures: false });

                      // Sign with Privy
                      const signedTx = await signTransaction({
                        transaction: unsignedTxBuffer,
                        wallet,
                        chain: 'solana:devnet',
                      });

                      // Send transaction
                      const signedTxBuffer = Buffer.from(signedTx.signedTransaction);
                      const signature = await rpc.sendRawTransaction(signedTxBuffer, {
                        skipPreflight: false,
                        preflightCommitment: 'confirmed',
                      });

                      return signature;
                    } finally {
                      setIsLoading(false);
                    }
                  };

                  return { compress, isLoading };
                }
                ```
              </Tab>
            </Tabs>
          </Tab>

          <Tab title="Decompress (Offramp)">
            Convert compressed tokens back to SPL tokens.

            <Tabs>
              <Tab title="Node.js">
                ```typescript theme={null}
                import 'dotenv/config';
                import {PrivyClient} from '@privy-io/node';
                import {createRpc} from '@lightprotocol/stateless.js';
                import {Keypair, PublicKey, Transaction} from '@solana/web3.js';
                import {getAssociatedTokenAddressSync, createAssociatedTokenAccount} from '@solana/spl-token';
                import {decompress} from '@lightprotocol/compressed-token';

                const decompressTokens = async (
                  fromAddress: 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!,
                  });

                  const fromPubkey = new PublicKey(fromAddress);
                  const mintPubkey = new PublicKey(tokenMintAddress);
                  const rawAmount = amount * Math.pow(10, decimals);

                  // Get destination ATA
                  const ownerAta = getAssociatedTokenAddressSync(mintPubkey, fromPubkey);

                  // Check ATA exists (decompress action will handle creation internally)
                  // But we need to be aware ATA creation requires a separate signer

                  // Create fake keypair for decompress action (only publicKey is used)
                  const dummyPayer = {
                    publicKey: fromPubkey,
                    secretKey: new Uint8Array(64),
                  } as any;

                  // Intercept sendAndConfirmTransaction to use Privy signing
                  const originalSendAndConfirm = (connection as any).sendAndConfirmTransaction;
                  (connection as any).sendAndConfirmTransaction = async (tx: Transaction, signers: any[]) => {
                    const signResult = await privy.wallets().solana().signTransaction(process.env.TREASURY_WALLET_ID!, {
                      transaction: tx.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');
                    }

                    const signedTransaction = Buffer.from(signedTx, 'base64');
                    const signature = await connection.sendRawTransaction(signedTransaction, {
                      skipPreflight: false,
                      preflightCommitment: 'confirmed'
                    });
                    await connection.confirmTransaction(signature, 'confirmed');
                    return signature;
                  };

                  try {
                    // Use high-level decompress action (handles account configuration correctly)
                    const signature = await decompress(
                      connection,
                      dummyPayer,
                      mintPubkey,
                      rawAmount,
                      dummyPayer,
                      ownerAta
                    );

                    return signature;
                  } finally {
                    // Restore original function
                    (connection as any).sendAndConfirmTransaction = originalSendAndConfirm;
                  }
                };

                export default decompressTokens;
                ```
              </Tab>

              <Tab title="React">
                ```typescript theme={null}
                import { useState } from 'react';
                import { PublicKey, Transaction, ComputeBudgetProgram } from '@solana/web3.js';
                import { getAssociatedTokenAddressSync, createAssociatedTokenAccountInstruction, getAccount, TokenAccountNotFoundError } from '@solana/spl-token';
                import { CompressedTokenProgram, selectMinCompressedTokenAccountsForTransfer, getSplInterfaceInfos, selectSplInterfaceInfosForDecompression } from '@lightprotocol/compressed-token';
                import { bn, createRpc } from '@lightprotocol/stateless.js';
                import type { ConnectedStandardSolanaWallet } from '@privy-io/js-sdk-core';
                import type { SignTransactionResult } from '@privy-io/react-auth/solana';

                export interface DecompressParams {
                  ownerPublicKey: string;
                  mint: string;
                  amount: number;
                  decimals?: number;
                }

                export interface DecompressArgs {
                  params: DecompressParams;
                  wallet: ConnectedStandardSolanaWallet;
                  signTransaction: (args: {
                    transaction: Buffer;
                    wallet: ConnectedStandardSolanaWallet;
                    chain: string;
                  }) => Promise<SignTransactionResult>;
                }

                export function useDecompress() {
                  const [isLoading, setIsLoading] = useState(false);

                  const decompress = async (args: DecompressArgs): Promise<string> => {
                    setIsLoading(true);

                    try {
                      const { params, wallet, signTransaction } = args;
                      const { ownerPublicKey, mint, amount, decimals = 6 } = params;

                      // Create RPC connection
                      const rpc = createRpc(import.meta.env.VITE_HELIUS_RPC_URL);

                      const owner = new PublicKey(ownerPublicKey);
                      const mintPubkey = new PublicKey(mint);
                      const tokenAmount = bn(Math.floor(amount * Math.pow(10, decimals)));

                      // Get destination ATA
                      const destinationAta = getAssociatedTokenAddressSync(mintPubkey, owner);

                      // Get compressed token accounts
                      const accounts = await rpc.getCompressedTokenAccountsByOwner(owner, { mint: mintPubkey });
                      if (!accounts.items || accounts.items.length === 0) {
                        throw new Error('No compressed token accounts found');
                      }

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

                      // Get validity proof and token pool info
                      const proof = await rpc.getValidityProof(
                        inputAccounts.map((account) => bn(account.compressedAccount.hash))
                      );
                      const splInterfaceInfos = await getSplInterfaceInfos(rpc, mintPubkey);
                      const tokenPoolInfos = selectSplInterfaceInfosForDecompression(splInterfaceInfos, tokenAmount);

                      // Build transaction
                      const { blockhash } = await rpc.getLatestBlockhash();
                      const transaction = new Transaction();
                      transaction.add(ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 }));

                      // Create ATA if needed
                      try {
                        await getAccount(rpc, destinationAta);
                      } catch (e) {
                        if (e instanceof TokenAccountNotFoundError) {
                          transaction.add(
                            createAssociatedTokenAccountInstruction(owner, destinationAta, owner, mintPubkey)
                          );
                        } else {
                          throw e;
                        }
                      }

                      // Build decompress instruction
                      const decompressIx = await CompressedTokenProgram.decompress({
                        payer: owner,
                        inputCompressedTokenAccounts: inputAccounts,
                        toAddress: destinationAta,
                        amount: tokenAmount,
                        recentInputStateRootIndices: proof.rootIndices,
                        recentValidityProof: proof.compressedProof,
                        tokenPoolInfos,
                      });

                      transaction.add(decompressIx);
                      transaction.recentBlockhash = blockhash;
                      transaction.feePayer = owner;

                      // Serialize unsigned transaction
                      const unsignedTxBuffer = transaction.serialize({ requireAllSignatures: false });

                      // Sign with Privy
                      const signedTx = await signTransaction({
                        transaction: unsignedTxBuffer,
                        wallet,
                        chain: 'solana:devnet',
                      });

                      // Send transaction
                      const signedTxBuffer = Buffer.from(signedTx.signedTransaction);
                      const signature = await rpc.sendRawTransaction(signedTxBuffer, {
                        skipPreflight: false,
                        preflightCommitment: 'confirmed',
                      });

                      return signature;
                    } finally {
                      setIsLoading(false);
                    }
                  };

                  return { decompress, isLoading };
                }
                ```
              </Tab>
            </Tabs>
          </Tab>
        </Tabs>
      </Step>
    </Steps>

    ## Get balances

    Fetch SPL and compressed token balances.

    <Tabs>
      <Tab title="Node.js">
        ```typescript theme={null}
        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 SOL balance
          const compressedSol = await rpc.getCompressedBalanceByOwner(owner);
          const compressedSolLamports = compressedSol.value ? BigInt(compressedSol.value.toString()) : 0n;

          // 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 {
            sol: compressedSolLamports.toString(),
            tokens: Array.from(balances.entries()).map(([mint, amount]) => ({
              mint,
              amount: amount.toString(),
              accounts: validItems.filter(a => a.parsed?.mint.toBase58() === mint).length
            }))
          };
        }
        ```
      </Tab>

      <Tab title="React">
        ```typescript theme={null}
        import { useState, useCallback } from 'react';
        import { PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js';
        import { getAccount, getAssociatedTokenAddressSync } from '@solana/spl-token';
        import { createRpc } from '@lightprotocol/stateless.js';

        const USDC_MINT = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
        const TEST_MINT = '7cT3PeXyDLkyEcvr9YzsjGLuZneKsea4c8hPbJQEjMCZ';

        export interface TokenBalance {
          mint: string;
          amount: string;
          decimals: number;
          isCompressed: boolean;
          isNative: boolean;
        }

        export function useCompressedBalances() {
          const [balances, setBalances] = useState<TokenBalance[]>([]);
          const [isLoading, setIsLoading] = useState(false);

          const fetchBalances = useCallback(async (ownerAddress: string) => {
            if (!ownerAddress) return;

            setIsLoading(true);
            try {
              const rpc = createRpc(import.meta.env.VITE_HELIUS_RPC_URL);
              const owner = new PublicKey(ownerAddress);
              const allBalances: TokenBalance[] = [];

              // Get compressed SOL balance
              try {
                const compressedSol = await rpc.getCompressedBalanceByOwner(owner);
                if (compressedSol && BigInt(compressedSol.toString()) > 0n) {
                  allBalances.push({
                    mint: 'So11111111111111111111111111111111111111112',
                    amount: compressedSol.toString(),
                    decimals: 9,
                    isCompressed: true,
                    isNative: true,
                  });
                }
              } catch {
                // No compressed SOL
              }

              // Get regular SOL balance
              try {
                const solBalance = await rpc.getBalance(owner);
                allBalances.push({
                  mint: 'So11111111111111111111111111111111111111112',
                  amount: solBalance.toString(),
                  decimals: 9,
                  isCompressed: false,
                  isNative: true,
                });
              } catch {
                // Failed to get SOL balance
              }

              // Get regular SPL token accounts
              try {
                const tokenAccounts = await rpc.getTokenAccountsByOwner(owner, {
                  programId: new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'),
                });

                for (const { account, pubkey } of tokenAccounts.value) {
                  const data = account.data;
                  if (data instanceof Buffer || (typeof data === 'object' && 'length' in data)) {
                    // Parse token account data
                    const dataBuffer = Buffer.from(data as Uint8Array);
                    if (dataBuffer.length >= 72) {
                      const mint = new PublicKey(dataBuffer.subarray(0, 32));
                      const amount = dataBuffer.readBigUInt64LE(64);

                      // Get mint info for decimals
                      try {
                        const mintInfo = await rpc.getAccountInfo(mint);
                        const decimals = mintInfo?.data ? (mintInfo.data as Buffer)[44] || 6 : 6;

                        allBalances.push({
                          mint: mint.toBase58(),
                          amount: amount.toString(),
                          decimals,
                          isCompressed: false,
                          isNative: false,
                        });
                      } catch {
                        allBalances.push({
                          mint: mint.toBase58(),
                          amount: amount.toString(),
                          decimals: 6,
                          isCompressed: false,
                          isNative: false,
                        });
                      }
                    }
                  }
                }
              } catch {
                // Failed to get token accounts
              }

              // Get compressed token accounts
              try {
                const compressedAccounts = await rpc.getCompressedTokenAccountsByOwner(owner);

                if (compressedAccounts?.items) {
                  const mintAmounts = new Map<string, bigint>();
                  const mintDecimals = new Map<string, number>();

                  for (const item of compressedAccounts.items) {
                    if (item?.parsed?.mint && item?.parsed?.amount) {
                      const mint = item.parsed.mint.toBase58();
                      const amount = BigInt(item.parsed.amount.toString());
                      const current = mintAmounts.get(mint) || 0n;
                      mintAmounts.set(mint, current + amount);

                      // Store decimals (assume 6 for now, could fetch from mint)
                      if (!mintDecimals.has(mint)) {
                        mintDecimals.set(mint, 6);
                      }
                    }
                  }

                  for (const [mint, amount] of mintAmounts) {
                    allBalances.push({
                      mint,
                      amount: amount.toString(),
                      decimals: mintDecimals.get(mint) || 6,
                      isCompressed: true,
                      isNative: false,
                    });
                  }
                }
              } catch {
                // Failed to get compressed accounts
              }

              // Add fallback tokens with 0 balance if not present
              const hasMint = (mint: string) => allBalances.some(b => b.mint === mint);

              if (!hasMint(USDC_MINT)) {
                allBalances.push({
                  mint: USDC_MINT,
                  amount: '0',
                  decimals: 6,
                  isCompressed: false,
                  isNative: false,
                });
              }

              if (!hasMint(TEST_MINT)) {
                allBalances.push({
                  mint: TEST_MINT,
                  amount: '0',
                  decimals: 6,
                  isCompressed: false,
                  isNative: false,
                });
              }

              setBalances(allBalances);
            } catch (error) {
              console.error('Failed to fetch balances:', error);
              setBalances([]);
            } finally {
              setIsLoading(false);
            }
          }, []);

          return { balances, isLoading, fetchBalances };
        }
        ```
      </Tab>
    </Tabs>

    ## Get transaction history

    Fetch compressed token transaction history for an owner, with optional detailed compression information.

    <Tabs>
      <Tab title="Node.js">
        ```typescript theme={null}
        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;
        ```
      </Tab>

      <Tab title="React">
        ```typescript theme={null}
        import { useState, useCallback } from 'react';
        import { PublicKey } from '@solana/web3.js';
        import { createRpc } from '@lightprotocol/stateless.js';

        export interface TransactionCompressionInfo {
          closedAccounts: number;
          openedAccounts: number;
        }

        export interface Transaction {
          signature: string;
          slot: number;
          blockTime: number;
          timestamp: string;
          compressionInfo?: TransactionCompressionInfo | null;
        }

        export function useTransactionHistory() {
          const [transactions, setTransactions] = useState<Transaction[]>([]);
          const [isLoading, setIsLoading] = useState(false);
          const [error, setError] = useState<string | null>(null);

          const fetchTransactionHistory = useCallback(
            async (
              ownerAddress: string,
              limit: number = 10,
              includeDetails: boolean = false
            ) => {
              if (!ownerAddress) {
                setTransactions([]);
                return;
              }

              setIsLoading(true);
              setError(null);

              try {
                const rpc = createRpc(import.meta.env.VITE_HELIUS_RPC_URL);
                const owner = new PublicKey(ownerAddress);

                const signatures = await rpc.getCompressionSignaturesForTokenOwner(owner);

                if (signatures.items.length === 0) {
                  setTransactions([]);
                  return;
                }

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

                if (includeDetails && limitedSignatures.length > 0) {
                  const transactionsWithDetails = await Promise.all(
                    limitedSignatures.map(async (sig) => {
                      const txInfo = await rpc.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,
                      };
                    })
                  );

                  setTransactions(transactionsWithDetails);
                  return;
                }

                const basicTransactions = limitedSignatures.map((sig) => ({
                  signature: sig.signature,
                  slot: sig.slot,
                  blockTime: sig.blockTime,
                  timestamp: new Date(sig.blockTime * 1000).toISOString(),
                }));

                setTransactions(basicTransactions);
              } catch (err) {
                const message = err instanceof Error ? err.message : String(err);
                setError(message);
                setTransactions([]);
              } finally {
                setIsLoading(false);
              }
            },
            []
          );

          return { transactions, isLoading, error, fetchTransactionHistory };
        }
        ```
      </Tab>
    </Tabs>
  </Tab>

  <Tab title="AI Prompt">
    <Tabs>
      <Tab title="Node.js">
        ````text theme={null}
        ---
        argument-hint: <project_path>
        description: Add compressed token support to Privy Node.js app
        allowed-tools: [Bash, Read, Glob, Grep, Task, mcp__deepwiki, mcp__zkcompression]
        ---

        ## Task: Add compressed token support to Privy Node.js app

        References:
        - Privy Docs: https://docs.privy.io/recipes/solana/send-spl-tokens
        - Privy Node Examples:
          - SPL: https://github.com/privy-io/examples/tree/main/privy-node-starter
          - Compressed: https://github.com/Lightprotocol/examples-zk-compression/tree/main/privy/nodejs-privy-compressed
        - SPL vs Compressed comparison: https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/COMPARISON.md
        - Compressed Token Guide for Privy Node.js: https://zkcompression.com/compressed-tokens/for-privy 

        MCP:
        * deepwiki https://mcp.deepwiki.com/mcp
        * ZK Compression https://www.zkcompression.com/mcp

        ## Workflow

        - This plan must execute without user intervention
        - All questions have been resolved in planning phase
        - If blocked, find alternative approach - do not stop
        - Keep working until ALL todos are complete
        - Use Task tool with subagents for parallel research or when stuck
        - Use subagents with Read, Glob, Grep, and Deepwiki permissions when stuck
        - Always assign Tasks to subagents and tell the user

        ## DeepWiki fallback

        ```
        mcp__deepwiki__read_wiki_structure("Lightprotocol/light-protocol"),
        mcp__deepwiki__read_wiki_contents("Lightprotocol/light-protocol"),
        mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "<question>")
        ```

        ## Quick Reference

        | Operation      | SPL                            | Compressed                                     |
        | -------------- | ------------------------------ | ---------------------------------------------- |
        | Transfer       | transferChecked()              | transfer()                                     |
        | Compress SPL to recipient       | N/A                            | compress()                                     |
        | Decompress to SPL    | N/A                            | decompress()                                   |
        | Get Balance    | getAccount()                   | getCompressedTokenAccountsByOwner()            |
        | Tx History     | getSignaturesForAddress()      | getCompressionSignaturesForOwner()             |

        ### Phase 1: Index project

        ```bash
        grep -r "createTransferInstruction\|getAssociatedTokenAddress\|@solana/spl-token" src/
        ```

        ### Phase 2: Add dependencies

        ```bash
        npm install @lightprotocol/stateless.js @lightprotocol/compressed-token
        ```

        ### Phase 3: Implement operations

        See Code Reference below all signed with Privy SDK.

        1. **Setup**: Connect to ZK Compression RPC (Helius, Triton) https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/nodejs-privy-compressed/src/index.ts
        2. **Get Balance**: Fetch SPL and compressed token balances https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/nodejs-privy-compressed/src/compressed/balances.ts
        3. **Transfer**: Send compressed tokens to another recipient, signed with Privy SDK https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/nodejs-privy-compressed/src/compressed/transfer.ts
        4. **Compress**: Convert SPL to compressed tokens and send to a recipient in one instruction https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/nodejs-privy-compressed/src/compressed/compress.ts
        5. **Decompress**: Convert compressed tokens back to SPL tokens for offramps https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/nodejs-privy-compressed/src/compressed/decompress.ts
        6. **Tx History**: Fetch compressed token transaction history for an owner

        ### Phase 4: Test

        Run the implemented functions. On failure, debug and retry. Assign always to subagents with Tasktool

        ````
      </Tab>

      <Tab title="React">
        ````text theme={null}
        ---
        argument-hint: <project_path>
        description: Add compressed token support to Privy React app
        allowed-tools: [Bash, Read, Glob, Grep, Task, mcp__deepwiki, mcp__zkcompression]
        ---

        ## Task: Add compressed token support to Privy React app

        References:
        - Privy Docs: https://docs.privy.io/recipes/solana/send-spl-tokens
        - Privy React Examples:
          - SPL: https://github.com/privy-io/examples/tree/main/privy-react-starter
          - Compressed: https://github.com/Lightprotocol/examples-zk-compression/tree/main/privy/react-privy-compressed
        - SPL vs Compressed comparison: https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/COMPARISON.md
        - Compressed Token Guide for Privy React: https://zkcompression.com/compressed-tokens/for-privy

        MCP:
        * deepwiki https://mcp.deepwiki.com/mcp
        * ZK Compression https://www.zkcompression.com/mcp

        ## Workflow

        - This plan must execute without user intervention
        - All questions have been resolved in planning phase
        - If blocked, find alternative approach - do not stop
        - Keep working until ALL todos are complete
        - Use Task tool with subagents for parallel research or when stuck
        - Use subagents with Read, Glob, Grep, and Deepwiki permissions when stuck
        - Always assign Tasks to subagents and tell the user

        ## DeepWiki fallback

        ```
        mcp__deepwiki__read_wiki_structure("Lightprotocol/light-protocol"),
        mcp__deepwiki__read_wiki_contents("Lightprotocol/light-protocol"),
        mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "<question>")
        ```

        ## Quick Reference

        | Operation      | SPL                            | Compressed                                     |
        | -------------- | ------------------------------ | ---------------------------------------------- |
        | Transfer       | transferChecked()              | transfer()                                     |
        | Compress SPL to recipient       | N/A                            | compress()                                     |
        | Decompress to SPL    | N/A                            | decompress()                                   |
        | Get Balance    | getAccount()                   | getCompressedTokenAccountsByOwner()            |
        | Tx History     | getSignaturesForAddress()      | getCompressionSignaturesForOwner()             |

        ### Phase 1: Index project

        ```bash
        grep -r "createTransferInstruction\|getAssociatedTokenAddress\|@solana/spl-token" src/
        ```

        ### Phase 2: Add dependencies

        ```bash
        npm install @lightprotocol/stateless.js @lightprotocol/compressed-token
        ```

        ### Phase 3: Implement operations

        See Code Reference below all signed with Privy SDK.

        1. **Setup**: Connect to ZK Compression RPC (Helius, Triton) https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/react-privy-compressed/src/config/rpc.ts
        2. **Get Balance**: Fetch SPL and compressed token balances https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/react-privy-compressed/src/hooks/useCompressedBalances.ts
        3. **Transfer**: Send compressed tokens to another recipient, signed with Privy SDK https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/react-privy-compressed/src/hooks/useTransfer.ts
        4. **Compress**: Convert SPL to compressed tokens and send to a recipient in one instruction https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/react-privy-compressed/src/hooks/useCompress.ts
        5. **Decompress**: Convert compressed tokens back to SPL tokens for offramps https://github.com/Lightprotocol/examples-zk-compression/blob/main/privy/react-privy-compressed/src/hooks/useDecompress.ts
        6. **Tx History**: Fetch compressed token transaction history for an owner

        ### Phase 4: Test

        Run the implemented functions. On failure, debug and retry. Assign always to subagents with Tasktool

        ````
      </Tab>
    </Tabs>
  </Tab>
</Tabs>
