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.
NAV-Based Pricing
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_000 n , // 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_000 n , // 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_000 n , // 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_000 n , // 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_000 n , // 1 ULTRA in
rwaOutAmount: 0 n ,
});
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_000 n , // 1 USDC in
stablecoinOutAmount: 0 n ,
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_000 n ,
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_000 n ,
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_000 n ,
});
const txHash = await ml . swap . swapIntoStablecoin ({
stablecoinID: mainnet . assetIds . stablecoin . USDC ,
rwaID: mainnet . assetIds . rwa . ULTRA ,
rwaAmount: 1_000_000_000_000_000_000 n ,
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_000 n ,
rwaOutAmount: 0 n ,
});
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_000 n ,
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_000 n ,
stablecoinOutAmount: 0 n ,
user: account . address ,
});
const txHash = await ml . swap . swapStablecoinToStablecoin ({
exactOut: false ,
useDelegateForStablecoinOut: true ,
stablecoinInID: mainnet . assetIds . stablecoin . USDC ,
stablecoinInAmount: 1_000_000 n ,
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_000 n ,
minRwaAmount: quote . rwaAmount ,
});
// Returns: Hex-encoded calldata
Querying Assets
The ml.assets module provides read-only access to asset information, prices, and fee configuration.
// 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_000 n ,
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_000 n ,
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_000 n ,
});
}
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 - 7 n , 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_000 n ,
stablecoinWithdrawal: false ,
});
const amountForTargetValue = await ml . yield . getAmountForTargetValue ({
stablecoinID: TSY ,
user: account . address ,
targetDollars: 1_000_000_000_000_000_000 n ,
stablecoinWithdrawal: false ,
});
// Accrue interest
const txHash = await ml . yield . accrueInterest ( TSY , account . address );
Integration Best Practices
Security
Use Hardware Wallets : Never store private keys in application code
Simulate Before Executing : Use simulate: true on quotes to verify swaps before submitting
Test on Sepolia : Thoroughly test all swap flows on testnet before mainnet
Monitor Events : Use ml.events.watchEvents() for real-time swap tracking
Batch Approvals : Approve large amounts to reduce approval transactions
Use Multicall : ml.assets.getAllPrices() and ml.assets.getAllRWAInfo() batch reads into single RPC calls
Reuse Client : Create one client instance and reuse across operations
Cache Asset IDs : Asset IDs are static per deployment — reference them from the SDK config
Compliance
Check Whitelist Status : Call ml.assets.isWhitelistedForRWA() before presenting swap UI
Check Blacklist Status : Call ml.assets.isBlacklisted() to verify user eligibility
Metadata Fields : Pass compliance data in the optional metadata parameters
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