TypeScript SDK

Architecture & API Layers

How the SDK is layered, what each layer does for you, and how to pick the right one.

The SDK exposes the same operations at four levels of abstraction, from one-line wrapper methods down to raw Anchor instructions. Most integrations only ever touch the top layer; the lower layers exist as escape hatches when you need to compose instructions yourself. This page assumes you have a client and wrappedAccount from Getting Started.

The four layers

LayerWhatExampleWhen to use
4 — Account wrapperMarginfiAccountWrapper methods that auto-fill every parameter from the account and client, then call Layer 3wrappedAccount.makeBorrowTx(bankAddress, "100")Default. Standard lending UIs, bots, scripts.
3 — Transaction buildersFree functions that do the heavy lifting: oracle cranks, integration refreshes, lookup tables, transaction splittingmakeBorrowTx({ ...params })You manage your own account/client state or need to pass parameters the wrapper does not expose.
2 — Instruction buildersFree functions (and wrapper make*Ix methods) that return TransactionInstruction[] with accounts derived for you, and nothing elsemakeBorrowIx({ ...params })Composing with other programs, flash-loan bodies, custom fee payers.
1 — Raw instructionsThin Anchor wrappers from the /instructions entry; you derive every account yourselfinstructions.makeBorrowIx(program, accounts, args)Almost never.

Each layer calls the one below it, so a wrapper call produces exactly the same transactions as calling the Layer 3 builder with the same inputs.

Layer 4: MarginfiAccountWrapper

MarginfiAccountWrapper wraps a MarginfiAccount together with a Project0Client. Its methods look up the bank, the mint's token program, oracle prices, integration metadata and lookup tables from the client, call the matching Layer 3 builder, and return the finished transactions.

import { MarginfiAccountWrapper } from "@0dotxyz/p0-ts-sdk";

// You only provide the bank and the amount (UI units)
const borrowResult = await wrappedAccount.makeBorrowTx(bankAddress, "100");

// Behind the scenes the wrapper calls the Layer 3 makeBorrowTx() with
// marginfiAccount, authority, bankMap, oraclePrices,
// assetShareValueMultiplierByBank, bankMetadataMap, luts, program and
// connection — all pulled from the account and client.

for (const tx of borrowResult.transactions) {
  const sig = await wallet.sendTransaction(tx, connection);
  await connection.confirmTransaction(sig, "confirmed");
}

The wrapper also exposes the account's state (address, authority, balances, activeBalances, healthCache, isDisabled, isFlashLoanEnabled, ...) and getUnderlyingAccount() / getClient() when you need the raw objects for a lower layer.

Method families

FamilyMethodsDocumented on
Core lendingmakeDepositTx, makeWithdrawTx, makeBorrowTx, makeRepayTxDeposit, Withdraw, Borrow & Repay
LoopingmakeLoopTx, makeBridgedLoopTxLooping
Position swapsmakeSwapCollateralTx, makeSwapDebtTx, makeBridgedSwapCollateralTx, makeBridgedSwapDebtTx, makeRepayWithCollatTx, makeRollPtTxPosition Swaps
Flash loansmakeFlashLoanTx, makeBeginFlashLoanIx, makeEndFlashLoanIxFlash Loans
Integration banksmakeKaminoDepositTx, makeKaminoWithdrawTx, makeDriftDepositTx, makeDriftWithdrawTxIntegration Banks & Native Stake
Account lifecyclemakeCloseAccountIx, makeAccountTransferToNewAccountTx, makeTransferPositionsTx, makePulseHealthIxAccounts
Health, max amounts, e-modesimulateHealthCache, computeHealthComponentsFromCache, computeFreeCollateralFromCache, computeAccountValue, computeNetApy, computeMaxBorrowForBank, computeMaxWithdrawForBank, computeMaxDepositForBank, getActiveEmodePairs, getActiveEmodeWeightsByBank, getEmodeImpactsHealth, Max Amounts & E-mode
Single instructionsmakeDepositIx, makeWithdrawIx, makeBorrowIx, makeRepayIxLayer 2, below

Layer 3: Transaction builders

Layer 3 is where the optimisation happens. The builders live in src/services/account/actions and are exported from the package root under the same names as the wrapper methods (makeDepositTx, makeBorrowTx, makeLoopTx, makeKaminoDepositTx, makeJuplendDepositTx, ...). Each one takes a single params object and returns complete, ready-to-sign transactions.

What a builder does for you, beyond building the instruction itself:

  • Oracle cranking — runs the smart-crank analysis and prepends a Switchboard feed-update transaction when a borrow/withdraw would otherwise fail the health check on stale prices.
  • Integration refreshes — prepends Kamino reserve refresh, Drift market update and Jupiter Lend rate update instructions to the action transaction when the account touches integration banks.
  • Lookup tables — picks the right LUT subset for the banks involved and compiles v0 transactions with them.
  • Splitting and metadata — returns one or more transactions tagged with type, signers and addressLookupTables, plus actionTxIndex pointing at the one that holds your action.

All of these mechanics are documented on Transactions. Calling a builder directly looks like this — the parameter names are MakeBorrowTxParams:

import { makeBorrowTx } from "@0dotxyz/p0-ts-sdk";

const bank = client.getBank(bankAddress)!;
const { tokenProgram } = await wrappedAccount.getMintDataFromBank(bank);

const borrowResult = await makeBorrowTx({
  // MakeBorrowIxParams
  program: client.program,
  bank,
  bankMap: client.bankMap,
  tokenProgram,
  amount: "100", // UI units
  marginfiAccount: wrappedAccount.getUnderlyingAccount(),
  authority: wrappedAccount.authority,
  opts: { wrapAndUnwrapSol: true },
  // extra inputs a Tx builder needs
  connection,
  oraclePrices: client.oraclePriceByBank,
  assetShareValueMultiplierByBank: client.assetShareValueMultiplierByBank,
  bankMetadataMap: client.bankIntegrationMap,
  luts: client.addressLookupTables,
  crossbarUrl: "https://crossbar.0.xyz", // optional Switchboard Crossbar
});

// borrowResult: TransactionBuilderResult = { transactions, actionTxIndex }

Use Layer 3 when you keep your own copy of account/bank state, want to pass crossbarUrl, or need the few builders that have no wrapper method yet (makeJuplendDepositTx, makeJuplendWithdrawTx, makeBulkWithdrawTx, makeBulkRepayTx).

Layer 2: Instruction builders

Instruction builders return an InstructionsWrapper{ instructions: TransactionInstruction[], keys: Keypair[] } — and nothing else. They derive PDAs and remaining accounts (health-check banks and oracles) for you, but you are responsible for oracle cranks, integration refreshes, lookup tables, blockhashes and splitting.

The wrapper exposes the four core ones with the same auto-fill convenience as its *Tx methods:

const depositIxs = await wrappedAccount.makeDepositIx(usdcBank.address, "250");
const borrowIxs = await wrappedAccount.makeBorrowIx(solBank.address, "1.5");
// also: makeWithdrawIx(bank, amount, withdrawAll?, opts?) and
//       makeRepayIx(bank, amount, repayAll?, opts?)

const tx = new Transaction().add(...depositIxs.instructions, ...borrowIxs.instructions);

The free functions take explicit params (MakeBorrowIxParams and friends):

import { makeBorrowIx } from "@0dotxyz/p0-ts-sdk";

const borrowIxs = await makeBorrowIx({
  program: client.program,
  bank: usdcBank,
  bankMap: client.bankMap,
  tokenProgram,
  amount: "100",
  marginfiAccount: wrappedAccount.getUnderlyingAccount(),
  authority: wrappedAccount.authority,
  // no connection, luts or oracle handling here
});

// borrowIxs.instructions: TransactionInstruction[]
// borrowIxs.keys: Keypair[] (extra signers, e.g. a temporary wSOL account)

Integration-bank instruction builders follow the same pattern: makeKaminoDepositIx / makeKaminoWithdrawIx, makeDriftDepositIx / makeDriftWithdrawIx, makeJuplendDepositIx / makeJuplendWithdrawIx. For example, a Kamino deposit:

import { makeKaminoDepositIx } from "@0dotxyz/p0-ts-sdk";

const kaminoDepositIxs = await makeKaminoDepositIx({
  program: client.program,
  bank: kaminoBank,
  tokenProgram,
  amount: "50",
  accountAddress: wrappedAccount.address,
  authority: wrappedAccount.authority,
  group: client.group.address,
  reserve: kaminoReserve, // KaminoReserve from client.bankIntegrationMap
});

Reach for Layer 2 when you are:

  1. composing Project 0 instructions with other programs in one transaction,
  2. building the body of a flash loan,
  3. batching several account operations, or
  4. using a custom fee payer or your own priority-fee / compute-budget instructions.

Layer 1: Raw instructions

Raw instructions talk to the program IDL directly. You derive every account and add every remaining account yourself, and a program upgrade can break your code with no compatibility shim. Use Layer 2 instead unless you have a very specific reason not to.

The @0dotxyz/p0-ts-sdk/instructions entry point default-exports an object of Anchor-based builders (makeDepositIx, makeBorrowIx, makeWithdrawIx, makeRepayIx, makeBeginFlashLoanIx, makeEndFlashLoanIx, makePulseHealthIx, makeAccountTransferToNewAccountIx, makeCloseAccountIx, makeInitMarginfiAccountIx, the Kamino/Drift/Jupiter Lend variants, and the admin/group instructions):

import instructions from "@0dotxyz/p0-ts-sdk/instructions";
import BN from "bn.js";

const borrowIx = await instructions.makeBorrowIx(
  client.program,
  {
    marginfiAccount: wrappedAccount.address,
    bank: bankAddress,
    destinationTokenAccount: destinationAta,
    tokenProgram, // e.g. TOKEN_PROGRAM_ID
    // group and authority are inferred by Anchor unless you override them
  },
  { amount: new BN(100_000_000) }, // native units, not UI
  remainingAccounts // health-check banks + oracles — you build this list
);

The 0.1.10 program upgrade inserts a required account into six instructions, and the Layer 2+ builders switch layouts automatically while raw instructions do not. If you hand-roll any of the affected instructions, read the Program Upgrade page before the mainnet flip.

Sync instructions

src/sync-instructions.ts holds synchronous, Anchor-free builders with hardcoded discriminators. They are internal and simulation-only: the SDK uses them to estimate transaction size (for example sizing a flash loan before the real instructions are built). Their output is never sent on-chain, they always emit the 0.1.10 layout regardless of the live program, and there is no /sync-instructions subpath export. Do not build production transactions with them.

Choosing a layer

  • Standard lending flows (deposit, borrow, loop, swap, integration banks) — use the wrapper (Layer 4). This covers the vast majority of integrations.
  • You maintain your own account or bank state, need crossbarUrl, or need a builder without a wrapper method — call the transaction builder (Layer 3) directly; you get identical output.
  • You are composing with another program, writing a flash-loan body, or need a custom fee payer — use instruction builders (Layer 2) and assemble the transaction yourself (see Transactions for cranks, LUTs and size limits).
  • None of the above applies and you still want the IDL-level call — raw instructions (Layer 1), and you own the upgrade maintenance.

Summary

// Most of the time
const result = await wrappedAccount.makeBorrowTx(bankAddress, "100");

// Custom flows with the same optimisations
const result = await makeBorrowTx({ ...params });

// Cross-program composition
const ixs = await makeBorrowIx({ ...params });

The SDK is built to absorb the complexity of cranks, refreshes and lookup tables so you do not have to. Start at the top and only drop a layer when you have a concrete reason.

On this page