> ## Documentation Index
> Fetch the complete documentation index at: https://www.zkcompression.com/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>**Get Balance**</td>
      <td>getAccount()</td>
      <td>getCompressedTokenAccountsByOwner()</td>
    </tr>

    <tr>
      <td>**Transfer**</td>
      <td>transferChecked()</td>
      <td>transfer()</td>
    </tr>

    <tr>
      <td>**Compress**</td>
      <td>N/A</td>
      <td>compress()</td>
    </tr>

    <tr>
      <td>**Decompress**</td>
      <td>N/A</td>
      <td>decompress()</td>
    </tr>

    <tr>
      <td>**Transaction History**</td>
      <td>getSignaturesForAddress()</td>
      <td>getCompressionSignaturesForOwner()</td>
    </tr>
  </tbody>
</table>

<Steps>
  <Step>
    ### Prerequisites

    <Tabs>
      <Tab title="compressed-token">
        ```bash theme={null}
        npm install @lightprotocol/compressed-token@^0.23.0 \
                    @lightprotocol/stateless.js@^0.23.0
        ```
      </Tab>

      <Tab title="SDK 2.0 (token-interface)">
        ```bash theme={null}
        npm install @lightprotocol/token-interface@^0.1.2 \
                    @lightprotocol/stateless.js@^0.23.0
        ```
      </Tab>
    </Tabs>

    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 can compress or decompresss, we need:

        * An SPL mint with an interface PDA for compression. This interface 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 a complete example on Github: [privy/nodejs-privy-compressed](https://github.com/Lightprotocol/examples-zk-compression/tree/main/privy).
    </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>
        </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>
        </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>
        </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>
</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>
</Tabs>
