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 e.g. 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@^0.23.0 \
            @lightprotocol/compressed-token@^0.23.0
Install the CLI globally:
npm install -g @lightprotocol/zk-compression-cli
# 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.
SDK 2.0 (@lightprotocol/token-interface) is the latest JavaScript SDK. It has better API ergonomics, guarantees single-instruction loading, and is compatible with KIT (the new Solana SDKs).
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.

Verify payments

Query balances and transaction history.

Didn’t find what you were looking for?

Reach out! Telegram | email | Discord