Skip to main content

SDK (TypeScript)

@pendle/boros-sdk-public is a TypeScript wrapper around the Boros Open API and Send Txs Bot. It removes the boilerplate of building calldata, EIP-712 signing, and dispatching transactions. If you trade from Node, this is the recommended path.

For other languages — or if you want raw HTTP control — go to API instead. The SDK is a thin convenience layer; anything the SDK does, the API page documents end-to-end.

For the agent model the SDK depends on, see Agent Trading.

Install

npm install @pendle/boros-sdk-public viem

viem is a peer dependency. The SDK targets viem@2.x. The math helpers (FixedX18, estimateTickForRate, getRateAtTick) live in a separate package — install it when you need tick/rate conversion:

npm install @pendle/boros-offchain-math

Initialization

The SDK exposes two main classes:

  • Exchange — your read/write entry point. Wraps placing orders, cancelling, depositing, etc.
  • Agent — manages the agent keypair used to sign trading actions on behalf of your root.
import { createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { arbitrum } from 'viem/chains';
import { Agent, Exchange } from '@pendle/boros-sdk-public';

// 1. Root walletClient. For trading-only flows the account can be omitted —
// the agent signs every trading action. The root account is only needed
// for sensitive flows (deposit, approveAgent, withdraw).
const rootAccount = privateKeyToAccount(process.env.ROOT_PK as `0x${string}`);
const walletClient = createWalletClient({
account: rootAccount,
transport: http(process.env.RPC_URL),
chain: arbitrum,
});

// 2. Agent. Re-use a persisted private key in production; for first-time
// setup, `Agent.create(walletClient)` derives one deterministically from
// a root signature so users don't have to babysit an extra secret.
const agent = Agent.createFromPrivateKey(process.env.AGENT_PK as `0x${string}`);

// 3. Exchange.
const exchange = new Exchange(
walletClient,
rootAccount.address,
/* accountId */ 0,
[process.env.RPC_URL!],
agent,
);

The constructor signature is:

new Exchange(walletClient, root, accountId, rpcUrls, agent?)

agent is optional at construction time — set it later via exchange.setAgent(agent) if you bootstrap one with Agent.create() after the fact.

The SDK ships with the production backend pre-wired (api-boros.pendle.finance/apis). All endpoints — markets, accounts, calldata builders, and send-txs — go through this single host. For staging, override at startup:

import { setOpenApiBackendUrl } from '@pendle/boros-sdk-public';

setOpenApiBackendUrl('https://staging-api-boros.pendle.finance/apis');

Common flows

The SDK groups operations by who signs the calldata — root for sensitive actions, agent for everything else. Skim the table, then jump to whichever section you need.

FlowMethodSigned byBacked by
Generate + approve agentAgent.create(walletClient)exchange.approveAgent(agent)rootEIP-712 → Send Txs Bot
Check agent expiryexchange.getAgentExpiryTime()GET /v1/agents/expiry-time
Deposit collateralexchange.deposit(params)rootERC20 approve + on-chain deposit
Withdraw collateralexchange.withdraw(params)rootrequest-withdraw (auto-finalized after cooldown)
Pay treasury (gas top-up)exchange.payTreasury(params)agentcalldata builder + Send Txs Bot
Place single orderexchange.placeOrder(params)agentcalldata builder + Send Txs Bot
Place many in one txexchange.bulkPlaceOrders({orderRequests})agentcalldata builder + Send Txs Bot
Cancel ordersexchange.cancelOrders(params)agentcalldata builder + Send Txs Bot
Bulk cancelexchange.bulkCancelOrders(requests)agentcalldata builder + Send Txs Bot
Enter market (cross)exchange.enterMarkets(true, [marketId])agentcalldata builder + Send Txs Bot
Exit marketexchange.exitMarkets(true, [marketId])agentcalldata builder + Send Txs Bot
Cash transfer (cross↔isolated)exchange.cashTransfer(params)agentcalldata builder + Send Txs Bot
Read all marketsexchange.getAllMarkets(filters?)GET /v1/markets (paginated, cached 5 min)
Read order bookexchange.getOrderBook({marketId, tickSize})GET /v1/markets/order-book
Read your active ordersexchange.getOrdersPage({isActive: true})GET /v1/accounts/orders (cursor)
Read on-chain orders (no indexer lag)exchange.getActiveOrdersFromContract(params)direct Explorer contract read
Read entered marketsexchange.getEnteredMarkets(rootAddress)direct MarketHub contract read
Read user positionsexchange.getUserPositions(params)direct contract read
Market summary (mid/bid/ask APR)exchange.getMarketData(marketId)GET /v1/markets/by-ids + contract
Gas balance (USD)exchange.getGasBalance()GET /v1/accounts/gas-balance
List assetsexchange.getAssets()GET /v1/assets

Place a limit order

import { Side, TimeInForce, MarketAccLib, CROSS_MARKET_ID } from '@pendle/boros-sdk-public';

const market = (await exchange.getAllMarkets({ isUiWhitelisted: true }))[0];
const marketAcc = MarketAccLib.pack(rootAccount.address, 0, market.tokenId, CROSS_MARKET_ID);

const { result, executeResponse } = await exchange.placeOrder({
marketAcc,
marketId: market.marketId,
side: Side.LONG,
size: 10n ** 18n,
rate: 0.05, // 5% APR — backend rounds to nearest valid tick
tif: TimeInForce.GOOD_TIL_CANCELLED,
});

Pass rate (human-friendly decimal APR) or limitTick (raw integer tick) — exactly one. rate is the easy path; the backend rounds it to the nearest valid tick for the market. Use limitTick only if you've already computed the tick yourself (e.g. via estimateTickForRate from @pendle/boros-offchain-math).

ammId is optional and defaults to orderbook-only routing. Pass a specific AMM id to route through that AMM instead.

Place many orders in one transaction

bulkPlaceOrders accepts a heterogeneous list — mix single-order requests and per-market bulks in one call. One on-chain submission, one nonce, one gas charge.

await exchange.bulkPlaceOrders({
orderRequests: [
{
cross: true,
bulks: [{
marketId: market.marketId,
orders: {
tif: TimeInForce.GOOD_TIL_CANCELLED,
side: Side.LONG,
sizes: [10n ** 18n, 2n * 10n ** 18n],
limitTicks: [limitTick - 10, limitTick - 20],
},
cancelData: { ids: [], isAll: false, isStrict: false },
}],
},
],
});

Read your active orders (paginated)

import { OrderType } from '@pendle/boros-sdk-public';

let resumeToken: string | undefined;
const all = [];
do {
const page = await exchange.getOrdersPage({
isActive: true,
orderType: [OrderType.LIMIT],
limit: 200,
resumeToken,
});
all.push(...page.results);
resumeToken = page.resumeToken ?? undefined;
} while (resumeToken);

The SDK keeps cursor pagination explicit — there's no getAllOrders() helper. Backends impose per-call CU costs, and the SDK refuses to hide them behind sugar. See Computing Units.

If you need fresh on-chain truth (no indexer lag, useful right after a placeOrder returns), call exchange.getActiveOrdersFromContract({ marketAcc, marketId }) instead — it reads the Explorer contract directly.

Cancel everything in one market

await exchange.cancelOrders({
marketAcc,
marketId: market.marketId,
cancelAll: true,
orderIds: [],
});

Top up gas balance

The Send Txs Bot debits each agent-signed transaction from your off-chain gas budget (USD). Top it up via payTreasury:

await exchange.payTreasury({
isCross: true,
marketId: market.marketId,
usdAmount: 1, // credits ~$1 to gas budget
});

console.log('Gas balance (USD):', await exchange.getGasBalance());

Escape hatch — raw open-api access

The wrapped methods cover common trading flows. For everything else (leaderboard, OHLCV, settlement events, indicators, conditional orders), drop down to the codegen client:

import { getOpenApiSdk } from '@pendle/boros-sdk-public';

const sdk = getOpenApiSdk();

// Anything in the open-api spec is reachable here, fully typed.
const { data } = await sdk.markets.marketsControllerGetOhlcv({
marketId: 1,
resolution: '1h',
from: 1735689600,
to: 1735693200,
});

getOpenApiSdk() returns a typed client generated from the live swagger. New endpoints land here automatically when the SDK is republished — no waiting for hand-written wrappers. Use this whenever the wrapper doesn't exist or you need to pass a parameter the wrapper doesn't expose.

The SDK also re-exports the codegen response types under OpenApi.*:

import { OpenApi } from '@pendle/boros-sdk-public';

function summarize(market: OpenApi.MarketListItemResponse) {
return `${market.marketId} ${market.imData.symbol}`;
}

What the SDK doesn't do

  • WebSocket subscriptions — connect to the WebSocket feed using socket.io-client directly; see WebSocket.
  • Strategy logic — the SDK is plumbing. Indicators, sizing, risk management are yours to build.
  • Stop / take-profit ordersOrderType.TAKE_PROFIT_MARKET and OrderType.STOP_LOSS_MARKET exist on the type level but the SDK does not yet expose dedicated placeStopOrder / placeTakeProfitOrder methods. Use getOpenApiSdk() until they land. See Stop Orders.

End-to-end example

Bot Quickstart walks through a real grid bot built on top of the SDK — from approve-agent to a running reconcile loop. ~485 lines of TypeScript total, with full source embedded at the end of that page.


Migrating from @pendle/sdk-boros

@pendle/boros-sdk-public is the externally supported successor of the internal @pendle/sdk-boros package. Same trading surface — Exchange, Agent, calldata builders, codegen API client — minus internal-only helpers that did not belong on the public boundary.

Fresh integrators: skip this section. Lives here for users already wired up to the internal @pendle/sdk-boros.

TL;DR

ChangeOld importNew import
Package name@pendle/sdk-boros@pendle/boros-sdk-public
Backend wrapper namespaceBorosBackend (groups codegen client + URL setters)none — flat top-level exports
Open API codegen namespaceBorosBackend.CoreOpenApi
Open API accessorBorosBackend.getCoreSdk()getOpenApiSdk()
Open API URL setterBorosBackend.setCoreBackendUrl(url)setOpenApiBackendUrl(url)
Send Txs Bot accessorBorosBackend.getSendTxsBotSdk()removed — use getOpenApiSdk().sendTxs.*
Send Txs Bot URL setterBorosBackend.setSendTxsBotBackendUrl(url)removed — same host as Open API
Default Open API base URLhttps://api.boros.finance/corehttps://api-boros.pendle.finance/apis
Pendle V2 clientBorosBackend.getPendleV2Sdk() / BorosBackend.setPendleV2BackendUrl()removed
Structured-log clientLogStoreBackend.* namespaceremoved
UI helperscalculateIncentiveRange (from ui-support)removed
Strategy/fee/margin calculatorsCalculator exports (calculateExchangeFees, marginCalculator, strategyApr, strategyExecution, strategyFinder, strategyUtils)removed
Reward distributorsmultiTokenMerkleDistributor, lpRewardsDistributor, makerIncentiveRewardsDistributor, referralRewardsDistributorremoved
Exchange methodsseveral renames + removals — see Method-level changes
Token / TokenAmountcreateFromLifi removed; createFromBorosCore param type tweaked; logoURI constructor arg droppedsee same section

Everything else — Agent, Side, TimeInForce, MarketAccLib, Distributor (Pendle token), tick/rate math, the Explorer / MarketHub helpers, the error decoder, and the unchanged Exchange methods — keeps the same import path and signature.

Install

npm uninstall @pendle/sdk-boros
npm install @pendle/boros-sdk-public viem
# math helpers (tick/rate conversion):
npm install @pendle/boros-offchain-math

viem@2.x is required (see the Install section above). @pendle/boros-offchain-math is the same package the old SDK transitively depended on — install it explicitly if you import FixedX18, estimateTickForRate, or getRateAtTick directly.

Imports

Package rename is the bulk of the diff. Find-and-replace the module specifier in every import:

// old
import { Exchange, Agent, Side } from '@pendle/sdk-boros';

// new
import { Exchange, Agent, Side } from '@pendle/boros-sdk-public';

Most top-level entity exports (Exchange, Agent, Side, TimeInForce, MarketAccLib, CROSS_MARKET_ID, OrderType, Distributor, MarketHub helpers, error decoder, etc.) keep the same name. The two exceptions are the backend-client wrapper namespaces and a handful of Exchange methods — see the next two sections. Anything you imported under BorosBackend.*, LogStoreBackend.*, calculateIncentiveRange, or the strategy/margin/fee calculators is gone; see Removed surfaces.

Backend clients

The old package wrapped both codegen clients under a single BorosBackend namespace (and the structured-log client under LogStoreBackend). The new package drops the wrapper and exposes each piece at the top level. The codegen client itself was also regenerated against the public swagger and now points at a new default host — api-boros.pendle.finance/apis instead of api.boros.finance/core. Endpoint paths inside the SDK are still /v1/* in both versions; only the base URL differs. The regenerated client has fewer methods because internal-only endpoints (dApp settings, deposit-box intents, fear-greed-index, internal incentives admin, etc.) are not on the public swagger — calls that survived keep the same method names and request/response shapes.

Namespace + accessor rename:

// old
import { BorosBackend } from '@pendle/sdk-boros';
const core = BorosBackend.getCoreSdk();
const markets = await core.markets.marketsControllerGetAll({});
type MarketRow = BorosBackend.Core.MarketListItemResponse;

// new
import { getOpenApiSdk, OpenApi } from '@pendle/boros-sdk-public';
const sdk = getOpenApiSdk();
const markets = await sdk.markets.marketsControllerGetAll({});
type MarketRow = OpenApi.MarketListItemResponse;

Method names on the codegen client are unchanged. A handful of upstream NestJS @ApiProperty annotations were corrected before regeneration, so a few optional fields that used to type as any are now properly typed — TypeScript errors at the call site after the upgrade most likely fall here. The runtime payload did not change.

URL setters:

// Open API (renamed + new default host)
BorosBackend.setCoreBackendUrl('https://staging-api.boros.finance/core');
// ↓
setOpenApiBackendUrl('https://staging-api-boros.pendle.finance/apis');

// Send Txs Bot — removed; send-txs is now part of the Open API host
BorosBackend.setSendTxsBotBackendUrl(...); // no replacement

// Pendle V2 — removed
BorosBackend.setPendleV2BackendUrl(...); // no replacement

The env-driven prod/staging auto-switch from the old package is gone. Public SDK callers pass an explicit URL for staging; otherwise they inherit the production default baked into the package:

OPEN_API_BACKEND_URL = 'https://api-boros.pendle.finance/apis'

Send Txs Bot codegen client: removed. The send-txs endpoints (approve, trace, tx-status, tx-status-with-events, bulk-calls, dedicated/bulk-calls) are folded into the Open API SDK under sdk.sendTxs.* and live on the same host. The SendTxsBot namespace, SendTxsBotSdk interface, getSendTxsBotSdk() accessor, and setSendTxsBotBackendUrl() are gone. Callers using the high-level Exchange methods (placeOrder, bulkPlaceOrders, cancelOrders, payTreasury, …) do not see this — Exchange continues to dispatch through the bot internally.

// old
import { BorosBackend } from '@pendle/sdk-boros';
const bot = BorosBackend.getSendTxsBotSdk();
await bot.agent.agentControllerBulkDirectCall({ /* ... */ });

// new
import { getOpenApiSdk } from '@pendle/boros-sdk-public';
const sdk = getOpenApiSdk();
await sdk.sendTxs.sendTxsControllerBulkCalls({ /* ... */ });

Method-level changes

Exchange is the only class with breaking method-shape changes. Agent is unchanged at the API level. The other public entity classes (AccManager, Market, MarketHub, Distributor, Subaccount, VePendle, WrappedEth, Amm, the MarketAccLib helpers, Explorer reads) keep the same method signatures — only their internal references to the backend client were swapped from BorosCoreSdk to OpenApiSdk, which is transparent to callers. The Token / TokenAmount value classes had a small surface trim noted at the end of this section.

Exchange

A handful of Exchange methods were renamed or had their parameter / return types replaced. These don't show up under a package-name find-and-replace, so they fail at the call site after the import swap. The behavior of each method is unchanged — only the shape changed.

Renamed:

OldNewNotes
bulkPlaceOrdersV5(BulkPlaceOrderV5Params)bulkPlaceOrders(BulkPlaceOrderParams)V5 was the latest internal version; renamed to drop the version suffix. Parameter shape unchanged apart from the type alias.
getMarkets(GetMarketsParams): Promise<MarketsResponse>getAllMarkets(filters?: GetAllMarketsFilters): Promise<MarketListItemResponse[]>Now returns a flat array (already paginated through internally) and is cached for 5 min. Use filters.isUiWhitelisted for the equivalent of the old whitelisted flag.
getPnlLimitOrders(GetPnlLimitOrdersParams)getOrdersPage(GetOrdersPageParams)Cursor-paginated reads of your orders; same backing endpoint, clearer name. Iterate with resumeToken (see "Read your active orders" above).
getPnlLimitOrdersFromContract (private)getActiveOrdersFromContract(GetActiveOrdersFromContractParams)Was private in the old package; now public. Reads the Explorer contract directly — no indexer lag.
getUserPositions(GetPnlLimitOrdersParams)getUserPositions(GetUserPositionsParams)Same method name; the parameter type was renamed and tightened.

Removed (no replacement on Exchange):

MethodWhyWhat to do
bulkPlaceOrdersV2, bulkPlaceOrdersV4Legacy versions of bulkPlaceOrders.Use bulkPlaceOrders.
closeActivePositions(CloseActivePositionsParams)Wrapper around an internal dApp flow that batched a cancel-all + market-close sweep. Couples directly to UI assumptions.Do the equivalent yourself: bulkCancelOrders then a market placeOrder on the opposite side, or call the relevant /v1/calldata-builder/* endpoints via getOpenApiSdk().
updateSettings(UpdateSettingsParams)dApp-only account settings (display preferences, notification opt-ins).Not part of the public surface.
getCollaterals(...)Returned a dApp-shaped collateral summary.Use exchange.getAssets() plus the on-chain reads (getUserPositions) for collateral balances. Raw collateral metadata is also reachable via getOpenApiSdk().assets.*.
getAmmInfoByAmmId(ammId)AMM inspection helper used by the dApp's AMM page.Reach the same data via getOpenApiSdk().amm.* endpoints.
getCumulativePnl({ marketAcc, marketId })Time-series PnL aggregation used by the dApp's account page.Use the open-api /v1/accounts/* endpoints (settlement events, position-update events) via getOpenApiSdk().

approveAgentData / revokeAgentData (typed-data builders) and all other previously public methods (placeOrder, cancelOrders, bulkCancelOrders, enterMarkets, exitMarkets, cashTransfer, deposit, withdraw, payTreasury, scheduleCancel, approveAgent, getGasBalance, getOrderBook, getMarketData, getEnteredMarkets, getAmmCutOffTimestamp, getAssets, bulkSignAndExecute) keep the same signature.

Token / TokenAmount

Two small changes on the Token and TokenAmount value classes:

OldNewNotes
new Token(..., logoURI?)new Token(...)The optional logoURI constructor argument is gone — the public SDK does not carry token logos. Drop the trailing argument.
Token.createFromBorosCore(token: AssetResponse, ...)Token.createFromBorosCore(token: AssetItemResponse & { chainId?: number; dstTokenId?: number }, ...)The factory now takes the public-swagger response type (AssetItemResponse) and accepts optional chainId / dstTokenId for cross-chain plumbing. Same applies to TokenAmount.createFromBorosCore.
Token.createFromLifi(...) / TokenAmount.createFromLifi(...)removedLi.Fi integration lived in the dApp-only crossChainDeposit helpers, which are no longer shipped. Construct tokens from on-chain / Open API data instead.

Removed surfaces

Each row below names a symbol that used to be importable from @pendle/sdk-boros and is no longer exported from @pendle/boros-sdk-public. The "Why" column explains the reasoning; "What to do" is the migration path.

Old importWhy removedWhat to do
calculateIncentiveRange (from the ui-support re-export)Market-making UI helper — encodes the dApp's incentive-range rendering rules, not a stable trading API.Re-implement in your client if you need it. The logic is small and depends only on estimateTickForRate / getRateAtTick from @pendle/boros-offchain-math.
Calculator exports: calculateExchangeFees, marginCalculator, strategyApr, strategyExecution, strategyFinder, strategyUtils (plus their types)Internal strategy & margin simulator used by the dApp's strategy preview UI. Tightly coupled to dApp-side state shapes, not safe as a public API.Use the on-chain reads exposed by Exchange (getUserPositions, getActiveOrdersFromContract) and Open API endpoints under /v1/markets/*, /v1/accounts/* via getOpenApiSdk(). The contract + API are the source of truth; the calculator was a UI cache layer.
BorosBackend.getPendleV2Sdk(), BorosBackend.setPendleV2BackendUrl(), BorosBackend.PendleV2.*Pendle V2 (non-Boros) backend client. Out of scope for the Boros SDK.If you actually need Pendle V2 data, call the V2 API directly. The Boros SDK no longer ships a client for it.
LogStoreBackend.* namespaceInternal structured-logging dispatch — only used by the dApp.No replacement; not part of the public surface. Use your own observability stack.
multiTokenMerkleDistributorAbi, multiTokenMerkleDistributorContractMulti-token Merkle distributor for internal incentive programs.The single-token Pendle Distributor is still public. For multi-token programs, read the on-chain contract directly.
lpRewardsDistributor, makerIncentiveRewardsDistributor, referralRewardsDistributorInternal helper wrappers around the same multi-token distributor — UI conveniences.Same as above.
aggregatorHelpers, crossChainDeposit, collateralSwapperdApp-side deposit/aggregator/cross-chain plumbing.Not part of the public trading surface. If you need cross-chain deposits, drive them via your own wallet/aggregator.

Things that stayed public despite touching internal-feeling concerns: the single-token Pendle Distributor (Merkle claim for PENDLE rewards), the Explorer and MarketHub contract helpers, the multicall utility, the error decoder, and the in-process cache used by Exchange for getAllMarkets / getAssets. If you depended on any of these, no action is needed.

Mapping cheat sheet

Symbol-for-symbol substitutions:

// Backend wrapper namespace — removed; everything below moves to top-level imports
BorosBackend.getCoreSdk()getOpenApiSdk()
BorosBackend.Core.MarketListItemResponse → OpenApi.MarketListItemResponse
BorosBackend.setCoreBackendUrl(url)setOpenApiBackendUrl(url)
BorosBackend.getSendTxsBotSdk()removed (use getOpenApiSdk().sendTxs.*)
BorosBackend.setSendTxsBotBackendUrl(url)removed (same host as Open API)
BorosBackend.SendTxsBot.*removed (folded into OpenApi)
BorosBackend.getPendleV2Sdk() → removed
BorosBackend.setPendleV2BackendUrl(url) → removed
LogStoreBackend.* → removed

// Exchange methods (call-site renames)
exchange.bulkPlaceOrdersV5(...) → exchange.bulkPlaceOrders(...)
exchange.getMarkets(...) → exchange.getAllMarkets(...) // return type changed
exchange.getPnlLimitOrders(...) → exchange.getOrdersPage(...)
exchange.getPnlLimitOrdersFromContract(...) → exchange.getActiveOrdersFromContract(...) // was private
exchange.bulkPlaceOrdersV2 / V4removed (use bulkPlaceOrders)
exchange.closeActivePositions(...) → removed
exchange.updateSettings(...) → removed
exchange.getCollaterals(...) → removed
exchange.getAmmInfoByAmmId(...) → removed
exchange.getCumulativePnl(...) → removed

// UI helpers / calculators / distributors
calculateIncentiveRange(...)removed (re-implement client-side)
Calculator.*removed (use on-chain reads + Open API)
multiTokenMerkleDistributorAbi → removed
lpRewardsDistributor → removed
makerIncentiveRewardsDistributor → removed
referralRewardsDistributor → removed
Distributor (Pendle single-token claim) → unchanged

Verifying migration

After bumping the package, a clean tsc --noEmit is the fastest signal. Three failure modes are expected:

  1. Import-not-found on BorosBackend, LogStoreBackend, calculateIncentiveRange, Calculator.*, or a removed distributor. Replace per the tables above.
  2. Property-not-found on exchange.* — likely one of the renames (bulkPlaceOrdersV5, getMarkets, getPnlLimitOrders, …). See Exchange method changes.
  3. Type narrowing on an OpenApi.* response field. Some upstream @ApiProperty annotations were corrected; fields that previously typed as any may now be string | undefined, number, etc. Adjust the call site — the runtime payload is unchanged.

Once those are clean, behavioral parity is the same as the old package: calldata format, EIP-712 typed data, and endpoint contract were not changed by the extraction.