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, 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
Installation
npm install @uniformlabs/multiliquid-evm-sdk viem
The SDK requires viem as a peer dependency. 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 only for swap execution
});
The client exposes seven modules: ml.quote, ml.swap, ml.assets, ml.delegates, 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
);
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 ,
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 ();
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 );
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 );
// 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 ,
});
// 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