Skip to main content
  1. The light-token API matches the SPL-token API almost entirely, and extends their functionality to include the light token program in addition to the SPL-token and Token-2022 programs.
  2. Your users use the same stablecoins, just stored more efficiently.
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.
Snippets below assume rpc, payer, mint, owner, recipient, and amount are defined. See the full examples for runnable setup.
import { createRpc } from "@lightprotocol/stateless.js";

import {
  createTransferInterfaceInstructions,
  transferInterface
} from "@lightprotocol/compressed-token/unified";

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 runnable code examples: instruction | action.
2

Send a Payment

Use createTransferInterfaceInstructions to transfer tokens between wallets. The method:
  • automatically derives Associated Token Accounts for the sender and recipient. If the recipient’s ATA doesn’t exist, the instruction to create the account is automatically added to the same transaction.
  • 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 } from "@lightprotocol/compressed-token/unified";
import { rpc, payer, setup } from "../setup.js";

(async function () {
    const { mint } = await setup();
    const recipient = Keypair.generate();

    // Returns TransactionInstruction[][]. Each inner array is one txn.
    const instructions = await createTransferInterfaceInstructions(
        rpc,
        payer.publicKey,
        mint,
        100,
        payer.publicKey,
        recipient.publicKey
    );

    for (const ixs of instructions) {
        const tx = new Transaction().add(...ixs);
        const sig = await sendAndConfirmTransaction(rpc, tx, [payer]);
        console.log("Tx:", sig);
    }
})();
import {
  getAssociatedTokenAddressSync,
  createAssociatedTokenAccountIdempotentInstruction,
  createTransferInstruction,
} from "@solana/spl-token";

const sourceAta = getAssociatedTokenAddressSync(mint, owner.publicKey);
const destinationAta = getAssociatedTokenAddressSync(mint, recipient);

const tx = new Transaction().add(
  createAssociatedTokenAccountIdempotentInstruction(
    payer.publicKey, destinationAta, recipient, mint
  ),
  createTransferInstruction(sourceAta, destinationAta, owner.publicKey, amount)
);
Your app logic may require you to create a single sign request for your user:
const transactions = instructions.map((ixs) => new Transaction().add(...ixs));

// One approval for all
const signed = await wallet.signAllTransactions(transactions);

for (const tx of signed) {
  await sendAndConfirmTransaction(rpc, tx);
}
While almost always you will have only one transfer transaction, you can optimize sending in the rare cases where you have multiple transactions. Parallelize the loads, confirm them, and then send the transfer instruction after.
import {
  createTransferInterfaceInstructions,
  sliceLast,
} from "@lightprotocol/compressed-token/unified";

const instructions = await createTransferInterfaceInstructions(
  rpc,
  payer.publicKey,
  mint,
  amount,
  owner.publicKey,
  recipient
);
const { rest: loadInstructions, last: transferInstructions } = sliceLast(instructions);
// empty = nothing to load, will no-op.
await Promise.all(
  loadInstructions.map((ixs) => {
    const tx = new Transaction().add(...ixs);
    tx.sign(payer, owner);
    return sendAndConfirmTransaction(rpc, tx);
  })
);

const transferTx = new Transaction().add(...transferInstructions);
transferTx.sign(payer, owner);
await sendAndConfirmTransaction(rpc, transferTx);

Batch payments

Pack multiple transfers to multiple recipients into one transaction.

Gasless transactions

Abstract SOL fees from the user. Sponsor top-ups and transaction fees.

Receive payments

Unify token balances and share ATA address with the sender.

Didn’t find what you were looking for?

Reach out! Telegram | email | Discord