Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.multiliquid.xyz/llms.txt

Use this file to discover all available pages before exploring further.

Integration Overview

The Multiliquid Protocol on Ethereum can be integrated via the official TypeScript SDK, built on viem. The SDK provides type-safe access to all protocol operations including quoting, swap execution, asset queries, delegate queries, issuer administration, event monitoring, and yield management.

SDK Installation

Install and configure the TypeScript SDK

Quoting

Get swap quotes with optional simulation

Executing Swaps

Execute all swap types with the SDK

Asset Queries

Read asset information, prices, and fees

Issuer Admin

Manage stablecoin delegate issuer-admin configuration

Installation

npm install @uniformlabs/multiliquid-evm-sdk@0.1.3 viem@^2.37.9
The current SDK package is 0.1.3. Install viem directly in your application because integrations create and pass viem clients to the SDK. Node.js >=18 is required.

Client Initialization

The SDK exports a createMultiliquidClient factory along with pre-configured chain deployments:
import { createPublicClient, createWalletClient, http } from "viem";
import { mainnet as viemMainnet } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
import {
  createMultiliquidClient,
  mainnet,
} from "@uniformlabs/multiliquid-evm-sdk";

// Setup viem clients
const publicClient = createPublicClient({
  chain: viemMainnet,
  transport: http("https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY"),
});

const account = privateKeyToAccount("0x...");
const walletClient = createWalletClient({
  chain: viemMainnet,
  transport: http("https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY"),
  account,
});

// Create Multiliquid client
const ml = createMultiliquidClient({
  deployment: mainnet, // or sepolia for testnet
  publicClient,
  walletClient, // optional — required for swaps, issuer-admin writes, and yield accrual
});
The client exposes eight modules: ml.quote, ml.swap, ml.assets, ml.delegates, ml.issuerAdmin, ml.yield, ml.events, and ml.multicall.

Asset IDs

Assets in the protocol are identified by bytes32 IDs (not token addresses). The SDK includes all asset IDs for each deployment:
import { mainnet } from "@uniformlabs/multiliquid-evm-sdk";

// RWA Asset IDs
mainnet.assetIds.rwa.ULTRA;
mainnet.assetIds.rwa.JTRSY;
mainnet.assetIds.rwa.WTGXX;
mainnet.assetIds.rwa.BENJI;
mainnet.assetIds.rwa.USTB;
mainnet.assetIds.rwa.VBILL;

// Stablecoin Asset IDs
mainnet.assetIds.stablecoin.USDC;
mainnet.assetIds.stablecoin.TSY_YIELD;
Asset IDs are NOT derived from token addresses. They are chosen on deployment using keccak256 of a specific string. Always use the IDs from the SDK’s deployment config or the Deployments page.
The Multiliquid Protocol uses deterministic NAV-based pricing for all swaps. Unlike AMM-style DEXs (Uniswap, Curve, etc.), there is no price slippage based on liquidity depth or market impact.Use the exact amounts returned by the quote functions without applying slippage buffers. Pricing is deterministic and based on real-time NAV values.

Quoting

The ml.quote module provides quote functions for all swap types. Each returns the exact amounts, fees, and an optional simulation result.

Stablecoin to RWA (ExactIn)

const quote = await ml.quote.swapIntoRWA({
  rwaID: mainnet.assetIds.rwa.ULTRA,
  stablecoinID: mainnet.assetIds.stablecoin.USDC,
  stablecoinAmount: 1_000_000n, // 1 USDC (6 decimals)
  user: account.address,
  simulate: true, // optional: verify swap would succeed
});

console.log("RWA output:", quote.rwaAmount);
console.log("Protocol fee:", quote.protocolFee);
console.log("Redemption fee:", quote.redemptionFee);
console.log("Gas estimate:", quote.simulation?.gasEstimate);

Stablecoin to RWA (ExactOut)

const quote = await ml.quote.swapIntoRWAExactOut({
  rwaID: mainnet.assetIds.rwa.ULTRA,
  stablecoinID: mainnet.assetIds.stablecoin.USDC,
  rwaAmount: 1_000_000_000_000_000_000n, // 1 ULTRA (18 decimals)
  user: account.address,
});

console.log("Stablecoin required:", quote.stablecoinAmount);

RWA to Stablecoin (ExactIn)

const quote = await ml.quote.swapIntoStablecoin({
  stablecoinID: mainnet.assetIds.stablecoin.USDC,
  rwaID: mainnet.assetIds.rwa.ULTRA,
  rwaAmount: 1_000_000_000_000_000_000n, // 1 ULTRA
});

console.log("Stablecoin output:", quote.stablecoinAmount);
console.log("Protocol fee:", quote.protocolFee);

RWA to Stablecoin (ExactOut)

const quote = await ml.quote.swapIntoStablecoinExactOut({
  stablecoinID: mainnet.assetIds.stablecoin.USDC,
  rwaID: mainnet.assetIds.rwa.ULTRA,
  stablecoinAmount: 1_000_000n, // exact 1 USDC out
  user: account.address,
});

console.log("RWA required:", quote.rwaAmount);

RWA to RWA

Swap one RWA directly for another. The swap is priced through an intermediate stablecoin issuer.
const quote = await ml.quote.swapRWAToRWA({
  stablecoinID: mainnet.assetIds.stablecoin.USDC,
  rwaInID: mainnet.assetIds.rwa.ULTRA,
  rwaOutID: mainnet.assetIds.rwa.USTB,
  exactOut: false,
  rwaInAmount: 1_000_000_000_000_000_000n, // 1 ULTRA in
  rwaOutAmount: 0n,
});

console.log("RWA output:", quote.rwaOutAmount);
console.log("Protocol fee:", quote.protocolFee);
console.log("Redemption fee:", quote.redemptionFee);

Stablecoin to Stablecoin

const quote = await ml.quote.swapStablecoinToStablecoin({
  stablecoinInID: mainnet.assetIds.stablecoin.USDC,
  stablecoinOutID: mainnet.assetIds.stablecoin.TSY_YIELD,
  exactOut: false,
  useDelegateForStablecoinOut: true,
  stablecoinInAmount: 1_000_000n, // 1 USDC in
  stablecoinOutAmount: 0n,
  user: account.address,
});

console.log("Stablecoin output:", quote.stablecoinOutAmount);
The useDelegateForStablecoinOut parameter determines which stablecoin’s delegate handles the swap:
  • true: Use the output stablecoin’s delegate
  • false: Use the input stablecoin’s delegate
One stablecoin must have a delegate (full protocol integration), while the other can be “swap-only”. The delegate handles token movements, fee collection, and compliance checks.

Executing Swaps

The ml.swap module executes swaps on-chain. All methods require a walletClient with an attached account and return a transaction hash.
ERC-20 token approvals must be granted to the MultiliquidSwap contract before executing swaps. The SDK does not handle approvals automatically.

Stablecoin to RWA

// Get quote first
const quote = await ml.quote.swapIntoRWA({
  rwaID: mainnet.assetIds.rwa.ULTRA,
  stablecoinID: mainnet.assetIds.stablecoin.USDC,
  stablecoinAmount: 1_000_000n,
  user: account.address,
});

// Execute swap using quoted amounts
const txHash = await ml.swap.swapIntoRWA({
  rwaID: mainnet.assetIds.rwa.ULTRA,
  stablecoinID: mainnet.assetIds.stablecoin.USDC,
  stablecoinAmount: 1_000_000n,
  minRwaAmount: quote.rwaAmount,
});

const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });

RWA to Stablecoin

const quote = await ml.quote.swapIntoStablecoin({
  stablecoinID: mainnet.assetIds.stablecoin.USDC,
  rwaID: mainnet.assetIds.rwa.ULTRA,
  rwaAmount: 1_000_000_000_000_000_000n,
});

const txHash = await ml.swap.swapIntoStablecoin({
  stablecoinID: mainnet.assetIds.stablecoin.USDC,
  rwaID: mainnet.assetIds.rwa.ULTRA,
  rwaAmount: 1_000_000_000_000_000_000n,
  minStablecoinAmount: quote.stablecoinAmount,
});

RWA to RWA

const quote = await ml.quote.swapRWAToRWA({
  stablecoinID: mainnet.assetIds.stablecoin.USDC,
  rwaInID: mainnet.assetIds.rwa.ULTRA,
  rwaOutID: mainnet.assetIds.rwa.USTB,
  exactOut: false,
  rwaInAmount: 1_000_000_000_000_000_000n,
  rwaOutAmount: 0n,
});

const txHash = await ml.swap.swapRWAToRWA({
  stablecoinID: mainnet.assetIds.stablecoin.USDC,
  rwaInID: mainnet.assetIds.rwa.ULTRA,
  rwaOutID: mainnet.assetIds.rwa.USTB,
  exactOut: false,
  rwaInAmount: 1_000_000_000_000_000_000n,
  rwaOutAmount: quote.rwaOutAmount,
});

Stablecoin to Stablecoin

const quote = await ml.quote.swapStablecoinToStablecoin({
  stablecoinInID: mainnet.assetIds.stablecoin.USDC,
  stablecoinOutID: mainnet.assetIds.stablecoin.TSY_YIELD,
  exactOut: false,
  useDelegateForStablecoinOut: true,
  stablecoinInAmount: 1_000_000n,
  stablecoinOutAmount: 0n,
  user: account.address,
});

const txHash = await ml.swap.swapStablecoinToStablecoin({
  exactOut: false,
  useDelegateForStablecoinOut: true,
  stablecoinInID: mainnet.assetIds.stablecoin.USDC,
  stablecoinInAmount: 1_000_000n,
  stablecoinOutID: mainnet.assetIds.stablecoin.TSY_YIELD,
  stablecoinOutAmount: quote.stablecoinOutAmount,
});

Building Calldata Without Sending

For smart account batching or offline signing, generate raw calldata without submitting a transaction:
const calldata = ml.swap.buildSwapCalldata("swapIntoRWA", {
  rwaID: mainnet.assetIds.rwa.ULTRA,
  stablecoinID: mainnet.assetIds.stablecoin.USDC,
  stablecoinAmount: 1_000_000n,
  minRwaAmount: quote.rwaAmount,
});
// Returns: Hex-encoded calldata

Querying Assets

The ml.assets module provides read-only access to asset information, prices, and fee configuration.

Asset Information

// Individual asset queries
const ultraInfo = await ml.assets.getRWAInfo(mainnet.assetIds.rwa.ULTRA);
console.log("Accepted:", ultraInfo.accepted);
console.log("Decimals:", ultraInfo.decimals);
console.log("Token address:", ultraInfo.assetAddress);

const usdcInfo = await ml.assets.getStablecoinInfo(
  mainnet.assetIds.stablecoin.USDC,
);

// Bulk queries (multicalled into single RPC call)
const allRWAs = await ml.assets.getAllRWAInfo();
const allStablecoins = await ml.assets.getAllStablecoinInfo();

Prices

// Individual prices (WAD format: 1e18 = $1.00)
const ultraPrice = await ml.assets.getRWAPrice(mainnet.assetIds.rwa.ULTRA);
const usdcValue = await ml.assets.getStablecoinUSDValue(
  mainnet.assetIds.stablecoin.USDC,
);

// All prices in one call
const prices = await ml.assets.getAllPrices();
for (const p of prices) {
  console.log(p.assetId, p.price, p.source);
}

Fee Configuration

// Volume-based fee tiers
const feeTiers = await ml.assets.getFeeTiers();
for (const tier of feeTiers) {
  console.log("Max volume:", tier.maxVolume, "Fee:", tier.fee);
}

// Per-pair fees
const discount = await ml.assets.getDiscountRate(
  mainnet.assetIds.stablecoin.USDC,
  mainnet.assetIds.rwa.ULTRA,
);
const redemptionFee = await ml.assets.getRedemptionFee(
  mainnet.assetIds.stablecoin.USDC,
  mainnet.assetIds.rwa.ULTRA,
);

// Stablecoin-to-stablecoin fee configuration
const usdcToken = (
  await ml.assets.getStablecoinInfo(mainnet.assetIds.stablecoin.USDC)
).assetAddress;
const acceptanceFee = await ml.assets.getStablecoinAcceptanceFee(
  mainnet.assetIds.stablecoin.TSY_YIELD,
  usdcToken,
);
const stablecoinRedemptionFee = await ml.assets.getStablecoinRedemptionFee(
  mainnet.assetIds.stablecoin.TSY_YIELD,
  usdcToken,
);

Access Control Checks

const paused = await ml.assets.isPaused();
const blacklisted = await ml.assets.isBlacklisted(userAddress);
const whitelisted = await ml.assets.isWhitelistedForRWA(
  mainnet.assetIds.rwa.ULTRA,
  fromAddress,
  toAddress,
  amount,
);

Event Monitoring

The ml.events module provides historical event queries and real-time monitoring.

Query Historical Events

const events = await ml.events.getEvents({
  user: account.address,
  stablecoinID: mainnet.assetIds.stablecoin.USDC,
  fromBlock: 19_000_000n,
  toBlock: "latest",
});

for (const event of events) {
  switch (event.type) {
    case "SwapIntoRWA":
    case "SwapIntoRWAExactOut":
      console.log(
        "Bought RWA:",
        event.rwaAmount,
        "for",
        event.stablecoinAmount,
      );
      break;
    case "SwapIntoStablecoin":
    case "SwapIntoStablecoinExactOut":
      console.log("Sold RWA:", event.rwaAmount, "for", event.stablecoinAmount);
      break;
    case "RWAToRWASwap":
      console.log("Swapped RWA:", event.rwaInAmount, "->", event.rwaOutAmount);
      break;
    case "StablecoinToStablecoinSwap":
      console.log(
        "Swapped stable:",
        event.stablecoinInAmount,
        "->",
        event.stablecoinOutAmount,
      );
      break;
  }
}

Watch Events in Real-Time

const unsubscribe = ml.events.watchEvents(
  { user: account.address },
  (event) => {
    console.log("New swap event:", event.type);
  },
);

// Later: stop watching
unsubscribe();

Parse Raw Logs

const parsed = ml.events.parseLog(rawLog);

if (parsed !== undefined) {
  console.log("Parsed Multiliquid event:", parsed.type);
}

Multicall

The ml.multicall module batches arbitrary read calls into a single RPC round-trip with return types inferred from each ABI and function name.
import {
  multiliquidSwapAbi,
  priceAdapterAbi,
} from "@uniformlabs/multiliquid-evm-sdk";

const ultraPriceAdapter =
  mainnet.addresses.priceAdapters?.[mainnet.assetIds.rwa.ULTRA];
if (ultraPriceAdapter === undefined) {
  throw new Error("Missing ULTRA price adapter in deployment config");
}

const [ultraInfo, ultraPrice] = await ml.multicall.read([
  {
    address: mainnet.addresses.multiliquidSwap,
    abi: multiliquidSwapAbi,
    functionName: "rwaInfo",
    args: [mainnet.assetIds.rwa.ULTRA],
  },
  {
    address: ultraPriceAdapter,
    abi: priceAdapterAbi,
    functionName: "getPrice",
  },
]);

Error Handling

The SDK provides typed error classes for all protocol errors:
import {
  UserBlacklistedError,
  UserNotWhitelistedError,
  ContractPausedError,
  InsufficientRWAOutputError,
  InsufficientStablecoinOutputError,
  StablecoinPriceOutsideBandError,
  decodeMultiliquidError,
} from "@uniformlabs/multiliquid-evm-sdk";

try {
  const txHash = await ml.swap.swapIntoRWA({ ... });
} catch (error) {
  if (error instanceof ContractPausedError) {
    console.log("Protocol is paused");
  } else if (error instanceof UserBlacklistedError) {
    console.log("User is blacklisted:", error.args.user);
  } else if (error instanceof UserNotWhitelistedError) {
    console.log("Not whitelisted for RWA:", error.args.rwaID);
  } else if (error instanceof InsufficientRWAOutputError) {
    console.log("Output below minimum — re-quote and retry");
  } else if (error instanceof StablecoinPriceOutsideBandError) {
    console.log("Stablecoin price outside band:", error.args.oraclePrice);
  }
}
Use the simulate: true option on quote functions to catch errors before submitting a transaction:
const quote = await ml.quote.swapIntoRWA({
  rwaID: mainnet.assetIds.rwa.ULTRA,
  stablecoinID: mainnet.assetIds.stablecoin.USDC,
  stablecoinAmount: 1_000_000n,
  user: account.address,
  simulate: true,
});

if (!quote.simulation?.success) {
  console.log("Swap would fail:", quote.simulation?.error);
}

Delegate Queries

Query stablecoin delegate information for custody and compliance details:
const delegateInfo = await ml.delegates.getDelegateInfo(
  mainnet.assetIds.stablecoin.USDC,
);

console.log("Delegate type:", delegateInfo.type);
console.log("RWA custody:", delegateInfo.rwaCustodyAddress);
console.log("Stablecoin custody:", delegateInfo.stablecoinCustodyAddress);

// Direct delegate reads are also available when you only need one field
const delegateAddress = await ml.delegates.getDelegateAddress(
  mainnet.assetIds.stablecoin.USDC,
);
const rwaCustody = await ml.delegates.getRWACustodyAddress(
  mainnet.assetIds.stablecoin.USDC,
);
const stablecoinCustody = await ml.delegates.getStablecoinCustodyAddress(
  mainnet.assetIds.stablecoin.USDC,
);
const rwaColdStorage = await ml.delegates.getRWAColdStorageAddresses(
  mainnet.assetIds.stablecoin.USDC,
);
const stablecoinColdStorage =
  await ml.delegates.getStablecoinColdStorageAddresses(
    mainnet.assetIds.stablecoin.USDC,
  );

Issuer Admin Operations

The ml.issuerAdmin module manages stablecoin delegate issuer-admin configuration. Reads require a publicClient; writes require a walletClient whose account is authorized by the delegate contract.
const stablecoinID = mainnet.assetIds.stablecoin.USDC;

const isAdmin = await ml.issuerAdmin.isIssuerAdmin(
  stablecoinID,
  account.address,
);

if (isAdmin) {
  const txHash = await ml.issuerAdmin.setRWADiscountRate({
    stablecoinID,
    rwaID: mainnet.assetIds.rwa.ULTRA,
    rate: 50_000_000_000_000_000n,
  });
}
Available write methods include issuer-admin role management, RWA and stablecoin whitelist controls, fee and rate updates, custody address updates, cold-storage address updates, delegate-level blacklist controls, and delegate pause controls.
await ml.issuerAdmin.pause({ stablecoinID });
await ml.issuerAdmin.unpause({ stablecoinID });
For multisigs, smart accounts, or offline signing flows, build delegate calldata without sending:
const data = ml.issuerAdmin.buildIssuerAdminCalldata("whitelistRWA", {
  rwa: "0x...",
  accepted: true,
});

const tx = await ml.issuerAdmin.buildIssuerAdminTransaction(
  stablecoinID,
  "whitelistRWA",
  {
    rwa: "0x...",
    accepted: true,
  },
);
// tx.to is the resolved stablecoin delegate address
// tx.data is the encoded delegate calldata

Yield Operations

For yield-bearing stablecoins, the ml.yield module provides interest accrual and rate queries:
const TSY = mainnet.assetIds.stablecoin.TSY_YIELD;

// Query current day and rates
const currentDay = await ml.yield.getCurrentDay(TSY);
const rates = await ml.yield.getDailyRates(TSY, currentDay - 7n, currentDay);

// Query user yield accounting
const credits = await ml.yield.getCredits(TSY, account.address);
const totalWithheldCredits = await ml.yield.getTotalWithheldCredits(
  TSY,
  account.address,
);
const yieldMultiplier = await ml.yield.getYieldMultiplier(TSY, account.address);
const lastAccrualDay = await ml.yield.getLastAccrualDay(TSY, account.address);
const withheldCredits = await ml.yield.getWithheldCredits(TSY, account.address);

// Calculate yield for a user
const yieldAmount = await ml.yield.getYieldAmount({
  stablecoinID: TSY,
  user: account.address,
  redeemValue: 1_000_000_000_000_000_000n,
  stablecoinWithdrawal: false,
});

const amountForTargetValue = await ml.yield.getAmountForTargetValue({
  stablecoinID: TSY,
  user: account.address,
  targetDollars: 1_000_000_000_000_000_000n,
  stablecoinWithdrawal: false,
});

// Accrue interest
const txHash = await ml.yield.accrueInterest(TSY, account.address);

Integration Best Practices

Security

  1. Use Hardware Wallets: Never store private keys in application code
  2. Simulate Before Executing: Use simulate: true on quotes to verify swaps before submitting
  3. Test on Sepolia: Thoroughly test all swap flows on testnet before mainnet
  4. Monitor Events: Use ml.events.watchEvents() for real-time swap tracking

Performance

  1. Batch Approvals: Approve large amounts to reduce approval transactions
  2. Use Multicall: ml.assets.getAllPrices() and ml.assets.getAllRWAInfo() batch reads into single RPC calls
  3. Reuse Client: Create one client instance and reuse across operations
  4. Cache Asset IDs: Asset IDs are static per deployment — reference them from the SDK config

Compliance

  1. Check Whitelist Status: Call ml.assets.isWhitelistedForRWA() before presenting swap UI
  2. Check Blacklist Status: Call ml.assets.isBlacklisted() to verify user eligibility
  3. Metadata Fields: Pass compliance data in the optional metadata parameters
  4. Audit Trails: Use ml.events.getEvents() to build regulatory reports

Testing

Sepolia Testnet

import {
  createMultiliquidClient,
  sepolia,
} from "@uniformlabs/multiliquid-evm-sdk";
import { sepolia as viemSepolia } from "viem/chains";

const publicClient = createPublicClient({
  chain: viemSepolia,
  transport: http(),
});

const ml = createMultiliquidClient({
  deployment: sepolia,
  publicClient,
  walletClient,
});

// Sepolia asset IDs
sepolia.assetIds.rwa.MOCK_RWA_WHITELIST;
sepolia.assetIds.stablecoin.USDC;
Faucets:

Support and Resources