Skip to main content

A Solana transaction can contain one or more instructions that are executed sequentially and atomically by the network. This way, you can pack multiple transfers into a single transaction instead of sending three separate transactions. You send one transaction with three transfer instructions. Use just like with SPL token:
SPLLight
TransfercreateTransferInstruction()createTransferInterfaceInstructions()
Use the payments agent skill to add light-token payment support to your project:
npx skills add Lightprotocol/skills
For orchestration, install the general skill:
npx skills add https://zkcompression.com
1

Setup

Install packages in your working directory:
npm install @lightprotocol/stateless.js@beta \
            @lightprotocol/compressed-token@beta
Install the CLI globally:
npm install -g @lightprotocol/zk-compression-cli@beta
# start local test-validator in a separate terminal
light test-validator
In the code examples, use createRpc() without arguments for localnet.
import { createRpc } from "@lightprotocol/stateless.js";

const rpc = createRpc(RPC_ENDPOINT);
setup.ts
import "dotenv/config";
import { Keypair } from "@solana/web3.js";
import { createRpc } from "@lightprotocol/stateless.js";
import {
    createMintInterface,
    createAtaInterface,
    getAssociatedTokenAddressInterface,
} from "@lightprotocol/compressed-token";
import { wrap } from "@lightprotocol/compressed-token/unified";
import {
    TOKEN_PROGRAM_ID,
    createAssociatedTokenAccount,
    mintTo,
} from "@solana/spl-token";
import { homedir } from "os";
import { readFileSync } from "fs";

// devnet:
// const RPC_URL = `https://devnet.helius-rpc.com?api-key=${process.env.API_KEY!}`;
// export const rpc = createRpc(RPC_URL);
// localnet:
export const rpc = createRpc();

export const payer = Keypair.fromSecretKey(
    new Uint8Array(
        JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8"))
    )
);

/** Create SPL mint, fund payer, wrap into light-token ATA. */
export async function setup(amount = 1_000_000_000) {
    const { mint } = await createMintInterface(
        rpc,
        payer,
        payer,
        null,
        9,
        undefined,
        undefined,
        TOKEN_PROGRAM_ID
    );

    const splAta = await createAssociatedTokenAccount(
        rpc,
        payer,
        mint,
        payer.publicKey,
        undefined,
        TOKEN_PROGRAM_ID
    );
    await mintTo(rpc, payer, mint, splAta, payer, amount);

    await createAtaInterface(rpc, payer, mint, payer.publicKey);
    const senderAta = getAssociatedTokenAddressInterface(mint, payer.publicKey);
    await wrap(rpc, payer, splAta, senderAta, payer, mint, BigInt(amount));

    return { mint, senderAta, splAta };
}
Find full code examples: batch transfer.
2

Send to Multiple Recipients in One Transaction

Batching multiple transfers requires building each instruction separately, then combining them into a single transaction.The method createTransferInterfaceInstructions determines whether the sender’s account has a cold balance and adds load instructions if needed.
About loading: Light Token accounts reduce account rent ~200x by auto-compressing inactive accounts. Before any action, the SDK detects cold balances and adds instructions to load them. This almost always fits in a single atomic transaction with your regular transfer. APIs return TransactionInstruction[][] so the same loop handles the rare multi-transaction case automatically.
import {
    Keypair,
    Transaction,
    sendAndConfirmTransaction,
} from "@solana/web3.js";
import {
    createTransferInterfaceInstructions,
    getAtaInterface,
    getAssociatedTokenAddressInterface,
} from "@lightprotocol/compressed-token/unified";
import { rpc, payer, setup } from "../setup.js";

(async function () {
    const { mint } = await setup();

    const recipients = [
        { address: Keypair.generate().publicKey, amount: 100 },
        { address: Keypair.generate().publicKey, amount: 200 },
        { address: Keypair.generate().publicKey, amount: 300 },
    ];

    // Build transfer instructions for each recipient
    const COMPUTE_BUDGET = "ComputeBudget111111111111111111111111111111";
    const allTransferIxs = [];
    let hasComputeBudget = false;

    for (const { address, amount } of recipients) {
        const instructions = await createTransferInterfaceInstructions(
            rpc,
            payer.publicKey,
            mint,
            amount,
            payer.publicKey,
            address
        );
        
        for (const ix of instructions[instructions.length - 1]) {
            // Deduplicate ComputeBudget instructions
            if (ix.programId.toBase58() === COMPUTE_BUDGET) {
                if (hasComputeBudget) continue;
                hasComputeBudget = true;
            }
            allTransferIxs.push(ix);
        }
    }

    // Send all transfers in a single transaction
    const tx = new Transaction().add(...allTransferIxs);
    const sig = await sendAndConfirmTransaction(rpc, tx, [payer]);
    console.log("Batch tx:", sig);

    // Verify balances
    for (const { address, amount } of recipients) {
        const ata = getAssociatedTokenAddressInterface(mint, address);
        const { parsed } = await getAtaInterface(rpc, ata, address, mint);
        console.log(
            `  ${address.toBase58()}: ${parsed.amount} (expected ${amount})`
        );
    }
})();
import { findAssociatedTokenPda, TOKEN_2022_PROGRAM_ADDRESS } from "@solana-program/token";
import { getTransferInstruction } from "@solana-program/token";

const [senderAta] = await findAssociatedTokenPda({
  mint: mint.address,
  owner: sender.address,
  tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});

const [recipient1Ata] = await findAssociatedTokenPda({
  mint: mint.address,
  owner: recipient1.address,
  tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});

const [recipient2Ata] = await findAssociatedTokenPda({
  mint: mint.address,
  owner: recipient2.address,
  tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});

const transfer1Instruction = getTransferInstruction({
  source: senderAta,
  destination: recipient1Ata,
  authority: sender.address,
  amount: 250_000n
});

const transfer2Instruction = getTransferInstruction({
  source: senderAta,
  destination: recipient2Ata,
  authority: sender.address,
  amount: 250_000n
});

const signature = await client.transaction.prepareAndSend({
  authority: sender,
  instructions: [transfer1Instruction, transfer2Instruction],
  version: 0
});

const splToken = client.splToken({
  mint: mint.address,
  tokenProgram: "auto"
});

const senderBalance = await splToken.fetchBalance(sender.address);
const recipient1Balance = await splToken.fetchBalance(recipient1.address);
const recipient2Balance = await splToken.fetchBalance(recipient2.address);

Basic payment

Send a single transfer.

Gasless transactions

Separate the fee payer from the token owner.

Receive payments

Load cold accounts and share ATA address with the sender.

Didn’t find what you were looking for?

Reach out! Telegram | email | Discord