Skip to main content

Solana’s memo program lets you attach invoice numbers, order IDs, or custom references to any payment. These memos are permanently recorded on-chain and visible in transaction logs, making it easy to match payments to your internal systems. 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.
Additionally install the memo program package:
npm install @solana/spl-memo
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 };
}
2

Send a Payment with Memo

  1. Create the transfer instructions with createTransferInterfaceInstructions(). Load transactions are handled under the hood.
  2. Create a memo instruction with a message. This message will be visible in the program logs of the transaction.
  3. Both instructions execute atomically in the same transaction.
After the transaction confirms, fetch it to view the memo in the logs. The memo appears as a log message from the Memo program.
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 { createMemoInstruction } from "@solana/spl-memo";
import { rpc, payer, setup } from "../setup.js";

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

    const instructions = await createTransferInterfaceInstructions(
        rpc,
        payer.publicKey,
        mint,
        100,
        payer.publicKey,
        recipient.publicKey
    );

    // Append memo to the transfer transaction (last batch)
    const memoIx = createMemoInstruction("INV-2024-001");
    instructions[instructions.length - 1].push(memoIx);

    let signature;
    for (const ixs of instructions) {
        const tx = new Transaction().add(...ixs);
        signature = await sendAndConfirmTransaction(rpc, tx, [payer]);
    }
    console.log("Tx with memo:", signature);

    // Read memo back from transaction logs
    const txDetails = await rpc.getTransaction(signature!, {
        maxSupportedTransactionVersion: 0,
    });

    const logs = txDetails?.meta?.logMessages || [];
    const memoLogs = logs.filter((log: string) =>
        log.includes("Program MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr")
    );
    console.log("Memo logs:", memoLogs);
})();
import { getAddMemoInstruction } from "@solana-program/memo";
import { getTransferInstruction } from "@solana-program/token";

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

const memoInstruction = getAddMemoInstruction({
  memo: "Payment for services rendered - Invoice #12345"
});

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

const transaction = await client.runtime.rpc
  .getTransaction(signature, {
    encoding: "jsonParsed",
    maxSupportedTransactionVersion: 0
  })
  .send();

console.log("Transaction logs with Memo:");
console.log(transaction?.meta?.logMessages);
3

Read Memo from Transaction Logs

After the transaction confirms, the memo appears in the transaction logs:
Example Transaction Logs
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [1]",
"Program log: Instruction: Transfer",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1682 of 200000 compute units",
"Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success",
"Program MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr invoke [1]",
'Program log: Memo (len 46): "Payment for services rendered - Invoice #12345"',
"Program MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr consumed 18097 of 198318 compute units",
"Program MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr success",
"Program ComputeBudget111111111111111111111111111111 invoke [1]",
"Program ComputeBudget111111111111111111111111111111 success"

Basic payment

Send a single transfer without memo.

Verify payments

Query balances and transaction history.

Batch payments

Pack multiple transfers into one transaction.

Didn’t find what you were looking for?

Reach out! Telegram | email | Discord