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`](https://www.npmjs.com/package/@baseline-markets/sdk).

<Callout type="info">
  Use the lower-level [contract reference](/docs/contracts) when you need raw ABIs or contract-level details.
</Callout>

## Install

```bash
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

```ts
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](/docs/launch). The SDK exposes the same factory actions as the contracts.

### Single-transaction launch (zrp)

The simplest path is `sdk.launch`, which deploys the bToken and an initialized zero-reserve pool in one transaction. The full supply is minted straight into the pool, so the caller needs no balance or approvals. `initialFdv` sets the launch valuation in WAD reserve units, quoted when circulating supply first exits the frozen zone (5% of supply).

```ts
import { zeroHash } from 'viem';
import type { Address } from 'viem';

const WAD = 10n ** 18n;
const reserve = '0x4200000000000000000000000000000000000006' as Address; // Base WETH

const { hash, bToken } = await sdk.launch(
  {
    name: 'Example Token',
    symbol: 'EXAMPLE',
    totalSupply: 1_000_000n * WAD,
    salt: zeroHash,
    reserve,
    initialFdv: 10n * WAD, // launch FDV of 10 WETH at the frozen-zone exit
    creator: walletClient.account!.address,
    feeRecipient: walletClient.account!.address,
    creatorFeePct: 5n * 10n ** 17n, // 50%
    swapFeePct: 10n ** 16n, // 1%
  },
  { confirmations: 1 },
);
```

To build unsigned calls instead (for `wallet_sendCalls` or agent flows), use `sdk.calls.launch` — in `zrp` mode it returns a single `launch` call.

### Standard launch

Standard launches seed the pool with reserve liquidity and keep part of the supply circulating.

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

```ts
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.

```ts
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](/docs/contracts/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.

```ts
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.

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

## Stake

```ts
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

```ts
// 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

```ts
// 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`.

```ts
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:

```ts
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:

```ts
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:

```tsx
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, Base Sepolia, HyperEVM, and Robinhood Chain.

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

```ts
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:

```tsx
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`:

```tsx
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`](https://www.npmjs.com/package/@baseline-markets/sdk)
* Contracts: [Mercury contract reference](/docs/contracts)
* REST data API: [API reference](/docs/contracts/api)
