Skip to main content
  1. Share the associated token account address with the sender.
  2. We recommend to prepend a load instruction before. Load creates the associated token account if needed and loads any cold balance into it.
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.
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

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 };
}

Derive the associated token account address

Share this address with the sender so they know where to send tokens.
import { getAssociatedTokenAddressInterface } from "@lightprotocol/compressed-token/unified";

const ata = getAssociatedTokenAddressInterface(mint, recipient);

Load the associated token account

Find a full code example here.
import {
    Keypair,
    Transaction,
    sendAndConfirmTransaction,
} from "@solana/web3.js";
import {
    transferInterface,
    getAssociatedTokenAddressInterface,
    createLoadAtaInstructions,
} from "@lightprotocol/compressed-token/unified";
import { rpc, payer, setup } from "../setup.js";

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

    // Transfer to a fresh recipient so they have cold tokens
    const recipient = Keypair.generate();
    await transferInterface(
        rpc,
        payer,
        senderAta,
        mint,
        recipient.publicKey,
        payer,
        500
    );

    // --- Receive: load creates ATA if needed + pulls cold state to hot ---
    const recipientAta = getAssociatedTokenAddressInterface(
        mint,
        recipient.publicKey
    );

    // Returns TransactionInstruction[][]. Each inner array is one txn.
    // Almost always one. Empty = noop.
    const instructions = await createLoadAtaInstructions(
        rpc,
        recipientAta,
        recipient.publicKey,
        mint,
        payer.publicKey
    );

    for (const ixs of instructions) {
        const tx = new Transaction().add(...ixs);
        await sendAndConfirmTransaction(rpc, tx, [payer]);
    }

    console.log("Recipient ATA:", recipientAta.toBase58());
})();
import {
  createAssociatedTokenAccountInstruction,
  getAssociatedTokenAddressSync,
} from "@solana/spl-token";

const ata = getAssociatedTokenAddressSync(mint, recipient);

const tx = new Transaction().add(
  createAssociatedTokenAccountInstruction(payer.publicKey, ata, recipient, mint)
);

Verify payments

Check balances and transaction history.

Gasless transactions

Abstract SOL fees so users never hold SOL.

Basic payment

Send a single token transfer.

Didn’t find what you were looking for?

Reach out! Telegram | email | Discord