Launch Your Own ICP Token
A comprehensive guide to creating and deploying your own token on the Internet Computer Protocol network, from setup to launch.

Introduction
The Internet Computer Protocol (ICP) enables developers to create and deploy custom fungible tokens using the ICRC-1 token standard. This standard provides a universal framework for token creation, ensuring interoperability, safety, and extensibility across the ICP ecosystem.
ICRC-1 (Internet Computer Request for Comments 1) is the base token standard that defines core functionalities all ICP tokens must support. It includes extensions like ICRC-2 for advanced features such as approvals and transfers.
What is ICRC-1?
ICRC-1 is a comprehensive token standard developed by the Internet Computer's Ledger & Tokenization Working Group. It defines:
- Core functionality: Transfer, balance checking, minting, burning
- Safety features: Atomic transactions, upgrade-safe operations
- Extensibility: Support for metadata, extensions, and custom features
- Interoperability: Compatible with wallets, exchanges, and dApps
Data Types
ICRC-1 defines key data structures for token operations:
Account Structure:
type Subaccount = blob;
type Account = record { owner : principal; subaccount : opt Subaccount; };
An account consists of a principal (user identity) and an optional 32-byte subaccount for multiple addresses per principal. The subaccount with all bytes set to 0 is the default account.
Core Methods
icrc1_transfer: Moves tokens between accounts with atomic operations
icrc1_transfer : (TransferArgs) -> (variant { Ok: nat; Err: TransferError });
TransferArgs structure:
type TransferArgs = record {
from_subaccount : opt Subaccount;
to : Account;
amount : nat;
fee : opt nat;
memo : opt blob;
created_at_time : opt nat64;
};
icrc1_balance_of: Query account balances
icrc1_balance_of : (Account) -> (nat) query;
icrc1_total_supply: Get total token supply (excluding minting account)
icrc1_total_supply : () -> (nat) query;
Transaction Deduplication
ICRC-1 implements sophisticated deduplication to handle network failures:
- Uses
created_at_timeandmemofields for transaction identification - Prevents duplicate transactions within a configurable time window
- Returns
Duplicateerror with original transaction index for replayed transfers
Minting Account
A unique account that can create new tokens and receive burns:
- Transfers from minting account create tokens (minting)
- Transfers to minting account remove tokens (burning)
- Acts as the token supply regulator
Metadata Support
ICRC-1 supports rich token metadata through the icrc1_metadata endpoint:
- Optional key-value pairs for token information
- Standardized keys like
icrc1:symbol,icrc1:name,icrc1:decimals - Custom metadata for branding and additional information
ICRC-2: Approve and Transfer From
ICRC-2 extends ICRC-1 with approval mechanisms, enabling third-party transfers on behalf of token owners. This standard enables DeFi applications like automated trading and subscription services.
Key Features
Two-step Process:
- Approval: Token owner authorizes a spender to transfer tokens on their behalf
- Transfer From: Authorized spender executes the transfer
Non-transitive Approvals: If Alice approves Bob to spend her tokens, and Bob approves Charlie to spend his tokens, Charlie cannot access Alice's tokens.
Core Methods
icrc2_approve: Delegate transfer authority to a spender
icrc2_approve : (ApproveArgs) -> (variant { Ok : nat; Err : ApproveError });
ApproveArgs structure:
type ApproveArgs = record {
from_subaccount : opt blob;
spender : Account;
amount : nat;
expected_allowance : opt nat;
expires_at : opt nat64;
fee : opt nat;
memo : opt blob;
created_at_time : opt nat64;
};
icrc2_transfer_from: Execute approved transfer
icrc2_transfer_from : (TransferFromArgs) -> (variant { Ok : nat; Err : TransferFromError });
icrc2_allowance: Check remaining approved amount
icrc2_allowance : (AllowanceArgs) -> (Allowance) query;
Practical Use Cases
Recurring Payments: Alice approves a service to deduct monthly fees automatically.
Automated Trading: Alice approves a trading bot to execute trades within specified limits.
Subscription Services: Decentralized platforms can charge users based on approved allowances.
Example Flow
- Alice approves Canister C to spend 100 tokens:
dfx canister call token_ledger icrc2_approve "(record {
spender = record { owner = principal "canister-principal" };
amount = 10000000000; // 100 tokens
})"
- Canister C transfers 50 tokens from Alice to Bob:
dfx canister call token_ledger icrc2_transfer_from "(record {
from = record { owner = principal "alice-principal" };
to = record { owner = principal "bob-principal" };
amount = 5000000000; // 50 tokens
spender_subaccount = null;
})"
- Check remaining allowance:
dfx canister call token_ledger icrc2_allowance "(record {
account = record { owner = principal "alice-principal" };
spender = record { owner = principal "canister-principal" };
})"
Prerequisites
Before launching your token, ensure you have:
- dfx: The ICP SDK (latest version recommended)
- ICP Cycles: For canister deployment and operations
- Internet Identity: For authentication and control
- Basic ICP knowledge: Understanding of canisters and principals
Environment Setup
# Install dfx (if not already installed)
sh -ci "$(curl -fsSL https://internetcomputer.org/install.sh)"
# Check dfx version
dfx --version
# Start local replica (for testing)
dfx start --clean
Steps to Launch Your Token
Step 1: Download ICRC-1 Ledger Files
First, download the official ICRC-1 ledger implementation:
# Get the latest IC version from dashboard
export IC_VERSION=1612a202d030faa496e1694eed98be4179fca856
# Download Wasm module
curl -o icrc1-ledger.wasm.gz "https://download.dfinity.systems/ic/$IC_VERSION/canisters/ic-icrc1-ledger.wasm.gz"
# Download Candid interface
curl -o icrc1-ledger.did "https://raw.githubusercontent.com/dfinity/ic/$IC_VERSION/rs/ledger_suite/icrc1/ledger/ledger.did"
# Unzip Wasm file
gunzip icrc1-ledger.wasm.gz
Step 2: Configure Your Project
Create or update your dfx.json file:
{
"canisters": {
"my-token-ledger": {
"type": "custom",
"wasm": "icrc1-ledger.wasm",
"candid": "icrc1-ledger.did"
}
},
"defaults": {
"build": {
"args": "",
"packtool": ""
}
},
"version": 1
}
Step 3: Set Up Token Parameters
Configure your token's properties:
# Set token identity
export TOKEN_NAME="My Awesome Token"
export TOKEN_SYMBOL="MATK"
# Set controller principals
export MINTER_PRINCIPAL=$(dfx identity get-principal)
export ARCHIVE_CONTROLLER=$(dfx identity get-principal)
# Set token economics
export PRE_MINTED_TOKENS=1000000000 # 1 billion tokens
export TRANSFER_FEE=10000 # 0.0001 tokens per transfer
# Set archive options
export TRIGGER_THRESHOLD=2000
export NUM_BLOCKS_TO_ARCHIVE=1000
export CYCLE_FOR_ARCHIVE_CREATION=10000000000000
Step 4: Deploy Locally (Testing)
Deploy your token to the local replica first:
# Deploy with initialization arguments
dfx deploy my-token-ledger --argument "(variant {
Init = record {
token_symbol = "${TOKEN_SYMBOL}";
token_name = "${TOKEN_NAME}";
minting_account = record { owner = principal "${MINTER_PRINCIPAL}" };
transfer_fee = ${TRANSFER_FEE};
metadata = vec {};
feature_flags = opt record { icrc2 = true };
initial_balances = vec {
record {
record { owner = principal "${MINTER_PRINCIPAL}" };
${PRE_MINTED_TOKENS};
};
};
archive_options = record {
num_blocks_to_archive = ${NUM_BLOCKS_TO_ARCHIVE};
trigger_threshold = ${TRIGGER_THRESHOLD};
controller_id = principal "${ARCHIVE_CONTROLLER}";
cycles_for_archive_creation = opt ${CYCLE_FOR_ARCHIVE_CREATION};
};
}
})"
Step 5: Test Your Token
Verify your token deployment:
# Check token symbol
dfx canister call my-token-ledger icrc1_symbol
# Check token name
dfx canister call my-token-ledger icrc1_name
# Check your balance
dfx canister call my-token-ledger icrc1_balance_of "(record { owner = principal "$(dfx identity get-principal)" })"
# Test a transfer
dfx canister call my-token-ledger icrc1_transfer "(record {
to = record { owner = principal "some-other-principal" };
amount = 1000000;
})"
Deploying to Mainnet
Mainnet Considerations
Deploying to the Internet Computer mainnet requires:
- Cycles: Sufficient for deployment and archive creation
- Unique canister ID: Remove
--specified-idfor auto-assignment - Production principals: Use secure, dedicated identities
- Archive funding: Ensure cycles for archive canister spawning
Mainnet Deployment Command
# Set network to mainnet
export NETWORK=ic
# Deploy to mainnet
dfx deploy --network $NETWORK my-token-ledger --argument "(variant {
Init = record {
token_symbol = "${TOKEN_SYMBOL}";
token_name = "${TOKEN_NAME}";
minting_account = record { owner = principal "${MINTER_PRINCIPAL}" };
transfer_fee = ${TRANSFER_FEE};
metadata = vec {};
feature_flags = opt record { icrc2 = true };
initial_balances = vec {
record {
record { owner = principal "${MINTER_PRINCIPAL}" };
${PRE_MINTED_TOKENS};
};
};
archive_options = record {
num_blocks_to_archive = ${NUM_BLOCKS_TO_ARCHIVE};
trigger_threshold = ${TRIGGER_THRESHOLD};
controller_id = principal "${ARCHIVE_CONTROLLER}";
cycles_for_archive_creation = opt ${CYCLE_FOR_ARCHIVE_CREATION};
};
}
})"
Post-Deployment Checklist
- Verify deployment: Check canister status and cycles balance
- Test transactions: Send tokens between accounts
- Wallet integration: Add to supported ICP wallets
- Exchange listing: Submit for DEX/CEX listings
- Community building: Create documentation and support channels
Practical Code Examples
The Internet Computer ecosystem provides comprehensive examples for working with ICRC tokens. Here are key implementations from the official repositories.
Basic Token Transfer (Motoko)
From the token_transfer example, here's a basic canister that transfers ICRC-1 tokens:
import Icrc1Ledger "canister:icrc1_ledger_canister";
import Debug "mo:base/Debug";
import Result "mo:base/Result";
import Error "mo:base/Error";
persistent actor {
type TransferArgs = {
amount : Nat;
toAccount : Icrc1Ledger.Account;
};
public shared func transfer(args : TransferArgs) : async Result.Result<Icrc1Ledger.BlockIndex, Text> {
Debug.print(
"Transferring "
# debug_show (args.amount)
# " tokens to account"
# debug_show (args.toAccount)
);
let transferArgs : Icrc1Ledger.TransferArg = {
memo = null;
amount = args.amount;
from_subaccount = null;
fee = null;
to = args.toAccount;
created_at_time = null;
};
try {
let transferResult = await Icrc1Ledger.icrc1_transfer(transferArgs);
switch (transferResult) {
case (#Err(transferError)) {
return #err("Couldn't transfer funds:\n" # debug_show (transferError));
};
case (#Ok(blockIndex)) { return #ok blockIndex };
};
} catch (error : Error) {
return #err("Reject message: " # Error.message(error));
};
};
};
Transfer From (ICRC-2 Approval Flow)
From the token_transfer_from example, here's how to implement transfer-from functionality:
import Icrc1Ledger "canister:icrc1_ledger_canister";
import Debug "mo:base/Debug";
import Result "mo:base/Result";
import Error "mo:base/Error";
persistent actor {
type TransferArgs = {
amount : Nat;
toAccount : Icrc1Ledger.Account;
};
public shared ({ caller }) func transfer(args : TransferArgs) : async Result.Result<Icrc1Ledger.BlockIndex, Text> {
Debug.print(
"Transferring "
# debug_show (args.amount)
# " tokens to account"
# debug_show (args.toAccount)
);
let transferFromArgs : Icrc1Ledger.TransferFromArgs = {
from = {
owner = caller;
subaccount = null;
};
memo = null;
amount = args.amount;
spender_subaccount = null;
fee = null;
to = args.toAccount;
created_at_time = null;
};
try {
let transferFromResult = await Icrc1Ledger.icrc2_transfer_from(transferFromArgs);
switch (transferFromResult) {
case (#Err(transferError)) {
return #err("Couldn't transfer funds:\n" # debug_show (transferError));
};
case (#Ok(blockIndex)) { return #ok blockIndex };
};
} catch (error : Error) {
return #err("Reject message: " # Error.message(error));
};
};
};
Setup Process:
- Deploy ICRC-1 ledger with ICRC-2 enabled:
feature_flags = opt record { icrc2 = true } - Approve spender:
dfx canister call ledger icrc2_approve "(record { spender = record { owner = principal "spender-principal" }; amount = 10000000000 })" - Execute transfer-from: Call the canister's transfer function
Token Swap Implementation
From the icrc2-swap example, here's a DeFi swap canister:
Deposit Function:
public shared ({ caller }) func deposit(args : DepositArgs) : async Result.Result<(), Text> {
// 1. Transfer tokens from user to canister using ICRC-2 transfer_from
let transferArgs = {
spender_subaccount = null;
from = args.from;
to = { owner = Principal.fromActor(this); subaccount = null };
amount = args.amount;
fee = null;
memo = null;
created_at_time = null;
};
let transferResult = await tokenLedger.icrc2_transfer_from(transferArgs);
switch (transferResult) {
case (#Err(err)) { return #err("Transfer failed: " # debug_show(err)) };
case (#Ok(_)) {
// 2. Update internal balance tracking
let currentBalance = switch (balances.get(args.token)) {
case (?b) { b };
case null { HashMap.HashMap(10, Principal.equal, Principal.hash) };
};
let userBalance = Option.get(currentBalance.get(caller), 0);
currentBalance.put(caller, userBalance + args.amount);
balances.put(args.token, currentBalance);
#ok(())
};
};
};
Swap Function:
public func swap(userA : Principal, userB : Principal) : async Result.Result<(), Text> {
// Simple 1:1 swap logic
let tokenA = token_a;
let tokenB = token_b;
let balanceA = Option.get(getBalance(userA, tokenA), 0);
let balanceB = Option.get(getBalance(userB, tokenB), 0);
if (balanceA == 0 or balanceB == 0) {
return #err("Insufficient balance for swap");
};
// Update balances
updateBalance(userA, tokenA, balanceA - min(balanceA, balanceB));
updateBalance(userA, tokenB, min(balanceA, balanceB));
updateBalance(userB, tokenA, min(balanceA, balanceB));
updateBalance(userB, tokenB, balanceB - min(balanceA, balanceB));
#ok(());
};
Withdraw Function:
public shared ({ caller }) func withdraw(args : WithdrawArgs) : async Result.Result<(), Text> {
let balance = Option.get(getBalance(caller, args.token), 0);
if (balance < args.amount) {
return #err("Insufficient balance");
};
// Transfer tokens from canister back to user
let transferArgs = {
from_subaccount = null;
to = args.to;
amount = args.amount;
fee = null;
memo = null;
created_at_time = null;
};
let transferResult = await tokenLedger.icrc1_transfer(transferArgs);
switch (transferResult) {
case (#Err(err)) { return #err("Transfer failed: " # debug_show(err)) };
case (#Ok(_)) {
updateBalance(caller, args.token, balance - args.amount);
#ok(())
};
};
};
ICRC-1 Token Features
Core Functionality
- icrc1_transfer: Move tokens between accounts
- icrc1_balance_of: Check account balances
- icrc1_total_supply: Get total token supply
- icrc1_minting_account: View minter information
- icrc1_metadata: Access token metadata
Extensions Support
- ICRC-2: Approvals and transfer-from functionality
- Metadata: Custom token information and branding
- Archives: Efficient transaction history storage
Advanced Features
- Subaccounts: Multiple addresses per principal
- Notifications: Optional transaction notifications
- Batch operations: Multiple transfers in one call
- Time locks: Future-dated transactions
Token Configuration Options
Token Economics
| Parameter | Description | Example |
|---|---|---|
token_symbol | Ticker symbol | "MATK" |
token_name | Full name | "My Awesome Token" |
transfer_fee | Fee per transfer | 10000 (0.0001 tokens) |
initial_balances | Starting distribution | Pre-mint to creator |
Archive Configuration
| Parameter | Description | Recommended Value |
|---|---|---|
trigger_threshold | Blocks before archiving | 2000 |
num_blocks_to_archive | Archive batch size | 1000 |
cycles_for_archive_creation | Archive funding | 10^13 cycles |
Feature Flags
feature_flags = opt record {
icrc2 = true; // Enable approvals and transfer-from
};
Security Best Practices
Principal Management
- Use dedicated identities for minting and archives
- Implement multi-signature controls for large operations
- Regularly rotate archive controllers
Cycles Management
- Monitor canister cycles balance
- Set up automatic top-ups
- Plan for archive creation costs
Access Control
- Limit minting capabilities
- Implement proper archive controls
- Use secure deployment practices
Integration with Wallets and Exchanges
Wallet Support
Most ICP-compatible wallets support ICRC-1 tokens automatically. Add your token by canister ID.
Exchange Listing
To list on DEXs like ICP.Swap:
- Deploy token with proper metadata
- Create liquidity pools
- Submit for listing consideration
Troubleshooting
Common Issues
Deployment fails with "insufficient cycles"
- Ensure canister has enough cycles (minimum 1T cycles recommended)
- Check archive creation funding
Transfers fail with "insufficient funds"
- Account for transfer fees in balance checks
- Ensure sender has enough tokens including fee
Archive canister creation fails
- Verify archive controller permissions
- Check cycles allocation for archives
Getting Help
- Forum: https://forum.dfinity.org/
- Documentation: https://internetcomputer.org/docs/
- Working Group: Ledger & Tokenization discussions
Future Developments
The ICRC-1 standard continues to evolve with:
- ICRC-2: Enhanced approval mechanisms
- ICRC-3: Transaction history standardization
- ICRC-4: Non-fungible token extensions
- Improved metadata: Rich token information
Conclusion
Launching your own ICP token using the ICRC-1 standard is straightforward and powerful. The standard provides the foundation for creating tokens that are safe, interoperable, and feature-rich.
Remember to:
- Test thoroughly on local replica
- Plan your token economics carefully
- Implement proper security measures
- Build community and integrations
Your token can now participate in the growing ICP ecosystem, powering dApps, DeFi protocols, and digital economies.
Ready to launch your token? Start with the ICRC-1 standard and join the future of decentralized finance on the Internet Computer.