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(…, ownerPubkey, delegateSigner, …)
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@^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);

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. For delegated transfers, pass the source token-account owner as owner (pubkey) and the delegate as authority (signer). Do not use removed InterfaceOptions.owner — there is no { owner: ... } options bag for this flow.
import { transferInterface } from "@lightprotocol/compressed-token/unified";

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

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 the token-account owner pubkey as the flat owner argument; for delegate-signed transactions, set options.delegatePubkey.
import {
  Transaction,
  sendAndConfirmTransaction,
} from "@solana/web3.js";
import { createTransferInterfaceInstructions } from "@lightprotocol/compressed-token/unified";

const instructions = await createTransferInterfaceInstructions(
  rpc,
  payer.publicKey,
  mint,
  200_000,
  owner.publicKey,
  recipient.publicKey,
  9, // decimals
  { delegatePubkey: delegate.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.

Verify payments

Query balances and transaction history.

Didn’t find what you were looking for?

Reach out! Telegram | email | Discord