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.
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_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.
// 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_000n,
toBlock: "latest",
});
for (const event of events) {
switch (event.type) {
case "SwapIntoRWA":
console.log("Bought RWA:", event.rwaAmount, "for", event.stablecoinAmount);
break;
case "SwapIntoStablecoin":
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_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);
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);
// 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,
});
// 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