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 viemviem 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 newBaselineSDK. The SDK has nochainIdparameter on its methods by design. defaultUseNative: trueuses 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:
| Function | Direction | Gas | Reason |
|---|---|---|---|
buyTokensExactOut | reserve to bToken | Cheap | Direct curve computation |
sellTokensExactIn | bToken to reserve | Cheap | Direct curve computation |
buyTokensExactIn | reserve to bToken | Expensive | Binary-searches for amountOut |
sellTokensExactOut | bToken to reserve | Expensive | Binary-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.
- Chain change -> new clients -> new SDK. Construction is cheap because the SDK holds client references and resolves the proxy address from the chain ID.
- Do not mix chains in one SDK. Never pass a
publicClientfor one chain and awalletClientfor another. - For cross-chain reads, call the hook with an explicit
chainIdper component, such asuseBaselineSDK(mainnet.id)anduseBaselineSDK(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
- npm:
@baseline-markets/sdk - Contracts: Mercury contract reference
- REST data API: API reference