Skip to main content
This guide walks through the complete margin account lifecycle: creating an account, depositing collateral, borrowing, deploying capital to external protocols (Blend, Aquarius), repaying, and closing.

Account Lifecycle

create_account()

deposit_collateral_tokens()

borrow()

SmartAccount.execute()  [Blend deposit / Aquarius LP]

repay()

withdraw_collateral_balance()

close_account()   [optional]

Prerequisites


Step 1: Create a Margin Account

const ACCOUNT_MANAGER = 'CAK2IJIO2SKZWUODY4G7ZRIUUIIMJUUAIXE3I5YTQ5QYNSS2RYJ3P4CV';

async function createMarginAccount(userAddress: string): Promise<string> {
  // Submit the create_account transaction
  const txHash = await invokeContract(
    userAddress,
    ACCOUNT_MANAGER,
    'create_account',
    [nativeToScVal(userAddress, { type: 'address' })],
  );

  // After confirmation, find the account address in Registry
  const REGISTRY = 'CC35XWCH7SCQROTNW7PA6HZKP4JMNSVV2K7CX3HY2PSI2MI2ZQQH73ID';
  const accounts = await readContract(
    REGISTRY,
    'get_accounts',
    [nativeToScVal(userAddress, { type: 'address' })],
  ) as string[];

  return accounts[accounts.length - 1]; // newest account
}
If the user already has a closed account, create_account() will reuse it. The same Registry call will return the reactivated address.

Step 2: Check Health Factor Before Any Operation

Always check health factor before borrows and withdrawals:
const RISK_ENGINE = 'CBL7RCG5H4VIZCNF7BRM2FQFXK7N5KRQKW7ZVEQZJKNXHA6FEU4OXK5I';
const WAD = 10n ** 18n;

async function getHealthFactor(smartAccount: string): Promise<number | null> {
  const [collateralScVal, debtScVal] = await Promise.all([
    readContract(
      RISK_ENGINE, 'get_current_total_balance',
      [nativeToScVal(smartAccount, { type: 'address' })],
    ),
    readContract(
      RISK_ENGINE, 'get_current_total_borrows',
      [nativeToScVal(smartAccount, { type: 'address' })],
    ),
  ]);

  const collateralUsd = collateralScVal as bigint;
  const debtUsd       = debtScVal as bigint;

  if (debtUsd === 0n) return null; // no debt, infinite HF

  const hf = Number(collateralUsd * WAD / debtUsd) / 1e18;
  return hf; // e.g., 1.5 = 150% collateralization
}
Liquidation threshold: HF < 1.1

Step 3: Deposit Collateral

async function depositCollateral(
  userAddress: string,
  smartAccount: string,
  symbol: 'XLM' | 'USDC',
  amount: number,  // human-readable
): Promise<string> {
  const amountWad = BigInt(Math.floor(amount * 1e18));

  return await invokeContract(
    userAddress,
    ACCOUNT_MANAGER,
    'deposit_collateral_tokens',
    [
      nativeToScVal(smartAccount, { type: 'address' }),
      nativeToScVal(symbol,       { type: 'symbol' }),
      nativeToScVal(amountWad,    { type: 'u256' }),
    ],
  );
}

Step 4: Borrow

async function borrowAsset(
  userAddress: string,
  smartAccount: string,
  symbol: 'XLM' | 'USDC',
  amount: number,
): Promise<string> {
  const amountWad = BigInt(Math.floor(amount * 1e18));

  return await invokeContract(
    userAddress,
    ACCOUNT_MANAGER,
    'borrow',
    [
      nativeToScVal(smartAccount, { type: 'address' }),
      nativeToScVal(amountWad,    { type: 'u256' }),
      nativeToScVal(symbol,       { type: 'symbol' }),
    ],
  );
}
The RiskEngine runs a health check before the borrow. If the transaction would drop HF below 1.1×, it panics and the transaction fails. Check current health factor and available borrow capacity before calling. Maximum borrow capacity:
max_borrow_usd ≈ (collateral_usd / 1.1) - current_debt_usd

Step 5: Atomic Deposit + Borrow

For better UX, combine deposit and borrow in a single transaction:
// Same-asset: deposit XLM, borrow XLM
async function depositAndBorrow(
  userAddress: string,
  smartAccount: string,
  symbol: string,
  depositAmount: number,
  borrowAmount: number,
): Promise<string> {
  return await invokeContract(
    userAddress,
    ACCOUNT_MANAGER,
    'deposit_and_borrow',
    [
      nativeToScVal(smartAccount,                               { type: 'address' }),
      nativeToScVal(BigInt(Math.floor(depositAmount * 1e18)),   { type: 'u256' }),
      nativeToScVal(BigInt(Math.floor(borrowAmount * 1e18)),    { type: 'u256' }),
      nativeToScVal(symbol,                                     { type: 'symbol' }),
    ],
  );
}

// Cross-asset: deposit XLM, borrow USDC
async function depositAndBorrowCross(
  userAddress: string,
  smartAccount: string,
  depositSymbol: string,
  depositAmount: number,
  borrowSymbol: string,
  borrowAmount: number,
): Promise<string> {
  return await invokeContract(
    userAddress,
    ACCOUNT_MANAGER,
    'deposit_and_borrow_cross',
    [
      nativeToScVal(smartAccount,                               { type: 'address' }),
      nativeToScVal(BigInt(Math.floor(depositAmount * 1e18)),   { type: 'u256' }),
      nativeToScVal(depositSymbol,                              { type: 'symbol' }),
      nativeToScVal(BigInt(Math.floor(borrowAmount * 1e18)),    { type: 'u256' }),
      nativeToScVal(borrowSymbol,                               { type: 'symbol' }),
    ],
  );
}

Step 6: Deploy to External Protocols

After borrowing, the SmartAccount holds the borrowed assets. Route them to Blend or Aquarius via AccountManager.execute().
The execute() function routes through AccountManager → SmartAccount → external protocol. This is how the protocol tracks external positions via TrackingTokens.

Deploy to Blend (Earn Yield on Borrowed XLM)

import { xdr, Address } from '@stellar/stellar-sdk';

const BLEND_POOL = 'CCEBVDYM32YNYCVNRXQKDFFPISJJCV557CDZEIRBEE4NCV4KHPQ44HGF';

// SmartAccExternalAction enum values
const ACTION = {
  Deposit:         { tag: 'Deposit',         values: undefined },
  Withdraw:        { tag: 'Withdraw',         values: undefined },
  Swap:            { tag: 'Swap',             values: undefined },
  AddLiquidity:    { tag: 'AddLiquidity',     values: undefined },
  RemoveLiquidity: { tag: 'RemoveLiquidity',  values: undefined },
};

async function deployToBlend(
  userAddress: string,
  smartAccount: string,
  symbol: 'XLM' | 'USDC',
  amount: number,   // in native decimals (stroops for XLM)
): Promise<string> {
  return await invokeContract(
    userAddress,
    ACCOUNT_MANAGER,
    'execute',
    [
      nativeToScVal(smartAccount, { type: 'address' }),
      nativeToScVal(BLEND_POOL,   { type: 'address' }),   // target protocol
      xdr.ScVal.scvVec([xdr.ScVal.scvSymbol('Deposit')]), // action
      nativeToScVal(userAddress,  { type: 'address' }),   // trader
      xdr.ScVal.scvVec([xdr.ScVal.scvSymbol(symbol)]),    // tokens
      xdr.ScVal.scvVec([nativeToScVal(BigInt(amount), { type: 'u128' })]), // amounts
    ],
  );
}

Add Liquidity to Aquarius

const AQUARIUS_POOL = 'CD3LFMMLBQ6RBJUD3Z2LFDFE6544WDRMWHEZYPI5YDVESYRSO2TT32BX';

async function addAquariusLiquidity(
  userAddress: string,
  smartAccount: string,
  xlmAmount: number,   // in stroops
  usdcAmount: number,  // in microUSDC (6 decimals)
): Promise<string> {
  return await invokeContract(
    userAddress,
    ACCOUNT_MANAGER,
    'execute',
    [
      nativeToScVal(smartAccount,   { type: 'address' }),
      nativeToScVal(AQUARIUS_POOL,  { type: 'address' }),
      xdr.ScVal.scvVec([xdr.ScVal.scvSymbol('AddLiquidity')]),
      nativeToScVal(userAddress,    { type: 'address' }),
      xdr.ScVal.scvVec([
        xdr.ScVal.scvSymbol('XLM'),
        xdr.ScVal.scvSymbol('USDC'),
      ]),
      xdr.ScVal.scvVec([
        nativeToScVal(BigInt(xlmAmount),  { type: 'u128' }),
        nativeToScVal(BigInt(usdcAmount), { type: 'u128' }),
      ]),
    ],
  );
}

Step 7: Read Outstanding Debt

const POOL_XLM = 'CBA4E4ZMXUKCDTNT7LDKSO3LGNGKHRCE4GUVPSRCAKU3TKAONUY7SVOB';

async function getOutstandingDebt(
  smartAccount: string,
): Promise<{ xlm: bigint; usdc: bigint }> {
  const [xlmDebt, usdcDebt] = await Promise.all([
    readContract(
      POOL_XLM,
      'get_borrow_balance',
      [nativeToScVal(smartAccount, { type: 'address' })],
    ) as Promise<bigint>,
    readContract(
      POOL_USDC,
      'get_borrow_balance',
      [nativeToScVal(smartAccount, { type: 'address' })],
    ) as Promise<bigint>,
  ]);
  return { xlm: xlmDebt, usdc: usdcDebt };
}

Step 8: Repay

async function repayDebt(
  userAddress: string,
  smartAccount: string,
  symbol: 'XLM' | 'USDC',
  amount: number,
): Promise<string> {
  const amountWad = BigInt(Math.floor(amount * 1e18));

  return await invokeContract(
    userAddress,
    ACCOUNT_MANAGER,
    'repay',
    [
      nativeToScVal(amountWad,    { type: 'u256' }),
      nativeToScVal(symbol,       { type: 'symbol' }),
      nativeToScVal(smartAccount, { type: 'address' }),
    ],
  );
}

Step 9: Withdraw Collateral

async function withdrawCollateral(
  userAddress: string,
  smartAccount: string,
  symbol: string,
  amount: number,
): Promise<string> {
  const amountWad = BigInt(Math.floor(amount * 1e18));

  return await invokeContract(
    userAddress,
    ACCOUNT_MANAGER,
    'withdraw_collateral_balance',
    [
      nativeToScVal(smartAccount, { type: 'address' }),
      nativeToScVal(symbol,       { type: 'symbol' }),
      nativeToScVal(amountWad,    { type: 'u256' }),
    ],
  );
}
The RiskEngine validates the withdrawal. If it would drop HF below 1.1×, the transaction fails.

Step 10: Close Account

async function closeAccount(
  userAddress: string,
  smartAccount: string,
): Promise<string> {
  // Account must have zero debt before closing
  return await invokeContract(
    userAddress,
    ACCOUNT_MANAGER,
    'close_account',
    [nativeToScVal(smartAccount, { type: 'address' })],
  );
}
Panics if: the account still has outstanding debt. Repay all debt first.

Common Pitfalls

Health factor check: always read the current HF before borrows and withdrawals. Transactions that would violate the 1.1× threshold will panic and you’ll lose gas.
MistakeFix
Depositing less than origination feeBorrow amounts above origination_fee_rate × borrow
Not accounting for interest growthBorrow balance grows over time — read get_borrow_balance() fresh before repay
Closing account with debtCall settle_account() or repay() for each borrowed token first
Trying to call SmartAccount directlyAll operations must go through AccountManager