Skip to main content

Delegation with Light Token works similar to SPL. When you approve a delegate, you’re authorizing a specific account to transfer tokens on your behalf:
  • Owner retains custody: You still own the tokens and can transfer or revoke at any time. Delegation is non-custodial.
  • Capped spending: The delegate can spend tokens up to the limit, but cannot access or drain the account beyond the approved amount.
  • Single delegate per account: Each token account can only have one active delegate. The owner can revoke at any time.
  • New approval replaces old: Approving a new delegate automatically revokes the previous one
SPLLight
Approveapprove()approveInterface()
Delegated Transfertransfer() (delegate signs)transferInterface()
Revokerevoke()revokeInterface()
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

Use cases

Use caseHow delegation helps
SubscriptionsApprove a monthly cap. The service provider transfers the fee each period.
Recurring paymentsApprove a spending limit. The payment processor draws funds as needed.
Managed spendingA parent or admin approves a cap for a sub-account.
Agent walletsAn AI agent operates within a delegated spending limit.

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

Approve a delegate

Grant a delegate permission to spend up to a capped amount:
import { approveInterface } from "@lightprotocol/compressed-token/unified";

const tx = await approveInterface(
  rpc,
  payer,
  senderAta,
  mint,
  delegate.publicKey,     // who gets permission
  500_000,                // amount cap
  owner                   // token owner (signs)
);

console.log("Approved:", tx);
import { getApproveCheckedInstruction } from "@solana-program/token";

const approveInstruction = getApproveCheckedInstruction({
  source: tokenAccountAddress,
  mint: usdcMintAddress,
  delegate: delegateAddress,
  owner: ownerKeypair,
  amount: 1_000_000_000n,
  decimals: 6
});

Check delegation status

Check the delegation status of an account:
import { getAtaInterface } from "@lightprotocol/compressed-token";

const account = await getAtaInterface(rpc, senderAta, owner.publicKey, mint);

console.log("Delegate:", account.parsed.delegate?.toBase58() ?? "none");
console.log("Delegated amount:", account.parsed.delegatedAmount.toString());
import { fetchToken } from "@solana-program/token";

const tokenAccount = await fetchToken(rpc, tokenAccountAddress);

if (tokenAccount.data.delegate) {
  console.log("Delegate:", tokenAccount.data.delegate);
  console.log("Remaining allowance:", tokenAccount.data.delegatedAmount);
} else {
  console.log("No delegate set");
}

Transfer as Delegate

Once approved, the delegate can transfer tokens on behalf of the owner. The delegate is the transaction authority. Only the delegate and fee payer sign; the owner’s signature is not required.transferInterface takes a recipient wallet address and creates the recipient’s associated token account internally. Pass { owner } to transfer as a delegate instead of the owner.
import { transferInterface } from "@lightprotocol/compressed-token/unified";

const tx = await transferInterface(
  rpc,
  payer,
  senderAta,
  mint,
  recipient.publicKey,   // recipient wallet (associated token account created internally)
  delegate,              // delegate authority (signer)
  200_000,               // must be within approved cap
  undefined,
  undefined,
  { owner: owner.publicKey }  // owner (does not sign)
);

console.log("Delegated transfer:", tx);
import { getTransferCheckedInstruction } from "@solana-program/token";

const transferInstruction = getTransferCheckedInstruction({
  source: ownerTokenAccount,
  mint: usdcMintAddress,
  destination: recipientTokenAccount,
  authority: delegateKeypair,
  amount: 100_000_000n,
  decimals: 6
});
createTransferInterfaceInstructions returns TransactionInstruction[][] for manual transaction control. Pass owner to transfer as a delegate.
import {
  Transaction,
  sendAndConfirmTransaction,
} from "@solana/web3.js";
import { createTransferInterfaceInstructions } from "@lightprotocol/compressed-token/unified";

const instructions = await createTransferInterfaceInstructions(
  rpc,
  payer.publicKey,
  mint,
  200_000,
  delegate.publicKey,
  recipient.publicKey,
  9, // decimals
  { owner: owner.publicKey }
);

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

Revoke a delegate

Remove all spending permissions from the current delegate. If you need to reduce the limit, approve the same delegate with a lower amount.
import { revokeInterface } from "@lightprotocol/compressed-token/unified";

const tx = await revokeInterface(rpc, payer, senderAta, mint, owner);

console.log("Revoked:", tx);
import { getRevokeInstruction } from "@solana-program/token";

const revokeInstruction = getRevokeInstruction({
  source: tokenAccountAddress,
  owner: ownerKeypair
});

Basic payment

Send a single token 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