Skip to main content

Integration Overview

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

Installation

npm install @uniformlabs/multiliquid-svm-sdk@0.3.2
The current SDK package is 0.3.2. The package declares runtime dependencies on:
  • @solana/web3.js ^1.98.4
  • @coral-xyz/anchor ^0.32.1
  • @solana/spl-token ^0.4.14
Install @solana/web3.js and @coral-xyz/anchor directly in your application when importing them in integration code:
npm install @solana/web3.js@^1.98.4 @coral-xyz/anchor@^0.32.1

Client Initialization

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.

Pair Discovery

Built-In Registry (No RPC)

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 asset
const 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 params
const 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,
});

On-Chain Discovery (RPC)

For dynamically discovering pairs not in the registry:
const pairs = await client.discoverPairs({
  stableMint: USDC_MINT,
});
// Returns the same PairRegistryEntry format as getPairs()

Checking Pair Status

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.

Quoting

The SDK offers two quoting methods: client-side math replication and on-chain simulation.

Client-Side Quote

Replicates the on-chain Rust math exactly using BigInt. Fetches current NAV prices from oracles and computes the swap result:
import { PublicKey } from "@solana/web3.js";
import { BN } from "@coral-xyz/anchor";
import { SwapDirection, SwapType } from "@uniformlabs/multiliquid-svm-sdk";

const USDC = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
const USTB = new PublicKey("CCz3SGVziFeLYk2xfEstkiqJfYkjaSWb2GCABYsVcjo2");
const LP = new PublicKey("C8Mi6kn7ajFWuNe4ZmsR9A6fdqRYhzXFoqVBGMsdJ2Uf");

const quote = await client.getQuote({
  user: wallet.publicKey,
  liquidityProvider: LP,
  stableMint: USDC,
  assetMint: USTB,
  amount: new BN(1_000_000_000),  // 1000 USDC (6 decimals)
  swapDirection: SwapDirection.StableToAsset,
  swapType: SwapType.ExactIn,
});

console.log("Output:", MultiliquidClient.toHumanReadable(quote.amountOut, 6));
console.log("Protocol fees:", quote.protocolFees.toString());
console.log("Asset NAV:", quote.assetNav.toString());
console.log("Stable NAV:", quote.stableNav.toString());
The SwapQuote includes:
FieldDescription
amountInTotal input amount
amountOutOutput amount received
protocolFeesProtocol fee collected
discountAmountLP fee amount (redemption or discount)
assetNavRWA NAV price (9 decimals)
stableNavStablecoin NAV price (9 decimals)

Simulation Quote

Runs the swap instruction against the validator via simulateTransaction and parses the emitted event:
const simQuote = await client.getQuoteViaSimulation({
  user: wallet.publicKey,
  liquidityProvider: LP,
  stableMint: USDC,
  assetMint: USTB,
  amount: new BN(1_000_000_000),
  swapDirection: SwapDirection.StableToAsset,
  swapType: SwapType.ExactIn,
});

console.log("Output:", simQuote.amountOut.toString());
console.log("Compute units:", simQuote.computeUnitsConsumed);
The simulation quote also returns computeUnitsConsumed, which is useful for setting compute budget instructions.

Executing Swaps

The SDK is instruction-first: the primary API returns TransactionInstruction objects for maximum composability. A convenience method for building full transactions is also available.

Building a Swap Transaction

const { transaction, accounts } = await client.buildSwapTransaction({
  user: wallet.publicKey,
  liquidityProvider: LP,
  stableMint: USDC,
  assetMint: USTB,
  amount: new BN(1_000_000_000),  // 1000 USDC
  swapDirection: SwapDirection.StableToAsset,
  swapType: SwapType.ExactIn,
  minAmountOut: new BN(990_000_000),  // slippage protection
});

// Sign and send
transaction.sign([wallet]);
const signature = await connection.sendTransaction(transaction);
await connection.confirmTransaction(signature, "confirmed");

Building Individual Instructions

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.

Swap Examples

Buy RWA with USDC (ExactIn) Swap exactly 1000 USDC for USTB, accepting at minimum 990 USTB:
const { transaction } = await client.buildSwapTransaction({
  user: wallet.publicKey,
  liquidityProvider: LP,
  stableMint: USDC,
  assetMint: USTB,
  amount: new BN(1_000_000_000),      // 1000 USDC (6 decimals)
  swapDirection: SwapDirection.StableToAsset,
  swapType: SwapType.ExactIn,
  minAmountOut: new BN(990_000_000),  // min 990 USTB (6 decimals)
});
Sell RWA for USDC (ExactIn) Swap exactly 100 USTB for USDC, accepting at minimum 95 USDC:
const { transaction } = await client.buildSwapTransaction({
  user: wallet.publicKey,
  liquidityProvider: LP,
  stableMint: USDC,
  assetMint: USTB,
  amount: new BN(100_000_000),        // 100 USTB (6 decimals)
  swapDirection: SwapDirection.AssetToStable,
  swapType: SwapType.ExactIn,
  minAmountOut: new BN(95_000_000),   // min 95 USDC (6 decimals)
});
Buy Exact RWA Amount (ExactOut) Receive exactly 100 USTB, spending at most 105 USDC:
const { transaction } = await client.buildSwapTransaction({
  user: wallet.publicKey,
  liquidityProvider: LP,
  stableMint: USDC,
  assetMint: USTB,
  amount: new BN(100_000_000),        // exact 100 USTB out (6 decimals)
  swapDirection: SwapDirection.StableToAsset,
  swapType: SwapType.ExactOut,
  maxAmountIn: new BN(105_000_000),   // max 105 USDC (6 decimals)
});
Sell RWA for Exact USDC (ExactOut) Receive exactly 1000 USDC, spending at most 1010 USTB:
const { transaction } = await client.buildSwapTransaction({
  user: wallet.publicKey,
  liquidityProvider: LP,
  stableMint: USDC,
  assetMint: USTB,
  amount: new BN(1_000_000_000),      // exact 1000 USDC out (6 decimals)
  swapDirection: SwapDirection.AssetToStable,
  swapType: SwapType.ExactOut,
  maxAmountIn: new BN(1_010_000_000),  // max 1010 USTB (6 decimals)
});

Swap Parameters Reference

ParameterTypeDescription
userPublicKeyTransaction signer
liquidityProviderPublicKeyLP that owns the pair
stableMintPublicKeyStablecoin token mint
assetMintPublicKeyRWA token mint
amountBNPrimary amount in native token units (input for ExactIn, output for ExactOut)
swapDirectionSwapDirectionStableToAsset or AssetToStable
swapTypeSwapTypeExactIn or ExactOut
minAmountOutBN (optional)Minimum output for ExactIn swaps
maxAmountInBN (optional)Maximum input for ExactOut swaps
userStableTokenAccountPublicKey (optional)Override the user’s stablecoin ATA
userAssetTokenAccountPublicKey (optional)Override the user’s asset ATA
autoCreateAtaboolean (optional)Auto-create ATAs if missing (default: true)
remainingAccountsAccountMeta[] (optional)Extra accounts appended after SDK-derived oracle and transfer-hook accounts

LP Admin and Liquidity

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).

Create a Pair

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.
const { instruction: initPairIx } = await client.buildInitPairInstruction({
  liquidityProvider: wallet.publicKey,
  stableMint: USDC,
  assetMint: USTB,
  redemptionFeeBps: 50, // 0.5% Stable → RWA fee
  discountRateBps: 25,  // 0.25% RWA → Stable fee
  // admin: optional cached admin pubkey; fetched from GlobalConfig if omitted
});

// Or get a signed-ready VersionedTransaction directly:
const { transaction: initPairTx } = await client.buildInitPairTransaction({
  liquidityProvider: wallet.publicKey,
  stableMint: USDC,
  assetMint: USTB,
  redemptionFeeBps: 50,
  discountRateBps: 25,
});

Update Pair Configuration

const { instruction: updatePairIx } = await client.buildUpdatePairInstruction({
  liquidityProvider: wallet.publicKey,
  stableMint: USDC,
  assetMint: USTB,
  redemptionFeeBps: 10,
  discountRateBps: 15,
  paused: false,
});

Add and Remove Liquidity

const { instruction: addLiquidityIx, setupInstructions } =
  await client.buildAddLiquidityInstruction({
    liquidityProvider: wallet.publicKey,
    mint: USDC,
    amount: new BN(1_000_000_000),
  });

const { transaction: removeLiquidityTx } =
  await client.buildRemoveLiquidityTransaction({
    liquidityProvider: wallet.publicKey,
    mint: USTB,
    amount: new BN(500_000_000),
  });
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 a Pair

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.
const { instruction: closePairIx, setupInstructions } =
  await client.buildClosePairInstruction({
    liquidityProvider: wallet.publicKey,
    stableMint: USDC,
    assetMint: USTB,
    // Optional overrides:
    // lpStableTokenAccount, lpAssetTokenAccount — non-ATA recipient accounts
    // autoCreateAta: false                       — manage recipient ATAs externally
    // admin                                      — cached GlobalConfig admin
  });

const { transaction: closePairTx } = await client.buildClosePairTransaction({
  liquidityProvider: wallet.publicKey,
  stableMint: USDC,
  assetMint: USTB,
});
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.

Event Parsing

Parse SwapExecuted events from transaction logs:
// From a transaction signature
const events = await client.parseSwapEventsFromTransaction(signature);

for (const event of events) {
  console.log("Amount in:", event.amountIn.toString());
  console.log("Amount out:", event.amountOut.toString());
  console.log("Protocol fee:", event.protocolFeeAmount.toString());
  console.log("Direction:", event.swapDirection);
}
// From raw logs (e.g., from websocket subscription)
const events = client.parseSwapEventsFromLogs(logs);

Error Handling

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;
    }
  }
}

Amount Formatting

Convert between native token amounts (with decimals) and human-readable strings:
// Native to human-readable
MultiliquidClient.toHumanReadable(100_000_000n, 6);   // "100"
MultiliquidClient.toHumanReadable(99_800_000_000n, 9); // "99.8"

// Human-readable to native
MultiliquidClient.toNativeAmount("100", 6);            // 100_000_000n
MultiliquidClient.toNativeAmount("99.8", 9);           // 99_800_000_000n

Reading On-Chain State

Fetch All Swap State (Single RPC Call)

const state = await client.fetchSwapState(stableMint, assetMint, lp);

console.log("Protocol fees:", state.globalConfig.protocolFeesBps, "bps");
console.log("Redemption fee:", state.pair.redemptionFeeBps, "bps");
console.log("Discount rate:", state.pair.discountRateBps, "bps");
console.log("Pair paused:", state.pair.paused);
console.log("LP stable paused:", state.lpStableConfig.paused);
console.log("Program paused:", state.globalConfig.paused);

Fetch Individual Accounts

const globalConfig = await client.fetchGlobalConfig();
const pair = await client.fetchPair(pairAddress);
const assetConfig = await client.fetchAssetConfig(configAddress);
const lpStableConfig = await client.fetchLpStableConfig(lpStableConfigAddress);

PDA Derivation

All program accounts are deterministic PDAs. The SDK provides derivation helpers:
const [globalConfig] = client.deriveGlobalConfig();
const [assetConfig] = client.deriveAssetConfig(mint);
const [pair] = client.derivePair(lp, stableMint, assetMint);
const [vaultAuthority] = client.deriveVaultAuthority(lp);
const [vault] = client.deriveVault(mint, lp);
const [feeVault] = client.deriveFeeVault(stableMint);
const [lpStableConfig] = client.deriveLpStableConfig(stableMint, lp);
const [programAuthority] = client.deriveProgramAuthority();
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.

Integration Best Practices

Security

  1. Use Hardware Wallets: Never store private keys in code or environment variables for production
  2. Simulate First: Always call getQuote() or getQuoteViaSimulation() before executing
  3. Set Slippage: Always provide minAmountOut or maxAmountIn for production swaps
  4. Check Pause State: Call checkPauseStatus() before building transactions

Performance

  1. Use Client-Side Quotes: getQuote() is faster than getQuoteViaSimulation() for most use cases
  2. Set Compute Budget: Use computeUnitsConsumed from simulation to set appropriate compute limits
  3. Reuse Client: Create one MultiliquidClient instance and reuse across operations
  4. Use Built-In Registry: getPairs() returns instantly without RPC calls

Operational

  1. Priority Fees: Set appropriate priority fees via ComputeBudgetProgram.setComputeUnitPrice() for time-sensitive operations
  2. Confirmation: Wait for confirmed or finalized commitment before treating a swap as complete
  3. Event Monitoring: Use parseSwapEventsFromTransaction() to verify swap results after execution
  4. Error Recovery: Use parseSwapError() to categorize failures and implement appropriate retry logic

Testing

Devnet

Configure the client for devnet testing:
const connection = new Connection("https://api.devnet.solana.com");
const client = new MultiliquidClient({
  connection,
  cluster: "devnet",
});

// Devnet pairs use mock tokens — see SDK registry for addresses
const devnetPairs = client.getPairs();

Mainnet

const connection = new Connection("https://api.mainnet-beta.solana.com");
const client = new MultiliquidClient({
  connection,
  cluster: "mainnet-beta",
});

Support and Resources