ThaiChain Documentation
Complete guide for interacting with ThaiChain using Foundry Cast. Learn how to query blocks, transactions, tokens, deploy contracts, and manage fees.
Download SKILL.md for AI agentsA ready-to-use skill file for vibe-coding agents (Claude Code, Cursor, etc.) — drop it into your project to give the agent full ThaiChain context.
Network Information
Table of Contents
01. Install Foundry
Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust. castis Foundry's command-line tool for interacting with EVM smart contracts, sending transactions, and getting chain data.
Install via foundryup
curl -L https://getfoundry.sh/install | bashAfter installation, restart your terminal and run:
foundryupVerify Installation
cast --version02. Gas Fee Model
Important: ThaiChain has no volatile native gas token. Fees are paid in TIP-20 stablecoins — not native ETH.
- ▸ Default fee token: TCH (
0x20C0000000000000000000000000000000000000) — each account can change its own viasetUserToken - ▸ Transaction type:
0x76(Tempo transaction envelope withfeePayerfield) - ▸ Fee AMM: built into the Fee Manager precompile — converts any USD-denominated TIP-20 token to the validator's preferred fee token (requires pool liquidity, see Add Token to Fee AMM)
- ▸ Any-token gas: Any TIP-20 token can pay for gas
- ▸ Sponsorship: a
feePayerfield allows third parties to cover fees
Because fees are paid in a stablecoin, you must hold a fee token balance (e.g. TCH) — not native ETH — to transact.
03. Setup
Common Variables
# RPC endpoint
RPC=https://rpc.thaichain.org
# Token & system addresses
TCH=0x20C0000000000000000000000000000000000000
TIP20_FACTORY=0x20fc000000000000000000000000000000000000
FEE_MANAGER=0xfeEC000000000000000000000000000000000000
STABLECOIN_DEX=0xdec0000000000000000000000000000000000000
# Private key (store as env variable)
export PRIVATE_KEY=<your_private_key>Predeployed System Contracts
| Address | Name | Purpose |
|---|---|---|
| 0x20C0...0000 | TCH | First stablecoin (default fee token) |
| 0x20fc...0000 | TIP-20 Factory | Create new TIP-20 tokens |
| 0xfeEC...0000 | Fee Manager | Fee payments, Fee AMM & fee token settings |
| 0xdec0...0000 | Stablecoin DEX | Enshrined DEX for stablecoin swaps |
| 0x403c...0000 | TIP-403 Registry | Transfer policy registry |
| 0xcA11...CA11 | Multicall3 | Batch multiple calls |
| 0xba5E...a5Ed | CreateX | Deterministic CREATE2 deployment |
| 0x0000...8ba3 | Permit2 | Token approvals & transfers |
04. TIP-20 Token Standard
TIP-20 extends ERC-20 with payment-native features:
- ▸ Pay fees in any USD-denominated TIP-20 token (via Fee AMM)
- ▸ RBAC roles:
ISSUER_ROLE(mint/burn),PAUSE_ROLE,UNPAUSE_ROLE,BURN_BLOCKED_ROLE - ▸ Transfer memos (32-byte reference attached to transfers)
- ▸ Currency declaration — token tracks an asset (e.g.
"USD") - ▸ Supply caps and pause/unpause controls
- ▸ Compliance via TIP-403 Policy Registry (whitelist/blacklist)
05. Create a TIP-20 Token
Call createToken on the TIP-20 Factory:
cast send 0x20fc000000000000000000000000000000000000 \
"createToken(string,string,string,address,address,bytes32)" \
"My USD Coin" "MUSD" "USD" \
0x20C0000000000000000000000000000000000000 \
$YOUR_ADDRESS \
0x0000000000000000000000000000000000000000000000000000000000000000 \
--rpc-url https://rpc.thaichain.org \
--private-key $PRIVATE_KEYArgs: name, symbol, currency, quoteToken, admin, salt. The quoteToken must be TCH (0x20C0...0000) — passing address(0) reverts with InvalidQuoteToken.
The factory deploys the token deterministically from salt + admin — same wallet with the same salt reverts TokenAlreadyExists; bump the salt for each new token. The caller gets all roles (issuer/pause/etc.). The new token's address is in topics[1] of the TokenCreated event (zero-padded — take the last 40 hex chars).
TOPIC=$(cast logs --address 0x20fc000000000000000000000000000000000000 \
--from-block latest \
"TokenCreated(address,string,string,string,address,address,bytes32)" \
--rpc-url https://rpc.thaichain.org \
| grep -oE '0x0{24}[0-9a-fA-F]{40}' | head -1)
TOKEN="0x${TOPIC:26}"
echo "Token address: $TOKEN"06. View Token Info
# View all token info at once
echo "Name: $(cast call $TCH 'name()(string)' --rpc-url $RPC)"
echo "Symbol: $(cast call $TCH 'symbol()(string)' --rpc-url $RPC)"
echo "Currency: $(cast call $TCH 'currency()(string)' --rpc-url $RPC)"
echo "Decimals: $(cast call $TCH 'decimals()(uint8)' --rpc-url $RPC)"
echo "Total Supply: $(cast call $TCH 'totalSupply()(uint256)' --rpc-url $RPC)"Note: TIP-20 tokens always use 6 decimals (THAICHAIN-TIP21)
07. View Balance
cast call $TCH "balanceOf(address)(uint256)" $ADMIN --rpc-url $RPCConversion Table (6 decimals)
| Amount | Raw Value |
|---|---|
| 1 TCH | 1000000 |
| 10 TCH | 10000000 |
| 100 TCH | 100000000 |
| 1,000 TCH | 1000000000 |
| 10,000 TCH | 10000000000 |
08. Mint Token
Note: You must have ISSUER_ROLE to mint tokens.
# Mint 100 TCH (100 * 10^6 = 100000000)
cast send $TCH \
"mint(address,uint256)" \
$RECIPIENT 100000000 \
--private-key $PRIVATE_KEY \
--rpc-url $RPC09. Transfer Token
# Transfer 1 TCH (1,000,000 units) to recipient
cast send $TCH \
"transfer(address,uint256)(bool)" \
$RECIPIENT 1000000 \
--private-key $PRIVATE_KEY \
--rpc-url $RPC10. Add Token to Fee AMM
To let users pay gas in your TIP-20 token, the Fee AMMneeds liquidity to convert it into the validator's fee token (TCH). The Fee AMM lives inside the Fee Manager precompile (0xfeec000000000000000000000000000000000000) — not the Stablecoin DEX (which is an orderbook for trading).
Symptom of missing liquidity: transactions from accounts whose fee token is your token revert with insufficient liquidity in FeeAMM pool to swap fee tokens — even for a simple transfer.
Step 1 — Approve both tokens for the Fee Manager
# Your token (example: 10 tokens, 6 decimals)
cast send <TOKEN_ADDRESS> "approve(address,uint256)" \
0xfeec000000000000000000000000000000000000 10000000 \
--rpc-url https://rpc.thaichain.org --private-key $PRIVATE_KEY
# TCH (validator token)
cast send 0x20C0000000000000000000000000000000000000 \
"approve(address,uint256)" \
0xfeec000000000000000000000000000000000000 10000000 \
--rpc-url https://rpc.thaichain.org --private-key $PRIVATE_KEYStep 2 — Mint Fee AMM liquidity
# mint(userToken, validatorToken, amountValidatorToken, to)
# You specify how much TCH to add; the AMM pairs it with your token (~1:1, both USD-stable).
cast send 0xfeec000000000000000000000000000000000000 \
"mint(address,address,uint256,address)" \
<TOKEN_ADDRESS> \
0x20C0000000000000000000000000000000000000 \
10000000 \
$YOUR_ADDRESS \
--rpc-url https://rpc.thaichain.org --private-key $PRIVATE_KEYStep 3 — Verify pool reserves
cast call 0xfeec000000000000000000000000000000000000 \
"getPool(address,address)(uint128,uint128)" \
<TOKEN_ADDRESS> \
0x20C0000000000000000000000000000000000000 \
--rpc-url https://rpc.thaichain.orgHow much liquidity? Fees are tiny (~0.0002 USD per tx), so a small pool goes a long way — 10 TCH covers tens of thousands of transactions. Users paying fees in your token push it into the pool and TCH flows out to validators, keeping the pool balanced automatically.
Optional: to also make your token tradeable, add orderbook liquidity on the Stablecoin DEX (createPair + place bid/ask orders) — that is a separate system from the Fee AMM.
11. Fee Manager
Fee Manager precompile is located at 0xfeEC000000000000000000000000000000000000
Fee Flow
User sends transaction
│
▼
collect_fee_pre_tx → Deduct gas fee from user
│
▼
Transaction executes
│
▼
collect_fee_post_tx → Accumulate to collected_fees[validator][token]
│
▼
distributeFees() → Transfer accumulated fees → validator addressCheck Validator Fees
# Accumulated fees
cast call $FEE_MANAGER \
"collectedFees(address,address)(uint256)" \
$VALIDATOR $TCH --rpc-url $RPC
# Validator fee token
cast call $FEE_MANAGER \
"validatorTokens(address)(address)" \
$VALIDATOR --rpc-url $RPCDistribute Fees
# Anyone can call — transfers accumulated fees to validator
cast send $FEE_MANAGER \
"distributeFees(address,address)" \
$VALIDATOR $TCH \
--private-key $PRIVATE_KEY --rpc-url $RPC12. Set Your Fee Token
Every account has a default fee token (initially TCH). If you hold a different TIP-20 stablecoin, you can switch your fee token to it — as long as the Fee AMM has liquidity for that token. This lets an account transact without ever holding TCH.
Check your current fee token
cast call 0xfeec000000000000000000000000000000000000 \
"userTokens(address)(address)" \
$YOUR_ADDRESS --rpc-url https://rpc.thaichain.orgSet your fee token
# Example: pay fees in your USD stablecoin
cast send 0xfeec000000000000000000000000000000000000 \
"setUserToken(address)" \
<TOKEN_ADDRESS> \
--rpc-url https://rpc.thaichain.org --private-key $PRIVATE_KEYNote: the setUserToken transaction itself pays fees in your current fee token. Brand-new accounts with zero fee-token balance need a sponsored first transaction (see Gas Sponsorship) or a small TCH top-up to get started.
13. Gas Sponsorship
ThaiChain supports Tempo's fee sponsorship model — a third party (fee payer) signs the feePayerSignatureso users don't need to hold fee tokens.
Client-side (viem)
import { Account, withRelay } from "viem/tempo";
const account = Account.fromSecp256k1(privateKey);
const client = createWalletClient({
account,
chain: thaichain,
transport: withRelay(
http("https://rpc.thaichain.org"),
http("https://sponsor.thaichain.org"),
{ policy: "sign-and-broadcast" },
),
});
await client.sendTransaction({
to, data, value,
feePayer: true, // ← enables sponsorship
});Self-hosted Fee Payer
Deploy the fee-payer worker (Cloudflare Worker + Hono) with:
SPONSOR_PRIVATE_KEY=<sponsor_wallet_key>
TEMPO_ENV=thaichain
TEMPO_RPC_URL=https://rpc.thaichain.org14. Deploy Smart Contracts
Deploy with forge create
forge create src/MyContract.sol:MyContract \
--rpc-url https://rpc.thaichain.org \
--private-key $PRIVATE_KEY \
--constructor-args 0x20C0000000000000000000000000000000000000 \
--gas-limit 10000000 --slowDeploy with forge script
forge script script/Deploy.s.sol \
--rpc-url https://rpc.thaichain.org \
--broadcast \
--private-key $PRIVATE_KEY \
--slow⚠️ Gas limit gotcha: forge script may run out of gas. Add --gas-limit 10000000 --skip-simulation or use forge create per contract.
UUPS Upgradeable Proxy (two-step)
# Step 1 — Deploy implementation
forge create src/MyContractV2.sol:MyContractV2 \
--rpc-url https://rpc.thaichain.org \
--private-key $PRIVATE_KEY --gas-limit 10000000
# Step 2 — Deploy proxy
INIT_DATA=$(cast abi-encode "initialize(address)" 0xADMIN_ADDRESS)
forge create @openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy \
--rpc-url https://rpc.thaichain.org \
--private-key $PRIVATE_KEY --gas-limit 5000000 \
--constructor-args 0xIMPLEMENTATION_ADDRESS $INIT_DATA15. Verify Contracts
forge verify-contract 0xYOUR_CONTRACT_ADDRESS \
src/MyContract.sol:MyContract \
--chain-id 7 \
--verifier sourcify \
--verifier-url https://contracts.thaichain.org16. Tempo Transactions (0x76)
ThaiChain uses Tempo's transaction envelope (type 0x76) which extends EIP-1559 with:
- ▸
feeToken— which TIP-20 token pays the fee - ▸
feePayerSignature— optional sponsor signature - ▸
calls[]— multiple calls in one tx (batching) - ▸
accessList,authorizationList— standard fields
JSON-RPC Methods
# Fill missing fields (nonce, gas, fee payer)
eth_fillTransaction
# Broadcast and wait for receipt
eth_sendRawTransactionSync
# Sign-only policy
eth_signRawTransaction17. Decode Data
Decode Function Selector
cast 4byte 0xa6c07924
# Output: distributeFees(address,address)Decode Calldata
cast pretty-calldata 0xa6c079240000000000000000000000005266dfa5ae013674f8fdc832b7c601b838d94ee600000000000000000000000020c000000000000000000000000000000000000018. Example Scripts
Check All Tokens
#!/bin/bash
RPC=https://rpc.thaichain.org
TOKENS=(
"0x20C0000000000000000000000000000000000000"
)
for TOKEN in "${TOKENS[@]}"; do
echo "=== Token: $TOKEN ==="
echo "Name: $(cast call $TOKEN 'name()(string)' --rpc-url $RPC)"
echo "Symbol: $(cast call $TOKEN 'symbol()(string)' --rpc-url $RPC)"
echo "Currency: $(cast call $TOKEN 'currency()(string)' --rpc-url $RPC)"
echo "Decimals: $(cast call $TOKEN 'decimals()(uint8)' --rpc-url $RPC)"
echo "Supply: $(cast call $TOKEN 'totalSupply()(uint256)' --rpc-url $RPC)"
echo ""
doneCheck Validator Status
#!/bin/bash
RPC=https://rpc.thaichain.org
FEE_MANAGER=0xfeec000000000000000000000000000000000000
TCH=0x20C0000000000000000000000000000000000000
VALIDATORS=(
"0x5266Dfa5ae013674f8FdC832b7c601B838D94eE6"
)
for V in "${VALIDATORS[@]}"; do
echo "=== Validator: $V ==="
echo "Fee Token: $(cast call $FEE_MANAGER 'validatorTokens(address)(address)' $V --rpc-url $RPC)"
echo "Collected Fees: $(cast call $FEE_MANAGER 'collectedFees(address,address)(uint256)' $V $TCH --rpc-url $RPC)"
echo ""
done19. Use Case: Issue a Stablecoin
End-to-end: create a TIP-20 token, enable it as a gas fee token, and mint supply — the exact flow used to launch wrapped USDC on ThaiChain. Examples sign with --ledger; replace it with --private-key $PRIVATE_KEYif you don't use a Ledger.
Step 1 — Create the token via the TIP-20 Factory
cast send 0x20fc000000000000000000000000000000000000 \
"createToken(string,string,string,address,address,bytes32)" \
"J Point" "JPOINT" "USD" \
0x20C0000000000000000000000000000000000000 \
0xYOUR_WALLET \
0x0000000000000000000000000000000000000000000000000000000000000002 \
--rpc-url https://rpc.thaichain.org --ledgerquoteToken must be TCH (address(0) reverts). The salt must be unique per wallet — reusing one reverts TokenAlreadyExists.
Step 2 — Get the token address
Open the transaction on https://exp.thaichain.org — the new token address is in the TokenCreated event. Then export it:
export TOKEN=0x... # your new token addressStep 3 — Grant the Minter role
The factory does not always auto-assign ISSUER_ROLE. Grant it to your wallet (required before minting):
cast send $TOKEN \
"grantRole(bytes32,address)" \
0x114e74f6ea3bd819998f78687bfcb11b140da08e9b7d222fa9c1f1ba1f2aa122 \
0xYOUR_WALLET \
--rpc-url https://rpc.thaichain.org --ledgerStep 4 — Enable the token for gas fees (Fee AMM)
Seed the Fee AMM with TCH so fees paid in your token can be converted for validators. First approve TCH for the Fee Manager:
cast send 0x20C0000000000000000000000000000000000000 \
"approve(address,uint256)" \
0xfeec000000000000000000000000000000000000 10000000 \
--rpc-url https://rpc.thaichain.org --ledgerThen allow your token to be swapped into TCH for fees by minting Fee AMM liquidity:
cast send 0xfeec000000000000000000000000000000000000 \
"mint(address,address,uint256,address)" \
$TOKEN \
0x20C0000000000000000000000000000000000000 \
10000000 \
0xYOUR_WALLET \
--rpc-url https://rpc.thaichain.org --ledgerOnly TCH is taken — your token's side of the pool starts at zero and fills automatically as users pay fees in it. 10 TCH covers tens of thousands of transactions.
Step 5 — Mint your token
# Mint 1,000 tokens (6 decimals → 1000 * 10^6)
cast send $TOKEN "mint(address,uint256)" \
0xYOUR_WALLET 1000000000 \
--rpc-url https://rpc.thaichain.org --ledgerResult: any account holding only your token can now transact — fees (~$0.0002/tx) are charged in your token and auto-converted through the Fee AMM. To also make the token tradeable, add orderbook liquidity on the Stablecoin DEX (separate system).
20. Useful Resources
Building with an AI agent?
Download the SKILL.md file and add it to your project. It gives Claude Code, Cursor, and other vibe-coding agents full context about ThaiChain — chain config, gas model, system contracts, and all the commands above.
Download SKILL.md