Skip to main content
Tempo’s TIP-20 token standard extends ERC-20 with payment-specific features like on-transfer memos, dedicated payment lanes for predictable throughput, and sub-millidollar transaction costs.

TIP-20 Overview

TIP-20 tokens are enshrined at the protocol level with precompile addresses starting at 0x20c0.... They provide:
  • Predictable throughput via dedicated payment lanes
  • Native reconciliation with on-transfer memos
  • Low fees targeting <$0.001 per transfer
  • Compliance through the TIP-403 policy registry

Basic Token Transfer

Send a simple TIP-20 transfer using the Tempo Alloy SDK:
use alloy::{
    primitives::{U256, address},
    providers::ProviderBuilder,
};
use tempo_alloy::{TempoNetwork, contracts::precompiles::ITIP20};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let provider = ProviderBuilder::new_with_network::<TempoNetwork>()
        .connect(&std::env::var("RPC_URL").expect("No RPC URL set"))
        .await?;

    // AlphaUSD token at precompile address
    let token = ITIP20::new(
        address!("0x20c0000000000000000000000000000000000001"),
        &provider,
    );

    let receipt = token
        .transfer(
            address!("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb"),
            U256::from(100_000_000), // 100 tokens (6 decimals)
        )
        .send()
        .await?
        .get_receipt()
        .await?;

    println!("Transfer successful: {:?}", receipt.transaction_hash);

    Ok(())
}
TIP-20 tokens use 6 decimals by default. To send 100 tokens, use 100_000_000.

Transfer with Memo

Add a memo to your transfer for payment reconciliation, invoice tracking, or reference data:
use alloy::{
    primitives::{B256, U256, address},
    providers::ProviderBuilder,
};
use tempo_alloy::{TempoNetwork, contracts::precompiles::ITIP20};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let provider = ProviderBuilder::new_with_network::<TempoNetwork>()
        .connect(&std::env::var("RPC_URL").expect("No RPC URL set"))
        .await?;

    let token = ITIP20::new(
        address!("0x20c0000000000000000000000000000000000001"),
        &provider,
    );

    let receipt = token
        .transferWithMemo(
            address!("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb"),
            U256::from(100_000_000), // 100 tokens (6 decimals)
            B256::left_padding_from("INV-12345".as_bytes()),
        )
        .send()
        .await?
        .get_receipt()
        .await?;

    println!("Transfer successful: {:?}", receipt.transaction_hash);

    Ok(())
}

Memo Patterns

TIP-20 supports multiple memo patterns for different use cases:

Direct Memo (32 bytes)

Store small identifiers directly on-chain:
// Invoice number
let memo = B256::left_padding_from("INV-12345".as_bytes());

// UUID (16 bytes)
let uuid = uuid::Uuid::new_v4();
let memo = B256::left_padding_from(uuid.as_bytes());

Hash Commitment

Store a hash of off-chain data for privacy:
use alloy::primitives::keccak256;

// Hash sensitive payment data
let payment_details = "Invoice INV-12345, Customer: Acme Corp, Amount: $100";
let memo = keccak256(payment_details.as_bytes());

token.transferWithMemo(recipient, amount, memo).send().await?;

Locator Pattern

Reference external data systems:
// IPFS CID (first 32 bytes)
let ipfs_cid = "QmX..."; // Your IPFS hash
let memo = B256::left_padding_from(ipfs_cid.as_bytes());

// Database ID
let db_id = "payment_12345";
let memo = B256::left_padding_from(db_id.as_bytes());

Watching for Incoming Payments

Listen for incoming TIP-20 transfers to your address:
use alloy::{
    primitives::address,
    providers::ProviderBuilder,
    rpc::types::Filter,
};
use tempo_alloy::{TempoNetwork, contracts::precompiles::ITIP20};
use futures::StreamExt;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let provider = ProviderBuilder::new_with_network::<TempoNetwork>()
        .connect(&std::env::var("RPC_URL")?)
        .await?;

    let token = ITIP20::new(
        address!("0x20c0000000000000000000000000000000000001"),
        &provider,
    );

    let my_address = address!("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb");

    // Subscribe to Transfer events where 'to' is my address
    let filter = token.Transfer_filter()
        .to_owned()
        .to(my_address);

    let mut stream = provider.subscribe_logs(&filter).await?.into_stream();

    println!("Watching for incoming payments...");

    while let Some(log) = stream.next().await {
        let transfer = token.decode_event::<ITIP20::Transfer>(&log)?;
        println!(
            "Received {} from {:?}",
            transfer.value,
            transfer.from
        );
    }

    Ok(())
}

Available Tokens

Tempo testnet provides several TIP-20 stablecoins:
TokenAddressDecimals
AlphaUSD0x20c00000000000000000000000000000000000016
BetaUSD0x20c00000000000000000000000000000000000026
GammaUSD0x20c00000000000000000000000000000000000036
DeltaUSD0x20c00000000000000000000000000000000000046
Use the faucet to get test tokens: cast rpc tempo_fundAddress <ADDRESS> --rpc-url https://rpc.moderato.tempo.xyz

Best Practices

Always include memos for business payments to enable automatic reconciliation with your accounting systems.
Never put PII or sensitive information directly in memos. Use hash commitments and store full data off-chain.
Before calling transferFrom, verify the spender has sufficient allowance.
TIP-20 tokens use 6 decimals. Always convert user-facing amounts correctly.

Next Steps

Batch Payments

Send multiple payments atomically in a single transaction

Fee Sponsorship

Pay gas fees for your users to streamline onboarding

TIP-20 Protocol

Learn more about the TIP-20 standard

Smart Accounts

Enable passkey-based payments