If you build anything that touches payments, a wallet, or a trading bot, you will eventually see code like if token in ("USDT", "USDC"): value = amount. Both read $1.00 on a screen, so it is tempting to treat them as the same dollar. They are not. They come from different issuers, hold different reserves, follow different rules, and on-chain they are separate contracts with their own admin powers.
This post is the developer checklist: what actually differs, and a small Python script that reads both contracts directly. For the full side-by-side (issuers, reserve breakdowns, ratings, chains), see NutshellCrypto's USDT vs USDC comparison of reserves, rules, and risks.
Two issuers, two balance sheets
USDT is issued by Tether, launched in 2014, and is the bigger coin: about $183.5B market cap versus about $72.6B for USDC, per CoinGecko on October 16, 2026. USDC is issued by Circle, whose parent is listed on the NYSE.
The backing is where they split:
- USDT reported $187.75B in reserves against $183.64B in liabilities on June 30, 2026. Most of it is Treasury bills and repos, but it also holds gold, bitcoin, and secured loans. Checks are quarterly attestations.
- USDC reported $73.40B in reserves for $73.32B in circulation on August 31, 2026, almost all in a BlackRock-managed government money market fund plus bank cash. Checks are monthly.
So USDT has a bigger cushion made partly of volatile assets; USDC has a thin cushion made of very liquid ones. If your app holds large balances for users, that difference belongs in your risk notes, not just your UI.
Don't hardcode 1.00
Both coins have broken the peg briefly. USDT fell to about $0.92 in October 2018 and around $0.95 during the TerraUSD collapse in May 2022. USDC dropped to about $0.88 in March 2023 when Circle had reserves stuck at Silicon Valley Bank. Each recovered, but a bot or accounting system that assumed exactly $1 would have mispriced positions during those windows.
Practical rule: price stablecoins from a feed like any other asset, and alert when the price drifts outside a band you choose.
Same ticker, different contracts per chain
USDT and USDC both live on many networks, and every version is a separate token contract. USDT is mostly on Tron and Ethereum; USDC is mostly on Ethereum, with Circle listing native USDC on dozens of chains. Sending USDT on Tron to an address that only watches Ethereum is a classic way to lose funds.
For developers that means:
- Key your token registry by
(chain, contract_address), never by ticker. - Read
decimals()from each contract instead of assuming. Both use 6 on Ethereum, but bridged or chain-specific versions don't have to match. - Show users the network clearly before they send. NutshellCrypto's guide to wallet addresses and networks covers the user side of this.
Both can freeze your tokens
This surprises people who assume self-custody means nobody can touch their balance. Both issuers can blacklist addresses at the contract level, usually for law enforcement or sanctions requests. Your keys still control the address, but the token contract can refuse to move the balance.
You can check this yourself. The script below calls the Ethereum mainnet contracts over plain JSON-RPC (standard library only) and reads decimals, total supply on that chain, and whether a given address is blacklisted. Note the two contracts even spell the function differently: USDT uses isBlackListed, USDC uses isBlacklisted.
import json, urllib.request
RPC = "https://ethereum-rpc.publicnode.com"
TOKENS = {
"USDT": ("0xdAC17F958D2ee523a2206206994597C13D831ec7", "0xe47d6060"), # isBlackListed(address)
"USDC": ("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "0xfe575a87"), # isBlacklisted(address)
}
DECIMALS, TOTAL_SUPPLY = "0x313ce567", "0x18160ddd"
def eth_call(to, data):
body = json.dumps({"jsonrpc": "2.0", "id": 1, "method": "eth_call",
"params": [{"to": to, "data": data}, "latest"]}).encode()
req = urllib.request.Request(RPC, body, {"Content-Type": "application/json",
"User-Agent": "stable-check/0.1"})
with urllib.request.urlopen(req, timeout=15) as r:
return int(json.load(r)["result"], 16)
def check(wallet):
arg = wallet.lower().removeprefix("0x").rjust(64, "0")
for name, (token, blacklist_sel) in TOKENS.items():
dec = eth_call(token, DECIMALS)
supply = eth_call(token, TOTAL_SUPPLY) / 10**dec
frozen = bool(eth_call(token, blacklist_sel + arg))
print(f"{name}: decimals={dec} supply on Ethereum={supply:,.0f} frozen={frozen}")
check("0x000000000000000000000000000000000000dEaD")
Swap in any address you want to inspect. Public RPCs rate-limit, so use your own node or provider for anything beyond a quick check.
Rules decide what you can list
Regulation now shapes which coin a product can offer. USDC is an authorized e-money token under the EU's MiCA rules, and major exchanges restricted USDT trading for EU users. Canada's registered platforms offer USDC but not USDT. In the US, the GENIUS Act sets up licensing for stablecoin issuers, and Tether's US-regulated option is a separate token, USAT. If you ship to multiple regions, your supported-asset list may need to vary by jurisdiction.
Takeaways
- Treat USDT and USDC as different assets with different issuers and reserve profiles.
- Never hardcode a $1 price; monitor drift like any other asset.
- Identify tokens by chain and contract address, and read
decimals()per contract. - Both issuers can freeze addresses at the contract level, so check before you rely on a balance.
- Regional rules (MiCA, Canada, the GENIUS Act) can decide which coin you're allowed to offer.
New to the basics? NutshellCrypto's explainer on what stablecoins are and how the peg works is a good starting point.
Read the full guide on NutshellCrypto
This is for education only and is not financial or investment advice.
This post was written with AI assistance.
Top comments (0)