Resources

SDK

Install and use the Baseline SDK to read from and write to Baseline contracts with viem.

The Baseline SDK is the recommended way for apps and developers to interface with Baseline contracts. It exposes a single BaselineSDK class that allows apps to launch tokens, perform swaps, stake, borrow and leverage.

The package is available on NPM at @baseline-markets/sdk.

Use the lower-level contract reference when you need raw ABIs or contract-level details.

Install

npm install @baseline-markets/sdk viem
# or
bun add @baseline-markets/sdk viem

viem is a peer dependency. Bring your own viem clients from your app, wallet framework or RPC setup.

Quickstart

import { BaselineSDK } from '@baseline-markets/sdk';
import { createPublicClient, createWalletClient, custom, http } from 'viem';
import { base } from 'viem/chains';

const publicClient = createPublicClient({
  chain: base,
  transport: http(),
});

const walletClient = createWalletClient({
  chain: base,
  transport: custom(window.ethereum),
});

const sdk = new BaselineSDK(publicClient, walletClient, {
  defaultUseNative: true,
  approvals: 'infinite',
});

const bToken = '0x9fDbDE76236998Dc2836FE67A9954eDE456A1D63' as const;

const price = await sdk.activePrice(bToken);
const reserve = await sdk.getReserve(bToken);
const quote = await sdk.quoteBuyExactOut(bToken, 100n);

Caveats:

  • Each SDK instance is bound to a single chain from publicClient.chain. To use another chain, build new viem clients and instantiate a new BaselineSDK. The SDK has no chainId parameter on its methods by design.
  • defaultUseNative: true uses native ETH paths by default where supported.

Launch a token

For full launch context, see Launch. The SDK exposes the same factory actions as the contracts.

First, create the bToken. The full supply is minted to the caller, and only that caller can create the pool for the token.

import { zeroHash } from 'viem';

const WAD = 10n ** 18n;
const totalSupply = 1_000_000n * WAD;

const { bToken } = await sdk.createBToken(
  'Example Token',
  'EXAMPLE',
  totalSupply,
  zeroHash,
  { confirmations: 1 },
);

Next, create the pool. The caller must own and approve the bTokens and reserve assets used to initialize it. initialBLV: 0n lets the protocol calculate the starting BLV.

import { erc20Abi, zeroHash } from 'viem';
import type { Address } from 'viem';

const WAD = 10n ** 18n;
const totalSupply = 1_000_000n * WAD;
const STANDARD_CREATOR_FEE = 5n * 10n ** 17n; // 50%
const STANDARD_SWAP_FEE = 10n ** 16n; // 1%

function toWad(amount: bigint, decimals: number): bigint {
  if (decimals === 18) return amount;
  if (decimals > 18) return amount / 10n ** BigInt(decimals - 18);
  return amount * 10n ** BigInt(18 - decimals);
}

const account = walletClient.account;
if (!account) throw new Error('Wallet client must include an account');

const creator = account.address;
const reserve = '0x4200000000000000000000000000000000000006' as Address; // Base WETH

// Seed the pool with the creator's available bToken and reserve balances.
const [initialPoolBTokens, initialPoolReserves, reserveDecimals] =
  await Promise.all([
    sdk.getTokenBalance(bToken, creator),
    sdk.getTokenBalance(reserve, creator),
    publicClient.readContract({
      address: reserve,
      abi: erc20Abi,
      functionName: 'decimals',
    }),
  ]);

await sdk.ensureApproval(bToken, sdk.proxy, initialPoolBTokens, {
  confirmations: 1,
});
await sdk.ensureApproval(reserve, sdk.proxy, initialPoolReserves, {
  confirmations: 1,
});

// Set non-zero values to launch with Baseline Options.
const initialCollateral = 0n;
const initialDebt = 0n;

// Start at a 5% premium to backed circulating supply.
const reserves = toWad(initialPoolReserves + initialDebt, reserveDecimals);
const circulatingSupply = totalSupply - initialPoolBTokens;
const bookPrice = (reserves * WAD) / circulatingSupply;
const initialActivePrice = (bookPrice * 105n) / 100n;

await sdk.createPool(
  {
    bToken,
    initialPoolBTokens,
    reserve,
    initialPoolReserves,
    initialActivePrice,
    initialBLV: 0n, // use zero to auto-calculate BLV
    creator,
    feeRecipient: creator,
    creatorFeePct: STANDARD_CREATOR_FEE,
    swapFeePct: STANDARD_SWAP_FEE,
    createHook: false,
    claimMerkleRoot: zeroHash,
    initialCollateral,
    initialDebt,
  },
  { confirmations: 1 },
);

Swaps

The SDK exposes four BSwap functions, but only two are gas-efficient on-chain:

FunctionDirectionGasReason
buyTokensExactOutreserve to bTokenCheapDirect curve computation
sellTokensExactInbToken to reserveCheapDirect curve computation
buyTokensExactInreserve to bTokenExpensiveBinary-searches for amountOut
sellTokensExactOutbToken to reserveExpensiveBinary-searches for amountIn

The ExactIn / ExactOut naming refers to what's exact from the user's perspective. On-chain cost depends on whether the contract receives the natural input to the curve math (direct computation) or the other side (binary search).

Buying bTokens

Quote off-chain via quoteBuyExactIn (view call, solver is free), then execute via buyTokensExactOut with the quoted amountOut. Avoid calling buyTokensExactIn on-chain unless you can't pre-compute the amount.

const quote = await sdk.quoteBuyExactIn(bToken, reservesIn);

await sdk.buyTokensExactOut(bToken, quote.tokensOut, maxReservesIn, {
  confirmations: 2,
  onSimulateError: (error) => {
    console.error(error);
  },
});

Selling bTokens

Call sellTokensExactIn directly. It is already the efficient path. Avoid calling sellTokensExactOut on-chain unless you can't pre-compute the amount.

await sdk.sellTokensExactIn(bToken, amountIn, minReservesOut, {
  confirmations: 2,
  onSimulateError: (error) => {
    console.error(error);
  },
});

Stake

const amountToStake = 1_000n * 10n ** 18n;

await sdk.ensureApproval(bToken, sdk.proxy, amountToStake, {
  confirmations: 1,
});

await sdk.deposit(bToken, amountToStake, {
  confirmations: 1,
});

const position = await sdk.getStakedAccount(bToken, user);

if (position.earned > 0n) {
  const { amount: claimed } = await sdk.claim(bToken, {
    confirmations: 1,
  });
}

Borrow

// Borrow

// Read the user's current collateral and debt.
const creditAccount = await sdk.getCreditAccount(bToken, user);

// Check the maximum borrowable reserve amount.
const maxBorrow = await sdk.getMaxBorrow(bToken, user);

// Preview the account state after borrowing.
const borrowPreview = await sdk.previewBorrow(bToken, user, debtAmount);

// Borrow reserve assets against bToken collateral.
await sdk.borrow(bToken, debtAmount, recipient, { confirmations: 1 });

// Repay

// Preview how much collateral is redeemed and debt is repaid.
const repayPreview = await sdk.previewRepay(bToken, recipient, reservesIn);

// Repay debt with reserve assets.
await sdk.repay(bToken, reservesIn, recipient, { confirmations: 1 });

Leverage

// Leverage

// Quote the target collateral and swap bounds.
const leverageQuote = await sdk.quoteLeverage(
  bToken,
  collateralIn,
  leverageFactor,
);

// Add collateral and borrow against it in one transaction.
await sdk.leverage(
  bToken,
  leverageQuote.targetCollateral,
  collateralIn,
  leverageQuote.maxSwapReservesIn,
  { confirmations: 1 },
);

// Simulate leverage without submitting a transaction.
const simulatedLeverage = await sdk.simulateLeverage(
  bToken,
  leverageQuote.targetCollateral,
  collateralIn,
  leverageQuote.maxSwapReservesIn,
);

// Deleverage

// Simulate deleverage before unwinding collateral.
const simulatedDeleverage = await sdk.simulateDeleverage(
  bToken,
  collateralToSell,
  minSwapReservesOut,
);

// Sell collateral and repay debt in one transaction.
await sdk.deleverage(bToken, collateralToSell, minSwapReservesOut, {
  confirmations: 1,
});

Approvals

Execution methods do not automatically approve ERC20 spends. Use approve, getAllowance or ensureApproval before actions that transfer reserve tokens or bTokens. For protocol actions, the spender is the Relay address exposed as sdk.proxy.

const allowance = await sdk.getAllowance(reserve, owner, sdk.proxy);

if (allowance < maxReservesIn) {
  await sdk.ensureApproval(reserve, sdk.proxy, maxReservesIn, {
    confirmations: 1,
    policy: 'infinite',
  });
}

// Or approve manually.
await sdk.approve(reserve, sdk.proxy, maxReservesIn, {
  confirmations: 1,
});

Use defaultUseNative on the SDK config, or useNative on supported payable buy and repay calls, when the reserve asset should be sent as native value. borrow supports outputNative, and claim supports asNative.

ABIs

Baseline Mercury ABIs are exported from the SDK as abis for lower-level contract reads, writes or error decoding:

import { abis } from '@baseline-markets/sdk';

const result = await publicClient.readContract({
  address: sdk.proxy,
  abi: abis.bLens,
  functionName: 'activePrice',
  args: [bToken],
});

Error handling

Write methods throw SDKError, which exposes a .kind discriminator:

import { SDKError } from '@baseline-markets/sdk';

try {
  await sdk.buyTokensExactOut(bToken, amountOut, maxIn);
} catch (err) {
  if (err instanceof SDKError) {
    switch (err.kind) {
      case 'user_rejected':
      case 'insufficient_funds':
      case 'reverted':
      case 'network':
      case 'wallet':
      case 'unknown':
        break;
    }
  }
}

The original viem error is preserved on err.cause.

React and wagmi

wagmi returns viem clients, so you can wrap SDK construction in a hook:

import { useMemo } from 'react';
import { BaselineSDK } from '@baseline-markets/sdk';
import { useChainId, usePublicClient, useWalletClient } from 'wagmi';

export function useBaselineSDK(chainId?: number) {
  const walletChainId = useChainId();
  const targetChainId = chainId ?? walletChainId;
  const publicClient = usePublicClient({ chainId: targetChainId });
  const { data: walletClient } = useWalletClient({ chainId: targetChainId });

  return useMemo(() => {
    if (!publicClient) return null;
    return new BaselineSDK(publicClient, walletClient ?? undefined);
  }, [publicClient, walletClient]);
}

useWalletClient() returns undefined until the user connects. The SDK still works for reads in that state, and sdk.hasWallet tells you whether write actions are available.

Supported networks

The SDK only works on chains with Mercury deployments in the package address book. The current deployments are Ethereum mainnet, Base and Base Sepolia.

Use supportedChainIds to gate your app against the package's current runtime support:

import { supportedChainIds } from '@baseline-markets/sdk';

if (!supportedChainIds.includes(chainId)) {
  // Prompt the user to switch networks.
}

Switching networks

The SDK is single-chain: the chain is baked into publicClient, so a BaselineSDK instance can only talk to one network. When the user switches network, create new clients and a new SDK.

With the hook above, this happens automatically. Passing chainId to wagmi's usePublicClient and useWalletClient returns different client references per chain; those references update the useMemo dependencies and construct a fresh BaselineSDK.

  1. Chain change -> new clients -> new SDK. Construction is cheap because the SDK holds client references and resolves the proxy address from the chain ID.
  2. Do not mix chains in one SDK. Never pass a publicClient for one chain and a walletClient for another.
  3. For cross-chain reads, call the hook with an explicit chainId per component, such as useBaselineSDK(mainnet.id) and useBaselineSDK(base.id).

Include sdk.chainId in query keys so cached reads stay scoped to the network:

import { useQuery } from '@tanstack/react-query';

function Price({ bToken }: { bToken: `0x${string}` }) {
  const sdk = useBaselineSDK();
  const { data: price } = useQuery({
    queryKey: ['baseline', 'activePrice', sdk?.chainId, bToken],
    queryFn: () => sdk!.activePrice(bToken),
    enabled: !!sdk,
  });

  return <span>{price?.toString()}</span>;
}

For writes, gate actions on sdk.hasWallet:

import { useMutation } from '@tanstack/react-query';
import { SDKError } from '@baseline-markets/sdk';

function BuyButton({ bToken, amount, maxIn }: {
  bToken: `0x${string}`;
  amount: bigint;
  maxIn: bigint;
}) {
  const sdk = useBaselineSDK();
  const buy = useMutation({
    mutationFn: () =>
      sdk!.buyTokensExactOut(bToken, amount, maxIn, { confirmations: 1 }),
    onError: (err) => {
      if (err instanceof SDKError && err.kind === 'user_rejected') return;
      throw err;
    },
  });

  return (
    <button disabled={!sdk?.hasWallet || buy.isPending} onClick={() => buy.mutate()}>
      {buy.isPending ? 'Confirming' : 'Buy'}
    </button>
  );
}

References