The Multiliquid Protocol on Solana can be integrated via the official TypeScript SDK, which provides a complete interface for quoting, building, and executing swaps against the on-chain program, along with liquidity-provider administration flows.
SDK Installation
Install and configure the TypeScript SDK
Pair Discovery
Find available trading pairs
Quoting
Get swap quotes using client-side math or simulation
Executing Swaps
Build and submit swap transactions
LP Admin
Create, update, and close pairs and manage liquidity
Ladder Pricing Model
Implement a rolling 24-hour laddered pricing model as an LP
The SDK provides a MultiliquidClient class that wraps all functionality:
import { Connection, PublicKey } from "@solana/web3.js";import { MultiliquidClient } from "@uniformlabs/multiliquid-svm-sdk";const connection = new Connection("https://api.mainnet-beta.solana.com");const client = new MultiliquidClient({ connection, cluster: "mainnet-beta", // "devnet" | "mainnet-beta" commitment: "confirmed", // optional, default: "confirmed"});
The cluster parameter determines which built-in pair registry is used. Both devnet and mainnet use the same program ID: HaWDr94LKJQT2fXuHJGsSGeQf6M7S68FXpEQLcE5RYs6.
The SDK ships with a hardcoded registry of known pairs for instant lookup:
const pairs = client.getPairs();// Returns all registered pairs for the configured cluster// Filter by assetconst usdcPairs = client.getPairs({ stableMint: new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"),});
Each returned entry includes the pair PDA, both mints, the liquidity provider, and token decimals:
for (const pair of pairs) { console.log(pair.label); // e.g. "USDC / USTB" console.log(pair.pair.toBase58()); console.log(pair.stableMint.toBase58()); console.log(pair.assetMint.toBase58()); console.log(pair.liquidityProvider.toBase58()); console.log(pair.stableDecimals); // e.g. 6 console.log(pair.assetDecimals); // e.g. 6 for USTB}// Use the entry directly in swap paramsconst pair = pairs[0];const quote = await client.getQuote({ user: wallet.publicKey, liquidityProvider: pair.liquidityProvider, stableMint: pair.stableMint, assetMint: pair.assetMint, amount: new BN(1_000_000_000), swapDirection: SwapDirection.StableToAsset, swapType: SwapType.ExactIn,});
Before executing a swap, verify the pair is active:
const status = await client.checkPauseStatus(stableMint, assetMint, lp);if (status.anyPaused) { console.log("Swap blocked:", status.pauseReasons); // e.g. ["ProgramPaused"], ["PairPaused"], ["RwaPaused"], ["StablePaused"], ["LpStablePaused"]}
The protocol has five independent pause levels: global config, RWA asset config, stablecoin asset config, LP stablecoin config, and pair config. All must be unpaused for swaps to execute.
The SDK is instruction-first: the primary API returns TransactionInstruction objects for maximum composability. A convenience method for building full transactions is also available.
For more control, build the swap instruction separately and compose it with other instructions (e.g., compute budget):
import { ComputeBudgetProgram } from "@solana/web3.js";const { instruction, setupInstructions } = await client.buildSwapInstruction({ user: wallet.publicKey, liquidityProvider: LP, stableMint: USDC, assetMint: USTB, amount: new BN(1_000_000_000), swapDirection: SwapDirection.StableToAsset, swapType: SwapType.ExactIn, minAmountOut: new BN(990_000_000),});// setupInstructions contains ATA creation if needed (autoCreateAta defaults to true).// The builder also resolves Token-2022 transfer-hook accounts for known hook mints.// Compose with compute budget:const instructions = [ ComputeBudgetProgram.setComputeUnitLimit({ units: 150_000 }), ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 50_000 }), ...setupInstructions, instruction,];
The SDK does not include ComputeBudgetProgram instructions automatically. Set compute unit limits and priority fees based on your requirements. Use getQuoteViaSimulation() to measure actual compute units consumed.
Liquidity providers create, configure, and close their own pairs, and manage vault balances, using the same instruction-first API. The SDK derives PDAs, vault authorities, token program IDs, and Token-2022 transfer-hook accounts automatically. The LP signs every builder below; the global-config admin is referenced as a non-signing account (the SDK fetches it from GlobalConfig when not supplied via admin).
init_pair is LP-signed: the liquidity provider pays for the Pair PDA, LP-stable config, vault ATAs, and UserVaultInfo accounts. The builder pre-validates that the stable mint’s asset config has type Stable and the asset mint’s asset config has type Rwa.
Use lpTokenAccount when the liquidity provider uses a non-ATA token account. Set autoCreateAta: false if token accounts are managed externally, and use remainingAccounts only for extra accounts beyond those derived by the SDK.
close_pair is LP-signed and reclaims rent for the Pair PDA, and — when the pair was the last consumer of a shared vault — the vault ATA and UserVaultInfo PDA. Any remaining vault balances are returned to the LP’s recipient token accounts. The SDK reads UserVaultInfo.used for both vaults and only resolves Token-2022 transfer-hook accounts when the shared-vault counter shows the close will transfer.
For Token-2022 mints with a transfer hook, the LP recipient token account must already exist on-chain when the builder runs if the close will transfer vault balances. The builder resolves hook accounts from current on-chain data, so a recipient ATA that only exists as a setup instruction will be rejected. Pre-create the recipient account or pass it via lpStableTokenAccount / lpAssetTokenAccount.Hook account resolution uses a build-time snapshot of the vault balance, while the program transfers the execution-time balance during close_pair.
The SDK provides structured error parsing for on-chain program errors:
try { const signature = await connection.sendTransaction(transaction); await connection.confirmTransaction(signature);} catch (error) { const parsed = client.parseSwapError(error); if (parsed) { console.log("Error:", parsed.name, "-", parsed.message); console.log("Category:", parsed.category); switch (parsed.category) { case "slippage": // AmountOutTooLow or AmountInTooHigh — re-quote with fresh state break; case "paused": // ProgramPaused, PairPaused, RwaPaused, StablePaused — wait or skip break; case "oracle": // InvalidNav — NAV source unavailable or divergent break; case "liquidity": // InsufficientLiquidity — try smaller amount or different pair break; case "input_validation": // Fix input parameters break; case "math": // MathOverflow/Underflow — trade may be too small or too large break; } }}
LP vault token accounts are ATAs for (mint, vaultAuthority), where vaultAuthority is the LP-specific PDA derived from ["vault_authority", lp]. Fee vault token accounts are ATAs for (stableMint, programAuthority), where programAuthority is the global PDA derived from ["program_authority"].When deriving vault or fee-vault addresses manually for Token-2022 mints, pass the Token-2022 program ID as the optional tokenProgram argument. Swap and liquidity builders detect the mint owner and derive the correct ATAs automatically.