Skip to main content
On Stable, USDT0 functions both as the chain’s native asset and as an ERC-20 token. This means approve, transferFrom, and permit remain fully available alongside standard value transfers. This guide walks you through sending USDT0 via both methods and verifying that both operate on the same balance.
  • Concept: For background on what USDT0 is and how Stable enables it as the gas token, see USDT as Gas.
  • Behavior: For details on balance reconciliation and contract design requirements, see USDT0 Behavior on Stable.

Prerequisites

All you need to send USDT0 is USDT0 itself. No extra tokens are required. USDT0 contract addresses
  • Mainnet: 0x779ded0c9e1022225f8e0630b35a9b54be713736
  • Testnet: 0x78cf24370174180738c5b8e352b6d14c83a6c9a9

Setup

// config.ts
import { ethers } from "ethers";

export const STABLE_TESTNET_RPC = "https://rpc.testnet.stable.xyz";
export const CHAIN_ID = 2201;
export const USDT0_ADDRESS = "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9";
export const provider = new ethers.JsonRpcProvider(STABLE_TESTNET_RPC);
export const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
Native transfers work the same as sending ETH on Ethereum. The value field represents the USDT0 amount. A native transfer costs only 21,000 gas — the cheapest way to send USDT0.
// sendNative.ts
import { ethers } from "ethers";
import { wallet, provider } from "./config";

const recipient = "0xRecipientAddress";
const amount = ethers.parseUnits("0.001", 18);

const block = await provider.getBlock("latest");
const baseFee = block!.baseFeePerGas!;

const tx = await wallet.sendTransaction({
  to: recipient,
  value: amount,
  maxFeePerGas: baseFee * 2n,
  maxPriorityFeePerGas: 0n, // always 0 on Stable
});

const receipt = await tx.wait(1);
console.log("Native transfer tx:", receipt!.hash);
Native transfers use 18 decimals (standard EVM precision). Use ethers.parseUnits("0.001", 18) for native transfers.

Option 2: send as ERC-20 transfer

USDT0 can also be sent as an ERC-20 transfer. This deducts from the same underlying balance.
// sendERC20.ts
import { ethers } from "ethers";
import { wallet, USDT0_ADDRESS } from "./config";

const recipient = "0xRecipientAddress";
const amount = ethers.parseUnits("0.001", 6); // USDT0 ERC-20 uses 6 decimals

const usdt0 = new ethers.Contract(USDT0_ADDRESS, [
  "function transfer(address to, uint256 amount) returns (bool)"
], wallet);

const tx = await usdt0.transfer(recipient, amount);
const receipt = await tx.wait(1);
console.log("ERC-20 transfer tx:", receipt!.hash);
ERC-20 transfers use 6 decimals (standard USDT precision). Use ethers.parseUnits("0.001", 6) for ERC-20 transfers.

Verify the balance

After either transfer, check both balances to confirm they draw from the same source:
import { ethers } from "ethers";
import { provider, wallet, USDT0_ADDRESS } from "./config";

// Native balance (18 decimals)
const nativeBalance = await provider.getBalance(wallet.address);
console.log("Native balance:", ethers.formatEther(nativeBalance), "USDT0");

// ERC-20 balance (6 decimals)
const usdt0 = new ethers.Contract(USDT0_ADDRESS, [
  "function balanceOf(address) view returns (uint256)"
], provider);
const erc20Balance = await usdt0.balanceOf(wallet.address);
console.log("ERC-20 balance:", ethers.formatUnits(erc20Balance, 6), "USDT0");
Both values represent the same balance. The native balance has 18 decimals of precision while the ERC-20 balance has 6 decimals, so they may differ by up to 0.000001 USDT0 due to fractional balance reconciliation. See also:
Last modified on April 16, 2026