Overview

The Token Plugin provides comprehensive token management for ICP applications, supporting both ICP ledger and ICRC-1/ICRC-2 standard tokens. It handles transfers, balance queries, metadata retrieval, and account management with full type safety and error handling.

Core Features

ICP Transfers

Send ICP tokens between accounts with automatic fee calculation and validation

ICRC-1 & ICRC-2 Support

Full support for ICRC standard tokens including approvals and allowances

Balance Queries

Check token balances for any account, principal, or canister

Account Management

Create and manage multiple accounts with subaccount support

Transaction Utilities

Format amounts, parse transactions, and handle memos with ease

Metadata Retrieval

Get token metadata including symbol, decimals, and total supply

Quick Start

import { ICPAgent } from 'icp-agent-kit';

// Initialize agent with identity
const agent = new ICPAgent({ network: 'mainnet' });
await agent.initialize();

// Create identity for token operations
const identity = agent.identityPlugin;
const seedPhrase = identity.generateSeedPhrase(12);
await identity.createFromSeedPhrase(seedPhrase, 'my-wallet');
await identity.switch('my-wallet');

const token = agent.tokenPlugin;

// Transfer ICP tokens
const result = await token.transfer(
  'recipient-account-id-or-principal',
  1_00000000n // 1 ICP in e8s
);

// Check balance
const balance = await token.getBalance();
console.log(`Balance: ${token.formatAmount(balance, 8)} ICP`);

LangChain Integration

The Token Plugin is integrated with LangChain tools for natural language operations:

Available Tools

transfer_icp

Transfer ICP tokens with natural language

get_balance

Check ICP balance for current identity

icrc1_transfer

Transfer ICRC-1 standard tokens

Natural Language Examples

// Initialize with OpenAI
const agent = new ICPAgent({
  network: 'mainnet',
  openai: { apiKey: process.env.OPENAI_API_KEY }
});
await agent.initialize();

// Transfer ICP
await agent.processNaturalLanguage(
  "Transfer 5 ICP to alice.icp with memo: payment for services"
);

// Check balance
await agent.processNaturalLanguage("What is my ICP balance?");

// Transfer ICRC-1 tokens
await agent.processNaturalLanguage(
  "Send 1000 CHAT tokens from canister xyz to bob"
);

Using with DeFi Agent

const defiAgent = agent.createAgent('defi');

// Portfolio management
await defiAgent.chat("Show me all my token balances");
await defiAgent.chat("Help me distribute tokens to my team");
await defiAgent.chat("What's the most gas-efficient way to transfer?");

Core Methods

Transfer Operations

transfer(to, amount, options?)

Transfer tokens to another account with comprehensive validation and error handling.
// Basic ICP transfer
const result = await token.transfer(
  'recipient-account-id',
  1_00000000n // 1 ICP
);

// Transfer with options
const result = await token.transfer(
  recipientPrincipal,
  amount,
  {
    memo: 12345n,
    fee: 10000n,
    fromSubaccount: subaccount,
    createdAtTime: BigInt(Date.now() * 1_000_000)
  }
);

// ICRC-1 token transfer
const icrcResult = await token.transferICRC1(
  'canister-id',
  {
    to: { owner: recipientPrincipal, subaccount: undefined },
    amount: 1000000n,
    fee: 1000n
  }
);
Parameters:
  • to: Recipient account (Principal, account ID string, or IAccount object)
  • amount: Amount to transfer (bigint in smallest token units)
  • options: Transfer options including memo, fee, subaccount, and timestamp
Returns:
interface ITransferResult {
  blockHeight: bigint;
  transactionId?: string;
  fee: bigint;
  timestamp: bigint;
  amount?: bigint;
  status?: 'success' | 'pending' | 'failed';
}

Balance Operations

getBalance(account?)

Get token balance for the current identity or specified account.
// Current identity balance
const balance = await token.getBalance();

// Specific account balance
const otherBalance = await token.getBalance('account-id');

// Principal balance
const principalBalance = await token.getBalance(Principal.fromText('principal-id'));

// ICRC-1 token balance
const icrcBalance = await token.getBalanceICRC1('canister-id', {
  owner: principal,
  subaccount: undefined
});
Returns: bigint - Balance in smallest token units

Account Management

getAccountId(principal?, subaccount?)

Generate account identifier from principal and optional subaccount.
// Current identity account ID
const accountId = token.getAccountId();

// Specific principal account ID
const accountId = token.getAccountId(Principal.fromText('principal-id'));

// Account ID with subaccount
const subaccountedId = token.getAccountId(principal, subaccount);

createAccount(principal, subaccount?)

Create an IAccount object for ICRC operations.
// Create account from principal
const account = token.createAccount('principal-text');

// Create account with subaccount
const account = token.createAccount(principal, new Uint8Array(32));

generateSubaccount()

Generate a random 32-byte subaccount.
const subaccount = token.generateSubaccount();
const account = token.createAccount(principal, subaccount);

Utility Functions

formatAmount(amount, decimals?)

Format raw token amounts for display.
// Format ICP amount (8 decimals)
const formatted = token.formatAmount(100000000n, 8); // "1"
const formatted = token.formatAmount(150000000n, 8); // "1.5"

// Format with custom decimals
const formatted = token.formatAmount(1000000n, 6); // "1"

parseAmount(amountStr, decimals?)

Parse formatted amounts to raw bigint values.
// Parse ICP amount
const amount = token.parseAmount("1.5", 8); // 150000000n

// Parse with custom decimals
const amount = token.parseAmount("0.001", 6); // 1000n

createMemo(input)

Create transaction memos from various inputs.
// From string
const memo = token.createMemo("Payment for services");

// From number
const memo = token.createMemo(12345);

// From bigint
const memo = token.createMemo(12345n);

Validation

validateAccount(account)

Validate account identifiers and principals.
// Validate principal
const isValid = token.validateAccount('principal-text');

// Validate account ID
const isValid = token.validateAccount('account-identifier');

ICRC Token Operations

ICRC-1 Support

// Get ICRC-1 token metadata
const metadata = await token.getMetadataICRC1('canister-id');
console.log(`Token: ${metadata.name} (${metadata.symbol})`);
console.log(`Decimals: ${metadata.decimals}`);
console.log(`Fee: ${metadata.fee}`);

// Transfer ICRC-1 tokens
const result = await token.transferICRC1('canister-id', {
  to: { owner: recipientPrincipal, subaccount: undefined },
  amount: 1000000n,
  memo: new TextEncoder().encode("Payment"),
  fee: 1000n
});

// Get transaction fee
const fee = await token.getFeeICRC1('canister-id');

ICRC-2 Support (Approvals)

// Approve spending
const approveResult = await token.approveICRC2('canister-id', {
  spender: { owner: spenderPrincipal, subaccount: undefined },
  amount: 1000000n,
  expires_at: BigInt(Date.now() + 3600000) * 1_000_000n // 1 hour
});

// Check allowance
const allowance = await token.getAllowanceICRC2('canister-id', {
  account: { owner: ownerPrincipal, subaccount: undefined },
  spender: { owner: spenderPrincipal, subaccount: undefined }
});

console.log(`Allowance: ${allowance.allowance}`);
if (allowance.expires_at) {
  console.log(`Expires: ${new Date(Number(allowance.expires_at / 1_000_000n))}`);
}

Error Handling

The Token Plugin provides comprehensive error handling with descriptive messages:
try {
  await token.transfer(recipient, amount);
} catch (error) {
  if (error instanceof ICPAgentError) {
    switch (error.code) {
      case 'INSUFFICIENT_BALANCE':
        console.error('Not enough tokens for transfer');
        break;
      case 'INVALID_ACCOUNT_ID':
        console.error('Invalid recipient address');
        break;
      case 'TRANSFER_FAILED':
        console.error('Transfer failed:', error.message);
        break;
    }
  }
}

Common Error Types

Error CodeDescriptionSolution
INSUFFICIENT_BALANCENot enough tokens for transfer + feeCheck balance before transfer
INVALID_ACCOUNT_IDInvalid recipient address formatValidate address format
INVALID_PRINCIPALInvalid principal formatUse Principal.fromText()
TRANSFER_FAILEDLedger rejected the transferCheck amount, fee, and account validity
LEDGER_UNAVAILABLECanister temporarily unavailableRetry after some time
BAD_FEEIncorrect fee amountUse the expected fee from metadata

Integration with Identity Plugin

The Token Plugin automatically uses the active identity from the Identity Plugin:
// Set up multiple wallets
await identity.createFromSeedPhrase(seedPhrase1, 'main-wallet');
await identity.createFromSeedPhrase(seedPhrase2, 'trading-wallet');

// Use main wallet for transfer
await identity.switch('main-wallet');
await token.transfer(recipient, amount);

// Switch to trading wallet
await identity.switch('trading-wallet');
const balance = await token.getBalance();

Advanced Usage

Multi-Token Portfolio Management

// Manage multiple ICRC tokens
const tokens = [
  { canisterId: 'ckbtc-canister-id', symbol: 'ckBTC' },
  { canisterId: 'cketh-canister-id', symbol: 'ckETH' }
];

for (const tokenInfo of tokens) {
  const metadata = await token.getMetadataICRC1(tokenInfo.canisterId);
  const balance = await token.getBalanceICRC1(tokenInfo.canisterId, {
    owner: identity.getPrincipal(),
    subaccount: undefined
  });
  
  console.log(`${metadata.symbol}: ${token.formatAmount(balance, metadata.decimals)}`);
}

Batch Operations

// Check multiple account balances
const accounts = ['account1', 'account2', 'account3'];
const balances = await Promise.all(
  accounts.map(account => token.getBalance(account))
);

// Format all balances
balances.forEach((balance, index) => {
  console.log(`${accounts[index]}: ${token.formatAmount(balance, 8)} ICP`);
});

Custom Fee Estimation

// Estimate fees for priority transaction
const standardFee = 10000n; // 0.0001 ICP
const priorityFee = token.estimateFee(standardFee, true); // 2x for priority

await token.transfer(recipient, amount, {
  fee: priorityFee
});

Type Definitions

Core Types

interface IAccount {
  owner: Principal;
  subaccount?: Uint8Array;
}

interface ITransferOptions {
  memo?: bigint;
  fee?: bigint;
  fromSubaccount?: Uint8Array;
  createdAtTime?: bigint;
}

interface ITokenMetadata {
  name: string;
  symbol: string;
  decimals: number;
  fee: bigint;
  totalSupply: bigint;
  standard?: string;
  logo?: string;
}

ICRC Types

interface IICRC1TransferArg {
  to: IAccount;
  amount: bigint;
  fee?: bigint;
  memo?: Uint8Array;
  from_subaccount?: Uint8Array;
  created_at_time?: bigint;
}

interface IApproveArgs {
  spender: IAccount;
  amount: bigint;
  expected_allowance?: bigint;
  expires_at?: bigint;
  fee?: bigint;
  memo?: Uint8Array;
  from_subaccount?: Uint8Array;
  created_at_time?: bigint;
}

Best Practices

1. Always Validate Inputs

// Validate before transferring
try {
  token.validateAccount(recipient);
  const validAmount = token.parseAmount(userInput, 8);
  await token.transfer(recipient, validAmount);
} catch (error) {
  console.error('Validation failed:', error.message);
}

2. Handle Network Conditions

// Retry with exponential backoff
async function transferWithRetry(to: string, amount: bigint, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await token.transfer(to, amount);
    } catch (error) {
      if (error.code === 'LEDGER_UNAVAILABLE' && i < maxRetries - 1) {
        await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
        continue;
      }
      throw error;
    }
  }
}

3. Use Appropriate Precision

// Always use bigint for amounts
const oneICP = 100_000_000n; // 1 ICP = 10^8 e8s
const halfICP = 50_000_000n;  // 0.5 ICP

// Format for display
console.log(`Amount: ${token.formatAmount(oneICP, 8)} ICP`);

4. Secure Memo Handling

// Avoid sensitive data in memos (they're public)
const safeMemo = token.createMemo("Invoice #12345");

// Use structured data carefully
const memo = token.createMemo(JSON.stringify({
  type: "payment",
  invoice: "12345"
}));

Examples

Simple Payment System

class PaymentProcessor {
  constructor(private agent: ICPAgent) {}

  async processPayment(
    fromWallet: string,
    toAccount: string,
    amountICP: string,
    description: string
  ) {
    const token = this.agent.tokenPlugin;
    const identity = this.agent.identityPlugin;
    
    // Switch to sender wallet
    await identity.switch(fromWallet);
    
    // Parse amount and validate
    const amount = token.parseAmount(amountICP, 8);
    token.validateAccount(toAccount);
    
    // Check balance
    const balance = await token.getBalance();
    const fee = 10000n; // Standard ICP fee
    
    if (balance < amount + fee) {
      throw new Error('Insufficient balance');
    }
    
    // Create memo and transfer
    const memo = token.createMemo(description);
    const result = await token.transfer(toAccount, amount, { memo });
    
    return {
      blockHeight: result.blockHeight,
      amount: token.formatAmount(amount, 8),
      fee: token.formatAmount(result.fee, 8),
      timestamp: new Date(Number(result.timestamp / 1_000_000n))
    };
  }
}

Multi-Token Wallet

class MultiTokenWallet {
  constructor(private agent: ICPAgent) {}

  async getPortfolio() {
    const token = this.agent.tokenPlugin;
    const identity = this.agent.identityPlugin;
    const principal = identity.getPrincipal();
    
    const portfolio = [];
    
    // ICP balance
    const icpBalance = await token.getBalance();
    portfolio.push({
      symbol: 'ICP',
      balance: token.formatAmount(icpBalance, 8),
      decimals: 8
    });
    
    // ICRC tokens
    const icrcTokens = [
      { canisterId: 'ckbtc-canister', symbol: 'ckBTC' },
      { canisterId: 'cketh-canister', symbol: 'ckETH' }
    ];
    
    for (const tokenInfo of icrcTokens) {
      try {
        const metadata = await token.getMetadataICRC1(tokenInfo.canisterId);
        const balance = await token.getBalanceICRC1(tokenInfo.canisterId, {
          owner: principal,
          subaccount: undefined
        });
        
        portfolio.push({
          symbol: metadata.symbol,
          balance: token.formatAmount(balance, metadata.decimals),
          decimals: metadata.decimals
        });
      } catch (error) {
        console.warn(`Failed to fetch ${tokenInfo.symbol}:`, error.message);
      }
    }
    
    return portfolio;
  }
}

Performance Tips

  1. Batch Balance Queries: Use Promise.all() for multiple balance checks
  2. Cache Metadata: Token metadata rarely changes, consider caching
  3. Optimize Fee Queries: Get fee once and reuse for multiple transactions
  4. Use Subaccounts: Create subaccounts for different purposes within the same principal

Troubleshooting

Common Issues

Transfer Fails with “BadFee”
// Get the current fee and use it
const fee = await token.getFeeICRC1('canister-id');
await token.transferICRC1('canister-id', { to, amount, fee });
Account ID vs Principal Confusion
// Use the right format for the right ledger
const accountId = token.getAccountId(principal); // For ICP ledger
const account = { owner: principal, subaccount: undefined }; // For ICRC
Precision Loss in Amount Parsing
// Always specify decimals explicitly
const amount = token.parseAmount("1.5", 8); // ✓ Correct
const amount = token.parseAmount("1.5");    // ✗ May use wrong decimals
The Token Plugin provides a comprehensive foundation for all token operations in your ICP applications. Combined with the Identity Plugin, it enables secure, type-safe token management with full support for both legacy ICP and modern ICRC standards.