stellar/stellar-dev-skill

assets

Stellar Assets (classic) + trustlines + Stellar Asset Contract (SAC) bridge to smart contracts.

View source
Original skill document

Rendered from the source repository. Headings, examples, code, tables, links, and referenced images are preserved.

Stellar Assets, Trustlines, and SAC

Stellar's native token mechanism: classic asset issuance, trustlines, and the Stellar Asset Contract (SAC) bridge that makes classic assets usable from smart contracts. Default to classic assets over custom contract tokens unless you need custom logic.

When to use this skill

  • Issuing a new asset (stablecoin, security token, utility token)
  • Setting up trustlines from a client or contract
  • Managing issuer flags (auth required, auth revocable, clawback)
  • Bridging a classic asset into a smart contract via SAC
  • Building regulated-asset flows (compliance, KYC, freeze)

Related skills

  • Custom token contracts (when classic isn't enough) → ../smart-contracts/SKILL.md
  • UI flows for trustline creation and asset display → ../dapp/SKILL.md
  • Looking up balances and trustline state → ../data/SKILL.md
  • Token-related SEPs (SEP-41, SEP-7, etc.) → ../standards/SKILL.md

Overview

Stellar has two token mechanisms:

  1. Stellar Assets (Classic): Built-in, highly efficient, full ecosystem support
  2. Contract tokens (SEP-41): Custom contracts with flexible logic

Recommendation: Prefer Stellar Assets unless you need custom token logic.

Stellar Assets (Classic)

Asset Types

TypeDescription
Native (XLM)Stellar's native currency, no trustline needed
CreditIssued by an account, requires trustline
Liquidity Pool SharesRepresent LP positions

Asset Identifiers

typescript
import * as StellarSdk from "@stellar/stellar-sdk";

// Native XLM
const xlm = StellarSdk.Asset.native();

// Credit asset (code + issuer)
const usdc = new StellarSdk.Asset(
  "USDC",
  "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN"
);

// Asset code rules:
// - 1-4 chars: alphanumeric (credit_alphanum4)
// - 5-12 chars: alphanumeric (credit_alphanum12)

Issuing Assets

Create Issuing Account

typescript
import * as StellarSdk from "@stellar/stellar-sdk";

const server = new StellarSdk.Horizon.Server("https://horizon-testnet.stellar.org");

// 1. Create issuing account (should be separate from distribution)
const issuerKeypair = StellarSdk.Keypair.random();
const distributorKeypair = StellarSdk.Keypair.random();

// 2. Fund accounts (testnet)
await fetch(`https://friendbot.stellar.org?addr=${issuerKeypair.publicKey()}`);
await fetch(`https://friendbot.stellar.org?addr=${distributorKeypair.publicKey()}`);

Issue Asset

typescript
const asset = new StellarSdk.Asset("MYTOKEN", issuerKeypair.publicKey());

// 1. Distributor creates trustline to issuer
const distributorAccount = await server.loadAccount(distributorKeypair.publicKey());

const trustlineTx = new StellarSdk.TransactionBuilder(distributorAccount, {
  fee: StellarSdk.BASE_FEE,
  networkPassphrase: StellarSdk.Networks.TESTNET,
})
  .addOperation(
    StellarSdk.Operation.changeTrust({
      asset: asset,
      limit: "1000000", // Max amount to hold
    })
  )
  .setTimeout(180)
  .build();

trustlineTx.sign(distributorKeypair);
await server.submitTransaction(trustlineTx);

// 2. Issuer sends tokens to distributor
const issuerAccount = await server.loadAccount(issuerKeypair.publicKey());

const issueTx = new StellarSdk.TransactionBuilder(issuerAccount, {
  fee: StellarSdk.BASE_FEE,
  networkPassphrase: StellarSdk.Networks.TESTNET,
})
  .addOperation(
    StellarSdk.Operation.payment({
      destination: distributorKeypair.publicKey(),
      asset: asset,
      amount: "1000000",
    })
  )
  .setTimeout(180)
  .build();

issueTx.sign(issuerKeypair);
await server.submitTransaction(issueTx);

Lock Issuing Account

For fixed-supply tokens, lock the issuer:

typescript
const lockTx = new StellarSdk.TransactionBuilder(issuerAccount, {
  fee: StellarSdk.BASE_FEE,
  networkPassphrase: StellarSdk.Networks.TESTNET,
})
  .addOperation(
    StellarSdk.Operation.setOptions({
      masterWeight: 0, // Disable master key
    })
  )
  .setTimeout(180)
  .build();

lockTx.sign(issuerKeypair);
await server.submitTransaction(lockTx);
// Issuer can never issue more tokens

Asset Flags

Configure issuer account flags for compliance:

typescript
const setFlagsTx = new StellarSdk.TransactionBuilder(issuerAccount, {
  fee: StellarSdk.BASE_FEE,
  networkPassphrase: StellarSdk.Networks.TESTNET,
})
  .addOperation(
    StellarSdk.Operation.setOptions({
      setFlags:
        StellarSdk.AuthRequiredFlag |    // Trustlines require approval
        StellarSdk.AuthRevocableFlag |   // Can freeze trustlines
        StellarSdk.AuthClawbackEnabledFlag, // Can clawback tokens
    })
  )
  .setTimeout(180)
  .build();

Flag Descriptions

FlagEffect
AUTH_REQUIREDUsers must get approval before receiving tokens
AUTH_REVOCABLEIssuer can freeze user balances
AUTH_IMMUTABLEFlags cannot be changed (permanent)
AUTH_CLAWBACK_ENABLEDIssuer can clawback tokens from accounts

Authorize Trustline

typescript
// When AUTH_REQUIRED is set, approve trustlines:
const authorizeTx = new StellarSdk.TransactionBuilder(issuerAccount, {
  fee: StellarSdk.BASE_FEE,
  networkPassphrase: StellarSdk.Networks.TESTNET,
})
  .addOperation(
    StellarSdk.Operation.setTrustLineFlags({
      trustor: userPublicKey,
      asset: asset,
      flags: {
        authorized: true,
        // authorizedToMaintainLiabilities: true, // Partial auth
      },
    })
  )
  .setTimeout(180)
  .build();

Clawback Tokens

typescript
// Requires AUTH_CLAWBACK_ENABLED flag
const clawbackTx = new StellarSdk.TransactionBuilder(issuerAccount, {
  fee: StellarSdk.BASE_FEE,
  networkPassphrase: StellarSdk.Networks.TESTNET,
})
  .addOperation(
    StellarSdk.Operation.clawback({
      asset: asset,
      from: targetAccountId,
      amount: "100",
    })
  )
  .setTimeout(180)
  .build();

Trustlines

Create Trustline

typescript
const changeTrustTx = new StellarSdk.TransactionBuilder(userAccount, {
  fee: StellarSdk.BASE_FEE,
  networkPassphrase: StellarSdk.Networks.TESTNET,
})
  .addOperation(
    StellarSdk.Operation.changeTrust({
      asset: asset,
      limit: "10000", // Max amount to hold (see "Remove Trustline" for limit: "0")
    })
  )
  .setTimeout(180)
  .build();

Remove Trustline

ChangeTrust with limit: "0" deletes a trustline, but only if the trustline is in a deletable state. Before submitting:

  1. Identify the asset by code AND issuer. A wallet-display entry or a

claimable balance is not a trustline — only an actual classic asset trustline (or liquidity pool share) on the account can be removed with ChangeTrust. Claimable balances are removed by claiming or clawing them back, not by ChangeTrust.

  1. Clear the balance. The trustline balance must be exactly 0 — send

remaining funds back to the issuer (burns them) or to another account.

  1. Clear offers / buying liabilities. Open DEX offers that buy the asset

create buying liabilities; cancel them before removal (manageSellOffer with amount: "0", manageBuyOffer with buyAmount: "0").

  1. Exit liquidity pool positions. An asset trustline referenced by a

liquidity pool (liquidity_pool_use_count > 0, per CAP-0038) cannot be deleted — withdraw from the pool and remove the pool-share trustline first.

typescript
// 1. Check the trustline is deletable
const account = await server.loadAccount(userPublicKey);
const trustline = account.balances.find(
  (b) =>
    b.asset_type !== "native" &&
    b.asset_type !== "liquidity_pool_shares" &&
    b.asset_code === asset.getCode() &&
    b.asset_issuer === asset.getIssuer()
);

if (!trustline) throw new Error("No trustline (display/claimable state only?)");
if (parseFloat(trustline.balance) !== 0)
  throw new Error("Balance must be 0 — send funds away or back to issuer");
if (parseFloat(trustline.buying_liabilities) !== 0)
  throw new Error("Cancel open offers buying this asset first");

// Liquidity-pool usage (precondition 4) is not visible on this balance line —
// if the asset is still in one of the account's pools, the submit below fails
// with op_cannot_delete. Withdraw and remove the pool-share trustline first.

// 2. Submit removal and surface the specific result code
const removeTrustTx = new StellarSdk.TransactionBuilder(account, {
  fee: StellarSdk.BASE_FEE,
  networkPassphrase: StellarSdk.Networks.TESTNET,
})
  .addOperation(
    StellarSdk.Operation.changeTrust({
      asset: asset,
      limit: "0", // Delete the trustline
    })
  )
  .setTimeout(180)
  .build();

removeTrustTx.sign(userKeypair);

try {
  await server.submitTransaction(removeTrustTx);
} catch (e) {
  const codes = e.response?.data?.extras?.result_codes?.operations ?? [];
  if (codes.includes("op_invalid_limit")) {
    // CHANGE_TRUST_INVALID_LIMIT: balance or buying liabilities remain
    throw new Error("Clear balance and open offers before removing trustline");
  }
  if (codes.includes("op_cannot_delete")) {
    // CHANGE_TRUST_CANNOT_DELETE: trustline is used by a liquidity pool
    throw new Error("Withdraw from liquidity pools referencing this asset first");
  }
  throw e;
}

Check Trustline Status

typescript
const account = await server.loadAccount(userPublicKey);
const trustline = account.balances.find(
  (b) =>
    b.asset_type !== "native" &&
    b.asset_code === "USDC" &&
    b.asset_issuer === usdcIssuer
);

if (trustline) {
  console.log("Balance:", trustline.balance);
  console.log("Limit:", trustline.limit);
  console.log("Authorized:", trustline.is_authorized);
}

Stellar Asset Contract (SAC)

SAC provides a smart-contract interface for Stellar Assets, enabling smart contract interactions.

Deploy SAC for Existing Asset

bash
# Get the SAC address for an asset
stellar contract asset deploy \
  --asset USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN \
  --source-account alice \
  --network testnet

SAC Address Derivation

typescript
import * as StellarSdk from "@stellar/stellar-sdk";

const asset = new StellarSdk.Asset("USDC", issuerPublicKey);
const contractId = asset.contractId(StellarSdk.Networks.TESTNET);
// Returns the deterministic SAC contract address

Using SAC in Smart Contracts

rust
use soroban_sdk::{token::Client as TokenClient, Address, Env};

pub fn transfer_asset(
    env: Env,
    from: Address,
    to: Address,
    asset_contract: Address,
    amount: i128,
) {
    from.require_auth();

    // Use standard token interface
    let token = TokenClient::new(&env, &asset_contract);
    token.transfer(&from, &to, &amount);
}

SAC vs Custom Token Interface

SAC implements the standard SEP-41 token interface:

  • balance(id: Address) -> i128
  • transfer(from: Address, to: Address, amount: i128)
  • approve(from: Address, spender: Address, amount: i128, expiration_ledger: u32)
  • allowance(from: Address, spender: Address) -> i128
  • decimals() -> u32
  • name() -> String
  • symbol() -> Symbol

When to Use What

Use Stellar Assets When:

  • Standard fungible token (currency, stablecoin)
  • Need full ecosystem support (wallets, exchanges)
  • Regulatory compliance features (freeze, clawback)
  • Performance critical (classic operations are cheaper)
  • DEX integration via order book

Use Custom Contract Tokens When:

  • Complex transfer logic (royalties, fees, restrictions)
  • Custom authorization schemes
  • Non-standard token behaviors
  • Integration with custom DeFi contracts
  • NFTs or semi-fungible tokens
Reach for a fully custom token as a last resort. Many of the cases above (royalties, fees, transfer restrictions) can be built with a regular issued Stellar asset plus a thin "SAC admin" contract that drives the SAC's mint, burn, and clawback functions. That keeps the ecosystem compatibility of a normal asset (DEX, anchors, wallets, trustlines) while still layering on custom logic. Prefer a custom contract token only when you need token behavior the SAC genuinely cannot express. This runs in production today. USDT0 (USDT0:GATISXX6BZ6NC7IKQBY37CJD4SOZL3CYZJWXEDG6JVIY4WBS6KXJHN6Q) is a classic asset whose SAC admin is a contract: a role-gated manager that forwards mint, clawback, set_authorized, and set_admin to the SAC, with a cross-chain bridge holding only the minter role. See setting a custom SAC admin for the pattern and ../cross-chain/layerzero.md for that deployment. One hard prerequisite: lock the issuer (master key weight 0). Payments from a classic issuer are minting, so an unlocked issuer can mint outside the contract and bypass the role model entirely. Do it in this order: `set_admin` first, then lock the issuer. A SAC's admin starts as the issuer account, and only the current admin can authorize the first set_admin. Lock the issuer before that call and the admin stays the locked issuer forever, because nobody can sign the handover (SAC admin guide). After the handover the SAC still mints under the new admin, so lock the issuer then.

Use SAC When:

  • Need a Stellar asset inside a smart contract
  • Building DeFi protocols with existing assets
  • Bridge between classic and smart contract operations

Querying Assets

Get Account Balances

typescript
const account = await server.loadAccount(publicKey);

for (const balance of account.balances) {
  if (balance.asset_type === "native") {
    console.log("XLM:", balance.balance);
  } else {
    console.log(`${balance.asset_code}:`, balance.balance);
  }
}

Find Assets

typescript
// Search for assets by code
const assets = await server
  .assets()
  .forCode("USDC")
  .call();

// Get specific asset details
const assetDetails = await server
  .assets()
  .forCode("USDC")
  .forIssuer(issuerPublicKey)
  .call();

Get Asset Statistics

typescript
const stats = await server
  .assets()
  .forCode("USDC")
  .forIssuer(issuerPublicKey)
  .call();

// stats includes:
// - amount: total issued
// - num_accounts: trustline count
// - flags: issuer flags

SEP Standards for Assets

SEP-0001 (stellar.toml)

Publish asset metadata in your domain's /.well-known/stellar.toml:

toml
[[CURRENCIES]]
code = "MYTOKEN"
issuer = "GABC..."
display_decimals = 2
name = "My Token"
desc = "A description of my token"
image = "https://example.com/token-logo.png"

SEP-0010 (Web Authentication)

Authenticate users with their Stellar accounts. Flow: the server generates a challenge transaction, the client signs it with their wallet, and the server verifies the signature. See SEP-0010.

SEP-0024 (Hosted Deposit/Withdrawal)

For fiat on/off ramps: an interactive webview flow where the anchor handles KYC and the fiat transfer. See SEP-0024.

SEP-0045 (Web Auth for Contract Accounts)

Extends SEP-10 to support contract accounts (C... addresses) for web authentication. Required for smart wallet / passkey-based anchor integrations. Draft status; verify current status in SEP-0045.

SEP-0050 (Non-Fungible Tokens)

Standard contract interface for NFTs on Stellar. Reference implementations available in OpenZeppelin Stellar Contracts with Base, Consecutive, and Enumerable variants. Draft status; verify current status in SEP-0050.

Best Practices

Asset Issuance

  • Use separate issuing and distribution accounts
  • Lock issuer after initial distribution for fixed supply
  • Publish stellar.toml with asset metadata
  • Consider multisig for issuer account

Trustline Management

  • Check trustline exists before sending payments
  • Handle trustline creation in onboarding flow
  • Respect trustline limits
  • Monitor for frozen/deauthorized status
  • Before removing a trustline (limit: "0"): zero the balance, cancel offers

buying the asset, and exit liquidity pools that reference it

Security

  • Validate asset issuer, not just code
  • Be cautious of assets with clawback enabled
  • Verify stellar.toml from authoritative source
  • Use well-known asset lists for common tokens
  • Some live assets have no `stellar.toml` at all. A missing home_domain

is not evidence of a scam — USDT0 ships without one. Validate by issuer plus SAC derivation, never by the presence of home_domain or a [[CURRENCIES]] entry

  • **When AUTH_REVOCABLE and AUTH_CLAWBACK_ENABLED are both set, check the

issuer lock and the admin roles before listing the asset.** Those flags mean balances can be frozen or clawed back. A contract SAC admin does not contain that power on its own: an issuer whose master key still signs can mint, freeze and claw back directly, whatever the admin contract allows. So confirm the master key weight is 0, then identify who holds the admin contract's roles

from this repository

More skills

All skills
stellar
Community

agentic-payments

Agentic and machine-to-machine payments on Stellar. Covers x402 (HTTP 402 paid APIs via OZ Channels facilitator, fee-sponsored clients) and MPP (Machine Payments Protocol) in both Charge mode (per-request SAC) and Session mode (channel-backed off-chain commits, high-frequency; formerly called Channel mode). Defaults to USDC (SEP-41 SAC) on stellar:testnet/stellar:pubnet (CAIP-2). Use when selling a paid API to AI agents, building an x402 client, or designing a payment-channel architecture for high-frequency agent traffic.

installs
2
GitHub stars
51
Updated
Sep 5
stellar
Community

cross-chain

Cross-chain interoperability for Stellar. Entry point with a rail-selection decision table and shared pitfalls, routing to three companion files — cctp.md (Circle CCTP V2, native USDC burn-and-mint between Stellar and EVM/Solana chains, domain 27, the CctpForwarder requirement for Stellar recipients), axelar.md (Axelar GMP for Soroban contracts calling contracts on other chains, and the Interchain Token Service for multichain tokens), and layerzero.md (LayerZero V2 OApp messaging with configurable DVN security, OFT omnichain tokens, and USDT0 — native USDT on Stellar). Also covers NEAR Intents (intent-based cross-chain swaps into XLM or Stellar USDC) at the routing level. Use when bridging USDC or USDT to or from Stellar, sending messages between a Stellar contract and another blockchain, making a token exist on multiple chains, or adding cross-chain swaps to an app.

installs
2
GitHub stars
51
Updated
Sep 5
stellar
Community

dapp

Stellar dApp / frontend development. Covers the JavaScript stellar-sdk (browser + Node.js), Freighter wallet, Stellar Wallets Kit (multi-wallet), Wallet Standard, smart accounts with passkeys, transaction building / signing / submission, smart contract invocation from the client, simulation, and error handling. Use when building a React/Next.js/Node.js app that talks to Stellar — classic operations or smart contracts.

installs
2
GitHub stars
51
Updated
Sep 5
stellar
Community

data

Querying Stellar chain data via Stellar RPC (preferred) and Horizon (legacy). Covers RPC JSON-RPC methods, Horizon REST endpoints, streaming, pagination, historical queries, Hubble/Galexie for deep history, and the RPC/Horizon migration story. Use when reading balances, transactions, operations, ledgers, contract events, or building any indexer/analytics workflow.

installs
2
GitHub stars
51
Updated
Sep 5