All articles
solanastablecoinstoken-2022spl-token

Stablecoins on Solana: mint registry, decimals, and Token-2022 extensions

Every major Solana stablecoin — mint addresses, decimals, token program, and the Token-2022 extensions they ship. With code to detect each at runtime.

Every Solana stablecoin looks the same in your wallet — a token balance, a transfer, a swap. Under the hood, they diverge wildly: different token programs, different decimals, different Token-2022 extensions. If you're integrating against more than one, the differences matter the first time you hit a hook that rejects your transfer or a confidential balance you can't read.

This is the technical sheet: which mint is which, what program owns it, what extensions it carries, and code to detect everything at runtime.

The mints

The canonical mint addresses for the major Solana stablecoins:

typescript
// SPL Token (TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA)
const USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"  // 6 decimals
const USDT = "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"  // 6 decimals
const FDUSD = "9zNQRsGLjNKwCUU5Gq5LR8beUCPzQMVMqKAi3SSZh54u" // 6 decimals (verify)

// Token-2022 (TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb)
const PYUSD = "2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo" // 6 decimals
const USDG  = "2u1tszSeqZ3qBWF3uNGPFc8TzMk2tdiwknnRMWGWjGWH" // 6 decimals (verify)
const USDY  = "A1KLoBrKBde8Ty9qtNQUtq3C2ortoC3u7twggz7sEto6" // 6 decimals
const AUSD  = "AUSD1jCcaVrbfDNQyhrZkjnB4xPPquYAR9TgRgkkw5dT" // verify mint
const MXNB  = "MXNeFcZjVe1q1KGZFNZBJVZpe6QFEhNGvL3uHpKjDfx"  // 6 decimals (verify)

Verify these against the issuer's documentation before production use — issuers occasionally migrate mints and the ecosystem catalog lags. Authoritative sources: Circle (USDC), stablesonsolana.com, and each issuer's docs.

Detecting which program owns a mint

A mint's owner tells you whether to use SPL Token or Token-2022 program IDs in your instructions:

typescript
import { Connection, PublicKey } from "@solana/web3.js"
import { TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID } from "@solana/spl-token"

async function detectTokenProgram(conn: Connection, mint: PublicKey) {
  const info = await conn.getAccountInfo(mint)
  if (!info) throw new Error("mint not found")

  if (info.owner.equals(TOKEN_PROGRAM_ID))      return "spl-token"
  if (info.owner.equals(TOKEN_2022_PROGRAM_ID)) return "token-2022"
  throw new Error("not a recognised token mint")
}

Always do this dynamically. A new stablecoin landing on Solana in 2026 is almost certainly Token-2022; older ones may still be SPL Token. Hard-coding the program ID is a bug waiting to happen.

Enumerating extensions on a Token-2022 mint

Token-2022 mints append extension bytes after the base mint data. The SDK gives you a typed view:

typescript
import {
  getMint,
  getExtensionTypes,
  ExtensionType,
  TOKEN_2022_PROGRAM_ID,
} from "@solana/spl-token"

const mint = await getMint(conn, PYUSD, "confirmed", TOKEN_2022_PROGRAM_ID)
const extensions = getExtensionTypes(mint.tlvData)

// extensions might include:
//   ExtensionType.MetadataPointer
//   ExtensionType.TokenMetadata
//   ExtensionType.PermanentDelegate
//   ExtensionType.DefaultAccountState
//   ExtensionType.TransferHook
//   ExtensionType.MintCloseAuthority
//   ExtensionType.ConfidentialTransferMint
//   ExtensionType.InterestBearingConfig
//   ExtensionType.GroupPointer / GroupMemberPointer
//   ExtensionType.TransferFeeConfig

for (const ext of extensions) {
  console.log(ExtensionType[ext], "→ enabled on this mint")
}

What each major stablecoin actually uses

Common extension shapes you'll see, by issuer pattern:

USDC, USDT, FDUSD (SPL Token): no extensions — these predate Token-2022 and stayed on the legacy program. Plain mint authority + freeze authority, both held by the issuer.

PYUSD, USDG (Token-2022, regulated USD): typically MetadataPointer + TokenMetadata (on-mint metadata, no Metaplex hop) + PermanentDelegate (compliance forced-transfer) + DefaultAccountState: Frozen + freeze authority for KYC gating. Some also wire TransferHook for live KYC attestation.

USDY (Token-2022, yieldcoin): InterestBearingConfig for the auto-accruing balance, plus the same compliance extensions as PYUSD-class stables.

AUSD (Token-2022, revenue-share): similar compliance shape to PYUSD plus a TransferHook Ondo uses for distribution-partner revenue attribution.

MXNB and other local-currency stables: match the PYUSD shape but commission-aware TransferFeeConfig is increasingly common on emerging-market stables.

Transferring a Token-2022 stablecoin with extensions

Use the extensions-aware helper rather than the legacy transfer. The helper handles TransferHook account resolution and TransferFee math automatically:

typescript
import {
  transferCheckedWithFee,
  transferCheckedWithTransferHook,
  TOKEN_2022_PROGRAM_ID,
} from "@solana/spl-token"

// For a TransferFee mint:
const sig = await transferCheckedWithFee(
  conn, payer, source, mint, destination, owner,
  /* amount */     1_000_000n,         // 1 PYUSD (6 decimals)
  /* decimals */   6,
  /* fee */        await calculateFee(conn, mint, 1_000_000n),
  [], { commitment: "confirmed" },
  TOKEN_2022_PROGRAM_ID,
)

// For a TransferHook mint (resolves hook program + extra accounts):
const sig2 = await transferCheckedWithTransferHook(
  conn, payer, source, mint, destination, owner,
  1_000_000n, 6, [],
  { commitment: "confirmed" },
  TOKEN_2022_PROGRAM_ID,
)

Calling the wrong helper (legacy transfer against a PYUSD mint with a hook) will fail at the validator with Custom program error: 0x1 or a hook-specific code, depending on the extension. Always branch on detected extensions.

Reading a holder's balance the right way

For an interest-bearing stable (USDY), the raw token account balance is the principal, not the displayed amount:

typescript
import { amountToUiAmountForMintWithoutSimulation } from "@solana/spl-token"

// Wrong: shows principal, ignores accrued interest
const raw = await conn.getTokenAccountBalance(tokenAccount)
console.log(raw.value.uiAmountString)

// Right: queries the mint's interest config and renders accrued balance
const displayAmount = await amountToUiAmountForMintWithoutSimulation(
  conn, USDY, BigInt(raw.value.amount),
)
console.log(displayAmount)  // e.g. "100.0421" instead of "100.0000"

Reference table

text
Stable  | Program     | Decimals | Likely extensions
--------|-------------|----------|----------------------------------------------
USDC    | SPL Token   | 6        | (none) — mint+freeze authority only
USDT    | SPL Token   | 6        | (none)
FDUSD   | SPL Token   | 6        | (none)
PYUSD   | Token-2022  | 6        | MetadataPointer, TokenMetadata,
        |             |          | PermanentDelegate, DefaultAccountState (Frozen)
USDG    | Token-2022  | 6        | similar to PYUSD
USDY    | Token-2022  | 6        | InterestBearingConfig + compliance shape
AUSD    | Token-2022  | 6        | TransferHook + compliance shape
MXNB    | Token-2022  | 6        | compliance + sometimes TransferFeeConfig

References

Integration heuristic: detect the program at runtime, enumerate extensions, branch on the ones that change semantics. Everything else is plumbing.

Stablecoins on Solana: mint registry, decimals, and Token-2022 extensions | devrels.xyz