# Project 0 Pay Spend crypto against your DeFi portfolio. Project 0 Pay let's you spend crypto against your DeFi portfolio. Instead of liquidating your portfolio to cover everyday expenses, you borrow against your existing, yielding DeFi positions and use that borrow to pay your bills. You keep using your existing debit or credit card — no new cards, no change in spending behavior, and your crypto stays working for you. ## How It Works [#how-it-works] Pay follows a simple three-step lifecycle: * **Onboarding** — Connect your wallet, link your bank account or credit card, and configure your preferences. This is a one-time setup. * **Dashboard** — Your home base. View your connected account, collateral, upcoming offramp schedule, pending purchases, and past offramp transactions. * **Offramp** — You'll be notified once it's time to cover your spending. Select the transactions you want to pay off, borrow against your collateral, and offramp the funds to your bank account. ## Getting Started [#getting-started] ### Onboarding [#onboarding] The onboarding flow walks you through setting up Pay in a few steps: 1. **Connect your bank account** — Pay uses Plaid to securely link your bank. We will never store your banking credentials. Plaid only provides read-only access to your transaction history so Pay can see your card spending. 2. **Select your account** — Choose which bank account (e.g. checking, credit card) you want to track spending from. 3. **Set transaction preferences (optional)** — Filter which transactions you want included in your offramps. You can: * Select specific spending categories (e.g. food, transportation, shopping) * Set a purchase price range (min/max amount) These preferences are used to pre-select transactions when you offramp. You can always override them later. 4. **Set an offramp schedule (optional)** — Choose how often you want to be reminded to offramp: * **Weekly** — Pick a day of the week * **Biweekly** — Every two weeks on a chosen day * **Monthly** — First of the month, last of the month, or a specific date 5. **Set notification preferences (optional)** — Choose how you want to be notified when it's time to offramp: * **Email** — Provide a recipient address * **Telegram** — Connect your telegram to the project 0 pay notification bot 6. **Review & confirm** — Review all your settings and complete the setup. Everything can be changed later from your dashboard. Steps 3–5 are optional and can be skipped during onboarding. You can always configure them later from the settings panel. ### Dashboard [#dashboard] After completing onboarding, you land on the Pay dashboard. From here you can view your connected bank account, available collateral, upcoming offramp date, pending purchases for the current period, and past offramp transactions. You can also add collateral directly from the dashboard. All of your onboarding preferences — bank account, transaction filters, schedule, and notifications — can be updated at any time from the settings panel. Settings auto-save when you close the panel. You can fully disconnect your bank account and delete all associated data from the settings panel. This revokes Plaid access and removes all stored data. This action cannot be undone — you'll need to go through onboarding again to reconnect. ### Offramp Process [#offramp-process] When you're ready to offramp (either on schedule or manually), the flow has three steps: #### 1. Select Transactions [#1-select-transactions] Review your bank transactions from the current offramp period. Transactions matching your preferences are pre-selected, but you can manually select or deselect individual transactions, filter by category, or skip this step entirely and enter a custom borrow amount in the next step. #### 2. Borrow [#2-borrow] Borrow against your collateral to cover the selected transaction total (or your custom amount). You can borrow in USDC, USDT, or SOL. The borrow is executed as an on-chain transaction that you sign from your connected wallet — your collateral remains deposited and continues earning yield. #### 3. Offramp [#3-offramp] The borrowed funds are converted from crypto to fiat via MoonPay and sent directly to your bank account. MoonPay handles the conversion and transfer. Their own KYC requirements and processing times apply. ## Important Things to Know [#important-things-to-know] ### Availability [#availability] Pay is currently only available in the United States. We will be expanding to further locations soon. If you are outside the US and interested in using Pay, please reach out. The offramp step is powered by MoonPay, which has its own geographic and token-specific restrictions. See the MoonPay supported cryptocurrencies list for full details on which tokens are available in your region. ### Data & Privacy [#data--privacy] * Project 0 does not store your banking credentials. Plaid handles authentication directly with your bank. * Project 0 does not have access to any sensitive banking data beyond read-only transaction history. * You can revoke access and delete all stored data at any time from the settings panel. ## FAQ [#faq] ### Do I need a new card? [#do-i-need-a-new-card] No. Project 0 Pay works with your existing debit or credit card. There's no need to get a new card. You'll keep earning your credit card points and don't have to change your spending habits. ### Can I delete my account? [#can-i-delete-my-account] Yes. Go to the settings panel from your dashboard, open the Connect tab, and click Disconnect & Delete. This will revoke Plaid access and permanently delete all stored account and transaction data. You'll need to go through onboarding again if you want to reconnect. ### When will my funds arrive in my bank account? [#when-will-my-funds-arrive-in-my-bank-account] Offramp processing times depend on MoonPay. Typically, funds arrive within 1–3 business days after the offramp is initiated, but this can vary depending on your bank and region. You can track the status of your offramp from your transaction history on the dashboard. --- # Getting Started Learn about the first DeFi native prime broker on Solana. Welcome to the documentation for Project 0 (P0), a permissionless prime broker built on Solana. This site provides an accessible overview of the protocol alongside enough technical depth to satisfy experienced builders and integrators. P0 is built on **mrgnLendv2**, a battle-tested borrow-lending program with a powerful on-chain risk engine. P0 extends this foundation with cross-venue collateral, enabling users to collateralize assets from multiple DeFi venues and borrow against them in a single unified margin account. ## Get Started [#get-started] What P0 is, how it works, and why permissionless prime brokerage matters. Dive into the protocol's architecture, risk engine, interest rates, liquidation mechanics, and more. Start building with the official P0 SDK. Deposit, borrow, and manage accounts programmatically. Step-by-step guides for users, developers, and liquidators. --- # What is Project 0 An on-chain prime broker that unifies collateral, risk, and margin across Solana DeFi. ## The Problem: Fragmented Capital [#the-problem-fragmented-capital] Solana has a thriving lending ecosystem. Kamino, Drift, Jupiter Lend, and others collectively secure billions in TVL and serve hundreds of thousands of users. But each of these venues operates in isolation. Your $100 on Drift, $200 on Kamino, and $300 on Jupiter are three separate pools of capital. Each venue is blind to the others. This means you overcollateralize separately everywhere. You track separate health factors, separate liquidation thresholds, separate interest rates. Moving capital from one opportunity to another requires unwinding positions, eating slippage, and losing yield during the transition. As the Solana DeFi ecosystem grows more complex, this fragmentation gets worse, not better. Fragmented collateral is a hidden tax on capital efficiency, liquidation risk, rates, and user experience. Until now, there hasn't been a clean way to remove that tax without giving up control to centralized systems. ## Project 0: A DeFi Native Prime Broker [#project-0-a-defi-native-prime-broker] In traditional finance, prime brokers solve this exact problem. Institutional firms give clients a single margin account that works across multiple exchanges and counterparties. The client posts collateral once and deploys it across venues and strategies. This is how professional capital allocation works. Project 0 brings this model on-chain. P0 is a permissionless, non-custodial prime broker on Solana. One margin account, unified credit, unified risk, across the venues you already use. If you have assets on Kamino, Drift, and P0's native market, you can borrow against your entire combined portfolio in one click. P0 is not a lending aggregator. It is not a dashboard. It is not a managed vault where a centralized manager captures spreads and charges fees. It is infrastructure for unified margin and credit origination across DeFi venues. Unlike traditional prime brokers, P0 has no application process, no minimum balance, and no relationship manager. Anyone with a Solana wallet can use it. ## How It Works [#how-it-works] P0's architecture is lightweight. It inserts a self-custodial account between you and the underlying venues you use. This account enables credit origination against your whole portfolio and ensures unified liquidations can be processed to protect solvency. When you deposit into a third-party venue, P0 creates a balance in the corresponding Bank. The risk engine applies the appropriate asset weights and oracle prices to value that collateral alongside everything else in your account. Your borrowing power is the sum of all your positions, regardless of which venue they originated from. You keep earning yield from the originating venue. Depositing into a Kamino Bank on P0 means you earn what any Kamino lender earns. The interest comes from borrowers on Kamino, not P0. Borrowing is done against P0's native liquidity pools. Third-party venue positions serve as collateral only. Borrowing is limited to P0 native banks. ## What You Can Do [#what-you-can-do] Cross-venue unified margin unlocks use cases that are impossible with isolated venues: * **Interest rate arbitrage.** If Kamino offers a high lending rate on USDC and P0 has a low borrow rate, deposit on Kamino, use that deposit as collateral on P0, borrow, and loop the spread. This yield was previously inaccessible because the two venues couldn't see each other. * **Cross-venue leverage.** Use your combined collateral across Kamino, Drift, and P0 to take leveraged positions with better capital efficiency than any single venue can offer. * **Borrow against yield-bearing positions.** Keep assets in their highest-yielding venue while borrowing against them on P0. No need to unwind, swap, and re-deploy. ## Supported Venues [#supported-venues] P0 currently provides unified margin across: * **Project 0** * **Kamino** (Main, Maple, Jito, JLP, and Marinade markets) * **Drift** (Main, JLP, LST markets) * **Jupiter Lend** (coming soon...) Together, this covers roughly 98% of Solana lending TVL. Once we have Solana lending markets covered we will be moving onto derivatives starting with perps which opens up even more sophisticated trades. For a deeper look at how P0's protocol works, continue to the [Protocol Overview](/protocol-overview/architecture). --- # Agent Skill How to install and use the P0 agent skill to interact with Project 0 via AI coding tools. Prefer a hosted integration? The [MCP server](/guides/mcp-server) gives any agent the same tools over MCP or REST, with no keypair on the agent side. P0 publishes an open-source [Agent Skill](https://agentskills.io) that teaches AI coding agents how to deposit, borrow, and manage positions on Project 0. The skill works with any compatible agent -- Claude Code, OpenCode, Cursor, Windsurf, and others. ## Installation [#installation] Install the skill so your agent can discover it. Choose the method that matches your tool. ### npx (any compatible agent) [#npx-any-compatible-agent] ```bash npx skills add 0dotxyz/skill ``` This downloads the skill into your project's `.claude/skills/` directory (or equivalent for your agent). The skill auto-activates when the agent detects a relevant task. ### Claude Code [#claude-code] | Scope | Path | | -------------- | ------------------------------------------------------------- | | All projects | `~/.claude/skills/project0/SKILL.md` | | Single project | `.claude/skills/project0/SKILL.md` (relative to project root) | ```bash mkdir -p ~/.claude/skills/project0 curl -o ~/.claude/skills/project0/SKILL.md \ https://raw.githubusercontent.com/0dotxyz/skill/main/SKILL.md ``` ### OpenCode [#opencode] | Scope | Path | | -------------- | --------------------------------------------------------------- | | All projects | `~/.config/opencode/skills/project0/SKILL.md` | | Single project | `.opencode/skills/project0/SKILL.md` (relative to project root) | ```bash mkdir -p ~/.config/opencode/skills/project0 curl -o ~/.config/opencode/skills/project0/SKILL.md \ https://raw.githubusercontent.com/0dotxyz/skill/main/SKILL.md ``` ## Environment Setup [#environment-setup] No configuration is needed for read-only queries (bank rates, strategies, wallet balances). For on-chain operations, add these to a `.env` file in your project. ### Solana RPC URL [#solana-rpc-url] Required for all on-chain operations. ``` RPC_URL=https://mainnet.helius-rpc.com/?api-key=YOUR_KEY ``` Also recognized: `SOLANA_RPC_URL`, `HELIUS_RPC_URL`. [Helius](https://www.helius.dev) offers a free tier. ### Wallet Address (optional) [#wallet-address-optional] Your wallet's public key. Allows the agent to check balances and discover accounts without loading the keypair. ``` WALLET_ADDRESS=5Utci...your_public_key ``` If not set, the agent derives it from the keypair when needed. ### Wallet Keypair [#wallet-keypair] Required for signing transactions. Path to your keypair JSON file. ``` WALLET_KEYPAIR=~/.config/solana/id.json ``` If not set, the agent will ask you for the path. You can also provide it directly in your prompt (e.g., "you have access to a wallet at `./dev.json`"). To generate a new keypair: ```bash solana-keygen new --outfile ~/.config/solana/id.json ``` This keypair controls your funds. Never commit it to version control or share it publicly. ### P0 Account Address (optional) [#p0-account-address-optional] If your wallet has multiple P0 accounts, set this to skip the account selection prompt. Each account has isolated positions and risk. ``` P0_ACCOUNT=3p16...your_account_address ``` If not set and you have multiple accounts, the agent will list them and ask which to use. Single accounts are used automatically. ### Jupiter API Key (optional) [#jupiter-api-key-optional] Only needed if a strategy requires swapping tokens (e.g., SOL to bbSOL before depositing). ``` JUPITER_API_KEY=YOUR_KEY ``` Also recognized: `JUP_API_KEY`. Get a free key at [portal.jup.ag](https://portal.jup.ag) (60 requests per minute). ## How to Use [#how-to-use] The skill activates automatically when you ask your agent to interact with Project 0. Try prompts like: * "What are the best deposit yields on Project 0?" * "What are the highest-yield strategies on Project 0?" * "Deposit my SOL on Project 0 for the best yield." * "Borrow USDC against my Project 0 collateral." * "Check my Project 0 portfolio health." * "What is the cheapest stablecoin to borrow on Project 0?" The agent follows a structured workflow: 1. **Checks your wallet** -- fetches token balances via the P0 API. 2. **Fetches P0 data** -- pulls current deposit/borrow rates and strategies. 3. **Recommends an action** -- matches your holdings to the best opportunities with specific numbers. 4. **Waits for approval** -- you review and confirm before anything is executed. 5. **Executes and reports** -- sends transactions and provides [Solscan](https://solscan.io) links for verification. ## What the Skill Covers [#what-the-skill-covers] | Category | Operations | | --------------------- | --------------------------------------------------------------------- | | **Wallet balances** | Fetch token holdings and USD values for any wallet | | **Bank rates** | Deposit APYs and borrow APYs across P0, Kamino, and Drift venues | | **Strategies** | Precomputed rate arbitrage and looping strategies with projected APYs | | **Deposits** | Deposit into P0 native, Kamino, and Drift venues | | **Borrowing** | Borrow stablecoins against cross-venue collateral | | **Withdrawals** | Partial or full withdrawal from any position | | **Repayment** | Repay specific amounts or all debt | | **Swaps** | Token swaps via Jupiter (when needed for a strategy) | | **Health monitoring** | Health factor, free collateral, net APY, liquidation risk | For the full TypeScript SDK reference, see [TypeScript SDK](/typescript-sdk/overview). --- # Borrowing How to borrow against your cross-venue collateral with unified margin on Project 0. P0 is a prime broker. If you have $100 on P0, $200 on Kamino, and $300 on Drift, you can borrow against your entire $600 portfolio in one click. Your collateral across all integrated venues is unified under a single account with a single health factor. ## How Borrowing Works [#how-borrowing-works] When you borrow, the protocol: 1. Evaluates your Account health using **Initial weights** (the more conservative tier). 2. Checks that your weighted collateral exceeds your weighted liabilities, including the new borrow. 3. Transfers the borrowed tokens to your wallet. Your **maximum borrow** depends on the asset weights of your collateral and the liability weights of the asset you are borrowing: ``` Max Borrow = (Sum of Weighted Collateral) / (Liability Weight of Borrow Asset) ``` Collateral can come from any integrated venue: P0's native market, Kamino, Drift, or natively staked SOL. The risk engine evaluates your entire cross-venue portfolio as a single unit. ## Borrowing via the App [#borrowing-via-the-app] 1. Ensure you have collateral deposited on [app.0.xyz](https://app.0.xyz). This can be native P0 deposits, Kamino positions, Drift positions, or staked SOL. 2. Navigate to the borrowing page and select the asset you want to borrow. 3. Enter the amount. The interface shows your maximum borrowing capacity based on your entire portfolio. 4. Review the health impact. The preview shows how your health factor will change. 5. Confirm the transaction in your wallet. Borrows are sourced from P0's native credit market. Cross-venue positions serve as collateral only; you cannot borrow cross-venue assets. ## After You Borrow [#after-you-borrow] Once you have active debt, monitor your **health score** on the portfolio page. If it drops below zero, your account can be [liquidated](/protocol-overview/liquidation). Do not borrow to your maximum capacity; leave a buffer for normal price fluctuations. See [Managing Your Account](/guides/managing-your-account) for a full guide on health scores, using multiple accounts, and adjusting your positions with collateral and debt swaps. ## What Happens If You Get Liquidated [#what-happens-if-you-get-liquidated] If your Maintenance health drops below zero, a third-party liquidator can step in to partially repay your debt in exchange for seizing some of your collateral at a discount. * **Partial liquidation.** Liquidators can only bring your health back up to zero, not beyond. You keep remaining collateral. * **You lose a premium.** The liquidator receives your collateral at a 2.5% discount (classic) or up to 10% (receivership). An additional 2.5% goes to the insurance fund in classic liquidation. * **You can act first.** At any point before liquidation, you can deposit more collateral or repay some debt to restore your health and avoid the premium. Liquidation is not instant. You can monitor your health on the portfolio page and take action before it reaches zero. For the full mechanics, see [Liquidation](/protocol-overview/liquidation). ## Emode and Borrowing [#emode-and-borrowing] If your collateral and borrow asset are correlated (e.g., SOL/LST or USDC/USDT), you automatically receive enhanced borrowing power via [Emode](/guides/emode). Adding a non-paired borrow removes the benefit for the entire Account. See the [Emode guide](/guides/emode) for worked examples and multi-account strategies. ## Repaying Debt [#repaying-debt] There is **no repayment deadline**. You can hold your borrow as long as your account remains healthy. Interest accrues continuously, but there is no fixed repayment schedule or maturity date. When you are ready to repay: * **Partial repayment:** Specify the amount to repay. * **Full repayment:** Use the "repay all" option to close the debt completely, including any interest accrued up to that moment. There are **no fees** to repay on P0. Because interest accrues just before any transaction, the exact repayment amount may be slightly more than what is previewed. Use "repay all" to ensure you close the position fully. --- # CLI How to install, configure, and use the marginfi CLI for advanced Project 0 and operator workflows. The `mfi` CLI is the advanced interface for interacting directly with the marginfi protocol that powers Project 0. It is best suited for operators, integrators, admins, and power users who want fine-grained control from the terminal. Most users should use [app.0.xyz](https://app.0.xyz). ## What the CLI Is Best For [#what-the-cli-is-best-for] * Inspecting groups, banks, and accounts directly from the terminal * Managing marginfi accounts without using the web app * Running deposit, withdraw, borrow, repay, and health-check workflows * Operating integration flows for venues like Kamino, Drift, and JupLend * Running config-driven admin and infrastructure workflows The CLI is broad, so the built-in help is the live source of truth for flags and examples: ```bash mfi -h mfi -h mfi -h ``` Main command groups: ```text mfi group mfi bank mfi profile mfi account mfi kamino mfi drift mfi juplend mfi util ``` ## Install [#install] Build from source: ```bash cargo build -p marginfi-v2-cli ``` Install locally: ```bash cargo install --path clients/rust/marginfi-cli --locked --force ``` If you are using a tagged release build instead of installing from source, download the published `mfi` archive from the marginfi releases page. ## Create a Profile [#create-a-profile] Profiles are central to how the CLI works. They store your cluster, keypair path, RPC URL, and optional default group and account. Many commands use the active profile defaults when you omit those inputs. Typical setup: ```bash mfi profile create \ --name mainnet \ --cluster mainnet \ --keypair-path ~/.config/solana/id.json \ --rpc-url https://api.mainnet-beta.solana.com mfi profile set mainnet mfi profile show ``` You can also use a different saved profile for a single command: ```bash mfi --profile staging bank get ``` Profile defaults matter. For example, `mfi group get` can use the active profile group, and `mfi account get` can use the active profile account. ## Inspect the Protocol [#inspect-the-protocol] Before sending any transaction, start by discovering the current state of your profile, group, banks, and accounts. ```bash mfi profile show mfi group get mfi bank get-all mfi account list ``` These commands are the fastest way to confirm which group you are targeting, which banks are available, and which accounts exist for the current authority. ## Select or Create an Account [#select-or-create-an-account] Most user-facing workflows happen through the `account` command group. Create an account: ```bash mfi account create ``` List your accounts and inspect one in detail: ```bash mfi account list mfi account get ``` Set the default account for the current profile: ```bash mfi account use mfi account get ``` Once you set a default account, many account commands can use it automatically. ## Core Account Actions [#core-account-actions] The CLI expects bank arguments as public keys, not token symbols. ### Deposit [#deposit] ```bash mfi account deposit 10 ``` If you want the CLI to deposit only the remaining amount allowed before hitting the bank limit, use: ```bash mfi account deposit 10 --up-to-limit ``` ### Withdraw [#withdraw] Withdraw a specific amount: ```bash mfi account withdraw 5 ``` Withdraw the entire balance: ```bash mfi account withdraw 0 --all ``` ### Borrow [#borrow] ```bash mfi account borrow 3 ``` ### Repay [#repay] Repay a specific amount: ```bash mfi account repay 3 ``` Repay the full liability: ```bash mfi account repay 0 --all ``` ### Check Health [#check-health] Use these commands to inspect your balances and refresh your account health: ```bash mfi account get mfi account pulse-health ``` If you are managing active borrows, compare this with [Managing Your Account](/guides/managing-your-account) for the protocol-level guidance on health, buffers, and liquidation risk. ## Transaction Behavior [#transaction-behavior] By default, the CLI simulates a transaction first, then signs and broadcasts it if simulation succeeds. * `--no-send-tx` simulates and prints an unsigned base58 transaction instead of signing and sending * `-y` or `--skip-confirmation` skips the interactive safety prompt * `--compute-unit-price ` sets a priority fee in micro-lamports * `--compute-unit-limit ` overrides the compute unit limit * `-l` or `--lookup-table ` adds address lookup tables for versioned transactions * `--json` enables machine-oriented output where supported Most state-changing commands ask you to type the active profile name to continue. This is a safety check, not a simple yes/no confirmation. `--no-send-tx` is especially useful for external signing and multisig workflows, but some commands that create fresh accounts or require extra signers are designed for direct send mode. ## Config-Driven Workflows [#config-driven-workflows] More complex workflows use JSON config files instead of long flag lists. Use `--config-example` to print a template, then copy it into a real file for your environment. ```bash mfi bank add --config-example mfi bank add --config ./configs/bank/add/my-bank.json ``` Other common examples: ```bash mfi bank update --config ./configs/bank/update/config.json.example mfi group create --config ./configs/group/create/config.json.example mfi kamino add-bank --config ./configs/kamino/add-bank/config.json.example mfi drift withdraw --config ./configs/drift/withdraw/config.json.example mfi juplend add-bank --config ./configs/juplend/add-bank/config.json.example ``` The config templates live under `clients/rust/marginfi-cli/configs/` in the marginfi repository. Several templates are intentionally minimal because the CLI can derive deterministic accounts and integration-specific addresses automatically. ## Integration Command Groups [#integration-command-groups] The CLI also supports venue-specific integrations: * `mfi kamino ...` for Kamino integration banks and reserve interactions * `mfi drift ...` for Drift integration banks and spot market interactions * `mfi juplend ...` for JupLend integration banks and positions At a high level, these command groups cover bank creation, integration account initialization, deposits, withdrawals, and reward workflows. Some integration commands intentionally derive required accounts from a smaller root input set. Always check `mfi -h` and the example configs before filling fields manually. ## Common Gotchas [#common-gotchas] * Bank inputs are raw public keys, not asset symbols * The active profile can silently change which group or account a command targets * `account list` depends on the current authority and active group context * `account use` updates the profile default account for future commands * `--no-send-tx` is powerful, but it is not a drop-in replacement for every mutating workflow * Built-in help is more reliable than stale command snippets, especially as new subcommands are added ## Further Reading [#further-reading] * [Managing Your Account](/guides/managing-your-account) * [Liquidating Accounts](/guides/liquidating-accounts) * [Agent Skill](/guides/agent-skill) * [marginfi CLI README](https://github.com/0dotxyz/marginfi-v2/blob/main/clients/rust/marginfi-cli/README.md) --- # Emode How Efficiency Mode affects your borrowing power and how to use it effectively on Project 0. **Efficiency Mode** (Emode) automatically gives you better borrowing power when your collateral is correlated with your debt. For example, lending SOL and borrowing an LST gives you a much higher LTV than lending SOL and borrowing USDC, because SOL and LSTs are highly price-correlated. You do not need to toggle anything. P0 applies the most favorable Emode setting to your account automatically. ## Emode Tiers [#emode-tiers] Emode pairings are organized into **tiers**. Assets within the same category (e.g., LSTs) are grouped into tiers based on their risk profile, with higher tiers receiving better Emode weights. You can explore all available tiers and their current weights in the app. ## Common Emode Pairings [#common-emode-pairings] ### SOL / LST [#sol--lst] The most common Emode pairing. Lending SOL and borrowing an LST (or vice versa) benefits from the high price correlation between the two assets. LSTs are split into two tiers: | Collateral | Borrow | Emode Weight | | ---------- | ------------------------------- | ------------ | | SOL | LST Tier 1 (e.g. jitoSOL, mSOL) | Up to 90% | | SOL | LST Tier 2 | Up to 85% | ### JLP / SOL and Stables [#jlp--sol-and-stables] JLP has Emode pairings with both SOL and stablecoins, making it flexible collateral for different strategies. | Collateral | Borrow | Emode Weight | | ---------- | ------------------------------- | ------------ | | JLP | SOL | 85% | | JLP | Stable Tier 1 (e.g. USDC, USDT) | Up to 94% | The weights shown are illustrative; check the app for current values. ## How Emode Affects Your Health [#how-emode-affects-your-health] Emode changes your asset weights, which directly changes your account health and borrowing capacity. ### Example: Emode Active [#example-emode-active] You deposit $100 of SOL and borrow jitoSOL. With Emode, SOL has a 90% initial asset weight (instead of the standard 80%): ``` Borrowing power = $100 * 0.90 = $90 of weighted collateral ``` You can borrow up to $90 worth of jitoSOL (assuming a 100% liability weight). ### Example: Emode Lost [#example-emode-lost] Now imagine you also borrow $10 of USDC from the same Account. USDC has no Emode pairing with SOL, so the Emode benefit is lost entirely. SOL falls back to its standard 80% weight: ``` Borrowing power = $100 * 0.80 = $80 of weighted collateral Total debt = $10 USDC + existing jitoSOL borrow ``` Your weighted collateral just dropped from $90 to $80. If you were close to your borrowing limit, this could put your account in a liquidatable state. ## The "Worst Case" Rule [#the-worst-case-rule] When you borrow multiple assets, P0 uses the **least favorable** Emode benefit across all your borrows. It does not matter how much of each asset you borrow; a single non-preferential borrow removes the Emode advantage for the entire Account. ### Worked Example [#worked-example] You are lending SOL. SOL has the following Emode entries: * Borrowing an LST Tier 1 asset (e.g. jitoSOL): Emode weight = 90% * Borrowing an LST Tier 2 asset: Emode weight = 85% * Borrowing USDC: No Emode entry (standard weight = 80%) | What you borrow | SOL weight applied | Why | | ------------------------------ | ------------------ | ----------------------------------------------- | | LST Tier 1 asset only | 90% | Full Emode benefit | | LST Tier 1 + LST Tier 2 assets | 85% | Worst of the two Emode rates | | LST Tier 1 asset + USDC | 80% | USDC has no Emode entry, falls back to standard | | LST Tier 2 asset only | 85% | Emode benefit for Tier 2 | Before borrowing a new asset, check whether it has an Emode pairing with your collateral. Adding a non-paired borrow to an Account with active Emode can significantly reduce your health. ## Using Multiple Accounts [#using-multiple-accounts] The best way to preserve Emode benefits across different strategies is to use separate Accounts: * **Account 1:** Lend SOL, borrow jitoSOL. Emode active at 90%. Use this for your SOL/LST loop. * **Account 2:** Lend JLP, borrow stablecoins. Emode active at up to 94%. Use this for JLP strategies. * **Account 3:** Lend SOL, borrow USDC. No Emode, standard weights. Use this for general borrowing. If all three positions were in a single Account, the USDC borrow would break the SOL/LST Emode, and mixing borrows would break the JLP Emode. By splitting, each Account gets its optimal rate. You can create as many Accounts as you want on P0. Each Account has up to 16 positions. ## Emode for Isolated Assets (Not Yet Implemented) [#emode-for-isolated-assets-not-yet-implemented] Normally, Isolated assets have an asset weight of zero and cannot serve as collateral. A planned extension to Emode will allow Isolated assets to receive a non-zero weight when paired with a correlated borrow. This would be useful for assets that are too risky for general collateral but safe enough to borrow against in specific correlated scenarios. ## FAQ [#faq]
Why did my health drop when I borrowed a new asset? You likely lost an Emode benefit. If your existing positions had an Emode pairing (e.g., SOL/LST) and the new borrow does not (e.g., USDC), the Emode advantage is removed from your entire Account. Check whether the new borrow has an Emode entry with your collateral.
Can I get Emode back after losing it? Yes. Repay the non-preferential borrow. Once all your remaining borrows have Emode pairings with your collateral, the Emode benefit is restored automatically.
Do I need to do anything to activate Emode? No. P0 applies Emode automatically. Unlike Aave, there is no toggle or mode selection. You get the best rate your positions qualify for.
For the full technical details on Emode tags, configuration, and the tag resolution algorithm, see [Protocol Overview: Emode](/protocol-overview/emode). --- # Flashloans How to use flashloans on Project 0 for atomic, uncollateralized borrowing within a single transaction. **Flashloans** allow you to borrow any amount from P0 without collateral, as long as you repay it within the same transaction. If the loan is not repaid, the entire transaction reverts as if it never happened. Flashloans are a primitive for automated traders, HFT operators, liquidation bots, and integrators. Under the hood, P0's [looping and strategies](/guides/looping-and-strategies) features use flashloans to build leveraged positions atomically. ## How Flashloans Work [#how-flashloans-work] A flashloan is a pair of instructions that bookend a transaction: ``` lending_account_start_flashloan (any operations: borrows, swaps, deposits, etc.) lending_account_end_flashloan ``` Between the start and end: * You can borrow **as much as you want** from any Bank, with no health check. * At the end of the transaction, the risk engine runs a final health check. * If your Account is healthy after everything executes, the transaction succeeds. * If not, the entire transaction reverts. ## Use Cases [#use-cases] ### Leverage Loops [#leverage-loops] Build a leveraged position in a single transaction: ``` Start flashloan Borrow 60 USDC Swap USDC to SOL on Jupiter Deposit SOL as collateral End flashloan ``` ### Liquidation [#liquidation] Borrow the funds needed to repay a liquidatee's debt, execute the liquidation, and repay the flashloan from the seized collateral: ``` Start flashloan Liquidate unhealthy account (seize collateral) Swap seized collateral to debt token Repay the liquidatee's debt End flashloan ``` ### Arbitrage [#arbitrage] Capture price discrepancies across venues without tying up capital: ``` Start flashloan Borrow Asset A Sell A on Venue 1 at higher price Buy A on Venue 2 at lower price Repay flashloan End flashloan ``` ## Fees [#fees] There are **no fees** to execute flashloans on P0. ## Rules and Restrictions [#rules-and-restrictions] * **Cannot be called via CPI.** Flashloans must be invoked directly in a transaction, not through cross-program invocation from another program. * **Start must come first.** `lending_account_start_flashloan` must appear at the beginning of the transaction (after optional compute budget instructions). * **End must come last.** `lending_account_end_flashloan` must be the final instruction. The risk engine check happens here. * **One flashloan per transaction.** You cannot nest flashloans. For programmatic flashloan implementation, see the [TypeScript SDK](/typescript-sdk/overview). --- # Lending & Earning Yield How to deposit assets on Project 0 and earn yield across P0's native market and integrated venues. Lending is the simplest way to use P0. Deposit your idle assets and earn interest from borrowers automatically. Because P0 is a prime broker, you can deposit into multiple venues and have all your positions count as collateral under a single account. ## How Lending Yield Works [#how-lending-yield-works] When you deposit assets into a P0 Bank, your deposit joins a lending pool that borrowers draw from. Borrowers pay interest on their loans, and that interest is distributed proportionally to all lenders in the pool. Your yield depends on: * **Utilization** -- Higher utilization (more borrowing) means higher lending rates. * **Interest rate curve** -- Each Bank has its own rate curve that determines how rates change with utilization. See [Interest Rates](/protocol-overview/interest-rates) for details. * **Compounding** -- Interest compounds every time anyone interacts with the Bank, passively growing your balance. You do not need to claim or harvest yield. Interest accrues automatically through the share system. Your deposit is simply worth more over time. ## Depositing on P0's Native Market [#depositing-on-p0s-native-market] 1. Connect your wallet at [app.0.xyz](https://app.0.xyz). 2. Browse available Banks on the lending page. Each Bank shows the current APY and total deposits. 3. Select the asset you want to deposit (e.g., USDC, SOL). 4. Enter the amount. 5. Review and confirm the transaction in your wallet. Your deposit immediately begins earning interest. ## Depositing Cross-Venue Collateral [#depositing-cross-venue-collateral] P0 integrates with third-party venues like **Kamino** and **Drift**, so you can deposit into those venues through P0 and have your positions count toward your unified collateral. Currently supported: * **Kamino** -- Main, Maple, Jito, JLP, and Marinade markets * **Drift** -- Lending markets When you deposit into a cross-venue Bank (e.g., Kamino Main Market USDC), P0 inserts a self-custodial account between you and the underlying venue. Your yield comes from **the originating venue's borrowers**, not from P0. For example, depositing into the Kamino USDC Bank earns the same interest as any other Kamino USDC depositor. This is the core of P0's prime broker functionality: your deposits across multiple venues are unified under a single account with a single health factor. You can then [borrow](/guides/borrowing) against your entire cross-venue portfolio. Cross-venue deposits use venue-specific instructions (e.g., `kamino_deposit` instead of the standard deposit). The app handles this automatically. ## Withdrawing [#withdrawing] You can withdraw your deposits at any time, as long as sufficient liquidity exists in the Bank (i.e., not all funds are currently borrowed out). * **Partial withdrawal:** Specify the amount you want to withdraw. * **Full withdrawal:** Use the "withdraw all" option to close your position completely, including any interest accrued up to that moment. If a Bank is at very high utilization (close to 100%), you may not be able to withdraw your full balance immediately. The high interest rates at extreme utilization incentivize borrowers to repay, which frees up liquidity for withdrawals. ## Emissions and Incentives [#emissions-and-incentives] Some Banks offer additional **token incentives** on top of interest yield. These campaigns distribute bonus tokens to depositors (and sometimes borrowers) on a pro-rata basis, typically airdropped on Wednesdays. Some campaigns are **paired**: you must both lend one asset and borrow another to qualify (e.g., lend an LST and borrow SOL). See [Emissions](/protocol-overview/emissions) for full details on how campaigns work, minimum thresholds, and paired emissions. --- # Liquidating Accounts A guide for third-party liquidators on Project 0, covering account discovery, evaluation, and execution. Liquidation is a core part of P0's security model, and it is open to anyone. This guide covers the practical aspects of running a liquidation operation. ## Overview [#overview] Liquidators earn profit by repaying unhealthy borrowers' debts in exchange for seizing their collateral at a discount. There are two methods available: | Method | Min Capital | Max Premium | Complexity | | ---------------------------------------------------------- | --------------------------------------- | ----------- | ---------- | | [Classic Liquidation](/protocol-overview/liquidation) | Requires own P0 Account with collateral | 2.5% | Lower | | [Receivership Liquidation](/protocol-overview/liquidation) | Just SOL for gas | Up to 10% | Higher | We recommend **receivership liquidation** for most operators: higher profit potential, no capital requirements, and no slippage risk since unprofitable transactions can simply be aborted. ## Step 1: Discover Unhealthy Accounts [#step-1-discover-unhealthy-accounts] Most liquidators maintain a running inventory of accounts above a minimum dollar threshold, along with real-time prices for all relevant assets. With a large number of accounts on P0 (500k+), this requires: * **Account indexing.** Fetch and cache all margin accounts using `getProgramAccounts` or a similar bulk query. * **Price monitoring.** Subscribe to oracle price updates for all active assets. * **Health computation.** Continuously recalculate Maintenance health for tracked accounts as prices change. Use `lending_account_pulse_health` to get the risk engine's own assessment of an account's health. This uses the same oracles and logic as actual risk checks. ## Step 2: Evaluate Profitability [#step-2-evaluate-profitability] Before liquidating, consider: * **Oracle cranking costs.** Switchboard oracles require a crank instruction before use. Pyth, Scope and exchange-rate (mSOL, LST, PT) oracles need no crank, but the exchange-rate setups need their pricing account included alongside the bank's oracle — see [Oracles](/protocol-overview/oracles#oracle-configuration). * **Swap route quality.** Check that a profitable route exists to swap seized collateral to the debt token (or your preferred holding currency). * **Transaction costs.** Gas, priority fees, and the flat SOL fee for receivership liquidation. * **Competition.** Other liquidators may be targeting the same account. Consider using Jito bundles for faster inclusion. ## Step 3: Execute the Liquidation [#step-3-execute-the-liquidation] ### Receivership Approach [#receivership-approach] ``` ComputeBudgetProgram (set compute units + priority fee) Start Liquidation Withdraw Asset A from the account Swap A to B via Jupiter Repay Liability B End Liquidation ``` **Key rules:** * Only withdraw, repay, and compute budget instructions are allowed between start/end. * Account health must improve between start and end. * Profit is capped at the maximum fee (currently 10%). * Cannot open new positions or close existing ones. ### Classic Approach [#classic-approach] ``` Crank Switchboard oracles (if needed) lending_account_liquidate ``` After liquidation, rebalance your own account: withdraw the seized asset, swap to debt token, repay the assumed debt. ### Accounts with Kamino Positions [#accounts-with-kamino-positions] If the target account has wrapped Kamino positions, remember to refresh all reserves and obligations involved before attempting to liquidate. Kamino positions use a different withdraw instruction and consuming CU is significant. It is recommended to handle the Kamino withdraw in a separate transaction from the liquidation instruction in most cases. ## Tips for Operators [#tips-for-operators] * **Be conservative with amounts.** Start at 70-80% of the theoretically liquidatable amount. Prices may shift by the time your transaction lands. * **Maintain asset buffers.** Keeping small balances of common assets reduces the need to swap within every liquidation transaction. * **Build a large LUT pool.** Lookup Tables are essential for fitting liquidation + swap into Solana's transaction size limits. * **Optimize for liquidity.** Choose collateral/debt pairs where the swap route is deepest. SOL/LST and SOL/USDC are typically the most liquid. * **Monitor gas market.** During volatile periods, priority fees spike and competition intensifies. Budget accordingly. Liquidation is not inherently profitable. If no viable swap route exists between the seized collateral and the debt token, you can lose money. Receivership liquidation mitigates this: abort the transaction if it is not profitable. For programmatic liquidation implementation, see the [TypeScript SDK](/typescript-sdk/overview) and the [developer guides on GitHub](https://github.com/mrgnlabs/marginfi-v2/tree/main/guides). --- # Looping & Strategies How to use leverage loops and cross-venue strategies on Project 0 to maximize yield. **Looping** multiplies your exposure to an asset by repeatedly lending, borrowing, and re-depositing. **Strategies** builds on this by scanning 40+ markets across P0's integrated venues to surface the best yield opportunities and let you capture them in one click. ## How Looping Works [#how-looping-works] The basic concept: 1. Deposit Asset A as collateral. 2. Borrow Asset B against your collateral. 3. Swap B for A on a DEX. 4. Deposit the additional A, increasing your collateral. 5. Repeat. Each cycle increases your exposure to Asset A while building up debt in Asset B. The theoretical maximum leverage for a pair (using Initial weights) is: ``` Max Leverage = 1 / (1 - Asset Weight / Liability Weight) ``` For example, with an asset weight of 90% and liability weight of 100%: ``` Max Leverage = 1 / (1 - 0.9/1.0) = 10x ``` In practice, P0 builds loops atomically in a single transaction using [flashloans](/guides/flashloans). Instead of iterating manually: ``` Start flashloan Borrow $60 of B (the full leveraged amount) Swap B to A via DEX (one set of swap fees + slippage) Deposit A ($10 initial + $60 from swap = $70 total) End flashloan ``` Result: $70 in deposits, $60 in debt, net value $10, but with 7x exposure to Asset A's price movement. ## How to Loop on P0 [#how-to-loop-on-p0] P0 provides looping directly from both the deposit and borrow action boxes. ### Looping from Deposit [#looping-from-deposit] 1. Select the asset you want to deposit (e.g. SOL). 2. Enter your deposit amount. 3. Click the **Loop** toggle to expand looping options. 4. Select the asset to borrow (e.g. mSOL). 5. Use the **Leverage** slider to set your desired leverage. The app shows the maximum leverage available for the pair. 6. Review the NET APY, collateral, and health preview. 7. Click **Loop** to execute. The entire leveraged position is built atomically in a single transaction. ### Looping from Borrow [#looping-from-borrow] 1. Select the asset you want to borrow (e.g. BONK). 2. Click the **Loop** toggle to expand looping options. 3. Under **Available Collateral**, select an existing collateral asset from your account (e.g. SOL). 4. Use the **Leverage** slider to set your desired leverage. 5. Click **Loop** to execute. Toggle **"Create New Account"** when looping to open the position in a fresh isolated account. This is recommended when you want to keep Emode benefits separate or isolate risk from your other positions. ## Loop Mode [#loop-mode] When your portfolio contains a pair of correlated assets (e.g. LST/SOL) or one volatile asset and one stable asset, P0 shows **Loop Mode** in the portfolio view. Loop Mode displays additional information specific to looped positions: * **Leverage** -- Your current leverage multiplier. * **Net Balance** -- The net value of your position (collateral minus debt). * **Net Yield** -- The combined yield after accounting for borrow costs. * **Liquidation Price** -- The price at which your position would be liquidated, if applicable. Loop Mode also provides two shortcut buttons: * **Increase Leverage** -- Opens the borrow-side looping interface to add more leverage to your existing position. * **Unwind Loop** -- Opens the collateral repay interface to partially or fully close your looped position by repaying debt with collateral. ## Cross-Venue Looping [#cross-venue-looping] This is where P0's prime broker architecture creates something new. Because P0 unifies collateral across venues, you can loop across venue boundaries: **Deposit on Kamino at a high lending rate, borrow on P0 at a lower rate, and use leverage to loop the spread.** On isolated venues, the lending rate is always lower than the borrow rate for the same asset (that is how lending protocols work). But across venues, a high lending rate on Kamino and a low borrow rate on P0 can create a positive spread. P0 lets you use your Kamino deposit as collateral to borrow on P0, and then lever up on that spread. This kind of capital-efficient interest rate arbitrage across venues was not possible before P0. ## Strategies [#strategies] [Strategies](https://app.0.xyz/strategies) is P0's recommendation engine that continuously monitors both P0 and its integrated venues (Kamino, Drift) to identify the best yield opportunities. Unlike vault products where a centralized manager runs strategies and charges fees, Strategies surfaces the trades and lets **you** execute them directly. You control the position, see exactly where the yield comes from, and pay no management fees. ### What Strategies Surfaces [#what-strategies-surfaces] * **Rate trades** -- Deposit at a higher rate on one venue, borrow at a lower rate on another, loop the spread. * **Leveraged directional trades** -- Go long an asset with leverage while capturing its base yield (e.g., staking rewards). In favorable market conditions, the yield can exceed borrowing costs, meaning you get paid to maintain leveraged exposure. * **Carry trades** -- Assets like JLP appreciate in value independently of lending yield. Strategies factors in both the lending rate and native asset appreciation. ### How to Use Strategies [#how-to-use-strategies] 1. Visit [app.0.xyz/strategies](https://app.0.xyz/strategies). 2. Browse the available strategies. Each one shows the assets involved, the expected yield breakdown (base rate, emissions, asset appreciation), and the borrow cost. 3. Select a strategy. If you do not hold the required deposit token, the app opens an **Asset Swap** step first, letting you swap any token into the required asset without leaving the flow. 4. In the **Enter Strategy** step, set your leverage using the slider and review the NET APY, health, and collateral preview. 5. Toggle **"Create New Account"** if you want to isolate this strategy in its own account (recommended for preserving Emode benefits). 6. Click **Loop** to execute. The entire position is built atomically. ### Smart Account Management [#smart-account-management] If you create a leveraged loop using Asset A as collateral, opening a separate loop that borrows Asset A would conflict. Strategies handles this by creating a new isolated Account when needed. You can also toggle **"Create New Account"** manually when entering any strategy to ensure it runs in its own account. Each strategy runs in its own loop with clear visibility into leverage levels and liquidation prices. ### Debt Swap and Collateral Swap [#debt-swap-and-collateral-swap] You can modify positions mid-trade without unwinding and rebuilding. Swap your debt to a lower-rate asset, or swap your collateral to a higher-yielding one, all without closing your position. See [Managing Your Account](/guides/managing-your-account) for full details on collateral and debt swaps. ### Unified PnL [#unified-pnl] P0 tracks your returns across all integrated venues in a single view. The PnL system captures: * **Native yield and emissions** from venues like Kamino and Drift * **Open positions** on P0, including individual strategy legs * **Debt costs** when running leveraged strategies * **Native asset appreciation** separate from lending yield (e.g., JLP can appreciate vs USDC independently of the lending rate earned on top) This eliminates the need to manually aggregate returns across multiple platforms and reconcile different position sizes, venue-specific yields, and borrowing costs. ## Costs [#costs] The primary cost of looping is **swap fees and slippage**, incurred once when opening the position and once when closing it. * Swap fees are typically around 5 bps (0.05%). * Slippage depends on the asset pair and trade size. * There is no dedicated looping or strategy fee. When switching from one strategy (A/B) to another (C/D), you incur **two sets** of swap fees: one to close the original loop and one to open the new one. ## Risks [#risks] * **Liquidation** -- If the price of your collateral drops relative to your debt, your account can be liquidated. * **Interest rate changes** -- If borrowing rates increase or lending rates decrease, a previously profitable strategy may become unprofitable. * **Swap costs** -- Opening and closing loops incurs swap fees and slippage that eat into returns. P0 has expanded into interest rate derivatives, including fixed-rate products via Exponent (e.g., PT-HYLOSOL, PT-BULKSOL, PT-HYUSD). These PT tokens can be combined with native Kamino, Drift, and P0 positions for cross-venue leverage. As more derivatives venues are integrated, strategies like multi-venue basis trades and delta-neutral perp portfolios become possible. --- # Managing Your Account How to monitor your health score, use multiple accounts, and swap collateral or debt on Project 0. Once you have active positions on P0, managing your account effectively is the difference between capturing yield and getting liquidated. This guide covers how to read your health score, when to use multiple accounts, and how to adjust your positions mid-trade using collateral and debt swaps. ## Health Score [#health-score] Your health score represents how close your account is to liquidation. P0 displays it on the portfolio page at [app.0.xyz](https://app.0.xyz). * **Positive health** means your weighted collateral exceeds your weighted debt. You are safe. * **Health at zero** means you are at the boundary. Any price movement against you could trigger liquidation. * **Negative health** means your account can be liquidated. A third-party liquidator can seize some of your collateral to repay your debt. Health is calculated using **Maintenance weights**, which are more generous than the Initial weights used when you first borrow. This creates a buffer: you can borrow up to the Initial limit, and you are only liquidated once you breach the Maintenance limit. The gap between these two is your safety margin. ### What Moves Your Health [#what-moves-your-health] | Event | Effect on health | | -------------------------- | -------------------------------------------------- | | Collateral price increases | Health increases | | Collateral price decreases | Health decreases | | Debt price increases | Health decreases | | Interest accruing on debt | Health decreases (slowly) | | Depositing more collateral | Health increases | | Repaying debt | Health increases | | Adding a non-Emode borrow | Can drop health significantly (Emode benefit lost) | ### Staying Safe [#staying-safe] * Do not borrow to your maximum capacity. Leave a buffer for normal price fluctuations. * Monitor your health when markets are volatile. The portfolio page shows your health in real time. * If health is declining, deposit more collateral or repay some debt before it reaches zero. * Be aware of [Emode](/guides/emode) effects. Adding a borrow that does not have an Emode pairing with your collateral can cause a sudden health drop. For the technical details on how health is calculated (weights, confidence intervals, the math), see [Risk Engine](/protocol-overview/risk-engine). ## Using Multiple Accounts [#using-multiple-accounts] You can create as many Accounts as you want on P0. Each Account has its own positions (up to 16), its own health score, and its own Emode configuration. Accounts are completely independent of each other. When depositing or looping, you can toggle **"Create New Account"** in the action box to automatically create a new account and open your position there. This is the easiest way to isolate a new position without manually creating an account first. ### When to Use Separate Accounts [#when-to-use-separate-accounts] **Preserving Emode benefits.** If you want to lend SOL and borrow an LST (Emode active) while also lending USDC and borrowing USDT (Emode active), put them on separate accounts. If both are on the same account, one pairing can degrade the other. See the [Emode guide](/guides/emode) for worked examples. **Borrowing an asset you are already lending.** You cannot have both a deposit and a borrow on the same Bank within a single Account. If you are lending USDC on one Bank and want to borrow USDC from a different Bank (e.g., to loop or arbitrage), you need a second Account. **Isolating risk.** If you are running a high-leverage strategy alongside a conservative yield position, separating them means a liquidation on the aggressive account does not affect the conservative one. **Running multiple strategies.** The [Strategies](/guides/looping-and-strategies) feature on app.0.xyz handles this automatically by creating isolated accounts when strategies would conflict, but you can also manage accounts manually. ## Collateral Repay [#collateral-repay] Collateral repay lets you repay a loan using collateral you have already deposited, without needing external funds. P0 withdraws some of your collateral, swaps it into the debt asset, and repays the loan in a single transaction. ### When to Use Collateral Repay [#when-to-use-collateral-repay] * **No liquid funds.** If you do not have the borrowed asset in your wallet but want to reduce debt, repay directly from your collateral. * **Partial or full unwind.** Reduce both your collateral and debt simultaneously. This is the building block for unwinding looped positions. * **Deleveraging.** Decrease your leverage ratio without needing to source the debt asset externally. On app.0.xyz, collateral repay is available from the borrow action box. When selecting a token to repay, your existing collateral assets appear under "Collateral Repay." Select the collateral you want to use and confirm. The app handles the withdrawal, swap, and repayment atomically. Collateral repay incurs standard swap fees and slippage. The preview shows the expected output before you confirm. ## Collateral Swap [#collateral-swap] Collateral swap lets you exchange one collateral asset for another without withdrawing, swapping externally, and re-depositing. Your position stays open and your debt is unaffected. ### When to Use Collateral Swap [#when-to-use-collateral-swap] * **Chasing higher yield.** If an LST you are lending has lower yield than another, swap your collateral to the better-yielding LST in one transaction. * **Better leverage parameters.** Some assets have higher asset weights or more favorable Emode pairings. Swap into them to improve your health or increase borrowing power. * **Rebalancing.** Shift your collateral mix as market conditions change without unwinding your debt position. On app.0.xyz, collateral swap is available from the portfolio page. Select the collateral position you want to change, choose the target asset, and confirm. The app handles the underlying swap and re-deposit atomically. Collateral swap incurs standard swap fees and slippage (typically around 5 bps). The preview shows the expected output before you confirm. ## Debt Swap [#debt-swap] Debt swap lets you exchange your borrowed asset for a different one. Your collateral stays the same; only the debt side changes. ### When to Use Debt Swap [#when-to-use-debt-swap] * **Lowering your borrow cost.** If you are borrowing a stablecoin at 5% and another stablecoin is available at 3%, swap your debt to immediately reduce your interest expense. * **Adjusting exposure.** If you are short an asset through a borrow and want to change which asset you are short, swap the debt directly. * **Preserving or gaining Emode.** Swap from a non-Emode debt to one that has an Emode pairing with your collateral, instantly improving your health and borrowing power. On app.0.xyz, debt swap is available from the portfolio page. Select the debt position you want to change, choose the target borrow asset, and confirm. Debt swap incurs standard swap fees and slippage. The app repays your current debt and opens a new borrow in the target asset atomically. ## Putting It Together [#putting-it-together] A typical workflow for an active P0 user might look like: 1. **Account 1:** Deposit SOL, borrow jitoSOL (Emode active, high LTV). Run a SOL/LST loop via [Strategies](https://app.0.xyz/strategies). 2. **Account 2:** Deposit USDC on Kamino, borrow USDT on P0. Use debt swap to rotate between USDT and USDC depending on which has the lower borrow rate. 3. **Account 3:** Deposit various assets as general collateral, borrow SOL for spending. Use collateral swap to shift into whichever asset has the best yield without disrupting the borrow. Each account has its own health, its own Emode configuration, and can be managed independently. --- # MCP Server Connect any AI agent to Project 0 through the hosted MCP server at ai.0.xyz. Live rates, strategies, health, and unsigned transaction builders. Your wallet signs. Project 0 runs a hosted [MCP](https://modelcontextprotocol.io) server at `https://ai.0.xyz/mcp`. It gives any MCP-capable agent (Claude Code, Cursor, Codex, custom agents) 15 tools for reading the market and building transactions. Every transaction comes back **unsigned**: the agent's own wallet signs and sends it, and the server never holds keys. The same capabilities are available as plain HTTP under `https://ai.0.xyz/v1` for agents that don't speak MCP. The OpenAPI spec is at [ai.0.xyz/v1/openapi.json](https://ai.0.xyz/v1/openapi.json). ## Connect [#connect] ```bash claude mcp add --transport http project0 https://ai.0.xyz/mcp ``` Add to `~/.cursor/mcp.json` (or `.cursor/mcp.json` in a project): ```json { "mcpServers": { "project0": { "url": "https://ai.0.xyz/mcp" } } } ``` The server speaks stateless Streamable HTTP. Point your client at: ``` https://ai.0.xyz/mcp ``` No session handshake or API key is required. ```bash curl https://ai.0.xyz/v1/banks?symbol=USDC curl https://ai.0.xyz/v1/strategies curl -X POST https://ai.0.xyz/v1/tx/deposit \ -H "content-type: application/json" \ -d '{"authority":"","bank":"","amount":100}' ``` ## Tools [#tools] | Tool | What it does | | ------------------------------- | ----------------------------------------------------------------------------------------------- | | `p0_get_banks` | Lending banks across Project 0, Kamino, Drift and Jupiter Lend with live rates, size and limits | | `p0_get_strategies` | Ranked loop and rate-arbitrage strategies with max leverage and capacity | | `p0_get_wallet` | Token holdings and USD values for a wallet | | `p0_get_positions` | An account's positions, health, and how much it can borrow or withdraw | | `p0_get_health` | Quick health-factor check | | `p0_get_activity` | Recent account activity | | `p0_search_docs` / `p0_get_doc` | Search and read these docs | | `p0_create_account` | Build a create-account transaction | | `p0_deposit` / `p0_withdraw` | Build a deposit or withdraw | | `p0_borrow` / `p0_repay` | Build a borrow or repay | | `p0_loop` | Open or increase a leveraged position in one flash-loan transaction | | `p0_close_position` | Unwind a position by repaying debt with collateral in one flash-loan transaction | Read tools return JSON the agent can reason over directly. Amounts on the wire are UI token units (`1.5` = 1.5 tokens). ## How transactions work [#how-transactions-work] Every builder returns the same envelope: ```json { "account": "…", "transactions": [ { "base64": "…", "type": "CRANK", "label": "Update oracle" }, { "base64": "…", "type": "DEPOSIT", "label": "Deposit" } ], "action_tx_index": 1, "submit": "sequential", "blockhash": { "value": "…", "last_valid_block_height": 123456789 }, "simulation": { "success": true, "post_health": { "status": "healthy", "health_factor": 0.61 }, "warnings": [] } } ``` 1. **Sign** every transaction with the `authority` wallet. 2. **Submit** according to `submit`: * `sequential`: send in array order, waiting for each to confirm. * `bundle`: sign all with the returned blockhash and submit them together as one atomic [Jito bundle](https://docs.jito.wtf). Sending them one by one will fail or leave a position half-open. 3. Treat a landed signature with a non-null `err` as a failed transaction. Transactions are simulated before they are returned; if the simulation fails, nothing is returned and the error says why. `simulation.post_health` is the account's health after the action lands. The blockhash expires in about a minute; if it does, call the tool again. Transactions are unsigned by design. The server cannot move funds, and there is nothing to revoke. ## Example: run a strategy [#example-run-a-strategy] 1. `p0_get_strategies` and pick one, for example `supply.bank = 4ecR…`, `borrow.bank = 2s37…`, `max_leverage = 2.44`. 2. `p0_loop`: ```json { "authority": "", "create_new_account": true, "deposit_bank": "", "borrow_bank": "", "amount": 0.5, "leverage": 2 } ``` `amount` is principal already in the wallet, in deposit-token units. `leverage` must be at or below the strategy's `max_leverage`. `create_new_account: true` isolates the position in a fresh account, which is what the app does; pass `account` instead to add to an existing one. 3. Sign and send per `submit`. 4. To unwind, `p0_close_position` with `repay_all: true` (or an `amount` for a partial repay; at most 250,000 USD of debt per call). ## Bring your own aggregator keys [#bring-your-own-aggregator-keys] Loops and closes route swaps through Jupiter and Titan using the server's keys. To use your own, send them as HTTP headers on the connection. Never put keys in tool arguments. ```bash claude mcp add --transport http project0 https://ai.0.xyz/mcp \ --header "x-jupiter-api-key: " \ --header "x-titan-api-key: " ``` The same headers work on `POST /v1/tx/loop` and `POST /v1/tx/close-position`. ## Limits and errors [#limits-and-errors] Rate limits per client IP: 120 requests per minute on `/mcp`, 60 on `/v1`, 10 on `/v1/tx/*`, 4 on loop and close-position. A `429` carries `Retry-After`. Errors come back as `{ "error": "", "code": "", "details": { … } }`. Common codes: | Code | Meaning | | ----------------------------------------- | ---------------------------------------------------------------- | | `ACCOUNT_AMBIGUOUS` | The wallet owns several accounts; pass `account` | | `BORROW_LIMIT_EXCEEDED` | Over the max borrow; `details.max_borrow` is the cap | | `LEVERAGE_EXCEEDED` | Over the pair's max leverage; `details.max_leverage` is the cap | | `SWAP_ROUTE_UNAVAILABLE` / `TX_TOO_LARGE` | No swap route fits; reduce the amount or pick a more liquid pair | | `SIMULATION_FAILED` | The transaction would fail on-chain; the cause is in `details` | | `RATE_LIMITED` | Back off for `Retry-After` seconds | ## Related [#related] * [Agent Skill](/guides/agent-skill): a skill file that teaches coding agents Project 0 workflows. * [Looping and Strategies](/guides/looping-and-strategies): what a loop is and how strategies are ranked. * [TypeScript SDK](/typescript-sdk/overview): build transactions in your own code. --- # Pay Spend crypto against your DeFi portfolio without selling your positions. **Pay** lets you spend crypto against your DeFi portfolio. Instead of liquidating your portfolio to cover everyday expenses, you borrow against your existing, yielding DeFi positions and use that borrow to pay your bills. You keep using your existing debit or credit card, no new cards, no change in spending behavior, and your crypto stays working for you. ## How It Works [#how-it-works] Pay follows a simple three-step lifecycle: * **Onboarding** -- Connect your wallet, link your bank account or credit card, and configure your preferences. This is a one-time setup. * **Dashboard** -- Your home base. View your connected account, collateral, upcoming offramp schedule, pending purchases, and past offramp transactions. * **Offramp** -- You are notified once it is time to cover your spending. Select the transactions you want to pay off, borrow against your collateral, and offramp the funds to your bank account. ## Getting Started [#getting-started] ### Onboarding [#onboarding] The onboarding flow walks you through setting up Pay in a few steps: 1. **Connect your bank account** -- Pay uses Plaid to securely link your bank. Project 0 does not store your banking credentials. Plaid only provides read-only access to your transaction history so Pay can see your card spending. 2. **Select your account** -- Choose which bank account (e.g. checking, credit card) you want to track spending from. 3. **Set transaction preferences (optional)** -- Filter which transactions you want included in your offramps. You can select specific spending categories (e.g. food, transportation, shopping) and set a purchase price range (min/max amount). These preferences are used to pre-select transactions when you offramp. You can always override them later. 4. **Set an offramp schedule (optional)** -- Choose how often you want to be reminded to offramp: weekly (pick a day), biweekly (every two weeks on a chosen day), or monthly (first of the month, last of the month, or a specific date). 5. **Set notification preferences (optional)** -- Choose how you want to be notified when it is time to offramp: email (provide a recipient address) or Telegram (connect to the Project 0 Pay notification bot). 6. **Review and confirm** -- Review all your settings and complete the setup. Everything can be changed later from your dashboard. Steps 3-5 are optional and can be skipped during onboarding. You can always configure them later from the settings panel. ### Dashboard [#dashboard] After completing onboarding, you land on the Pay dashboard. From here you can view your connected bank account, available collateral, upcoming offramp date, pending purchases for the current period, and past offramp transactions. You can also add collateral directly from the dashboard. All of your onboarding preferences (bank account, transaction filters, schedule, and notifications) can be updated at any time from the settings panel. Settings auto-save when you close the panel. You can fully disconnect your bank account and delete all associated data from the settings panel. This revokes Plaid access and removes all stored data. This action cannot be undone; you will need to go through onboarding again to reconnect. ### Offramp Process [#offramp-process] When you are ready to offramp (either on schedule or manually), the flow has three steps: #### 1. Select Transactions [#1-select-transactions] Review your bank transactions from the current offramp period. Transactions matching your preferences are pre-selected, but you can manually select or deselect individual transactions, filter by category, or skip this step entirely and enter a custom borrow amount in the next step. #### 2. Borrow [#2-borrow] Borrow against your collateral to cover the selected transaction total (or your custom amount). You can borrow in USDC, USDT, or SOL. The borrow is executed as an on-chain transaction that you sign from your connected wallet. Your collateral remains deposited and continues earning yield. #### 3. Offramp [#3-offramp] The borrowed funds are converted from crypto to fiat via MoonPay and sent directly to your bank account. MoonPay handles the conversion and transfer. Their own KYC requirements and processing times apply. ## Important Things to Know [#important-things-to-know] ### Availability [#availability] Pay is currently only available in the United States. Expansion to further locations is planned. If you are outside the US and interested in using Pay, please reach out. The offramp step is powered by MoonPay, which has its own geographic and token-specific restrictions. ### Data and Privacy [#data-and-privacy] * Project 0 does not store your banking credentials. Plaid handles authentication directly with your bank. * Project 0 does not have access to any sensitive banking data beyond read-only transaction history. * You can revoke access and delete all stored data at any time from the settings panel. ## FAQ [#faq]
Do I need a new card? No. Pay works with your existing debit or credit card. There is no need to get a new card. You keep earning your credit card points and do not have to change your spending habits.
Can I delete my account? Yes. Go to the settings panel from your dashboard, open the Connect tab, and click Disconnect and Delete. This will revoke Plaid access and permanently delete all stored account and transaction data. You will need to go through onboarding again if you want to reconnect.
When will my funds arrive in my bank account? Offramp processing times depend on MoonPay. Typically, funds arrive within 1-3 business days after the offramp is initiated, but this can vary depending on your bank and region. You can track the status of your offramp from your transaction history on the dashboard.
--- # Staking Earn yield by minting LST or staking SOL directly to a Project 0 validator. **Staking** lets you earn native yield on your SOL. You can mint LST through the stake pool or delegate SOL directly to a Project 0 validator. Both options are available from a single interface at [/stake](https://app.0.xyz/stake), alongside a unified table that tracks all your staking positions. ## How It Works [#how-it-works] There are two ways to stake: * **LST:** Mint LST by depositing SOL into the stake pool. LST is a liquid staking token that accrues staking rewards automatically. You can hold it in your wallet, supply it as lending collateral on Project 0 to earn additional yield, or swap it back to SOL at any time. * **Native Validator Staking:** Delegate SOL directly to a Project 0 validator (Meridian or Horizon). Your SOL is locked in a stake account and earns inflation rewards. Unstaking requires waiting until the end of the current epoch. ## Staking [#staking] Use the action box on the stake page to deposit into the LST Pool or delegate to a specific validator. You can stake using any token in your wallet — if you pick something other than SOL, it is automatically swapped to SOL before staking. The preview shows your estimated output, annual yield, and any swap details. ## Unstaking [#unstaking] * **Instant unstake (LST):** Swap LST back to SOL. Immediate liquidity. * **Delayed unstake (LST):** Withdraw through the stake pool. SOL becomes available at the end of the current epoch. No swap fees. * **Native unstake:** Deactivate an active stake account (fully or partially). SOL becomes available at the end of the current epoch. Once a stake account is inactive, you can withdraw the SOL back to your wallet. ## Stake Accounts [#stake-accounts] The table below the action box shows all your staking positions in one place: LST held in your wallet, LST deposited as lending collateral, and native stake accounts delegated to validators. Each row shows the relevant APY, SOL equivalent, and available actions like unstaking, depositing, or withdrawing. Native stake accounts go through a lifecycle — activating, active, deactivating, inactive — and the table surfaces inactive accounts first so you can withdraw them quickly. ## APY Chart [#apy-chart] The bar chart at the top of the page shows historical LST APY over time. You can switch between 1-week and 1-month intervals. The current live APY is shown in the chart header. ## FAQ [#faq]
What is LST? LST is a liquid staking token backed by SOL staked across Project 0 validators. It accrues staking rewards automatically, the SOL value of 1 LST increases over time. You can hold it, trade it, or use it as collateral on Project 0.
Can I stake with tokens other than SOL? Yes. Select any token from your wallet in the action box. It will be swapped to SOL before staking. The preview shows the swap route, price impact, and fees.
How long does unstaking take? Instant unstake is immediate. Delayed unstake and native deactivation require waiting until the end of the current epoch. The epoch countdown is shown in the stake accounts table so you know exactly when your SOL will be available.
Can I earn extra yield on my LST? Yes. After minting LST, you can deposit it as lending collateral on Project 0. This earns you both staking yield and lending yield simultaneously. Use the Deposit button on the LST wallet row in the table.
What happens if I partially deactivate a stake account? The stake account is split into two: one stays active with the remaining balance, and one is deactivated with the amount you chose. The deactivated portion becomes withdrawable at the end of the epoch.
--- # Architecture Core data model of Project 0, including Groups, Banks, Accounts, Balances, and Oracles. Project 0 is built on the **mrgnLendv2** program, a Solana smart contract that manages all lending, borrowing, and risk operations on-chain. The protocol's data model consists of five core entities. ## Architecture at a Glance [#architecture-at-a-glance] ``` ┌────────────┐ ┌───────────┐ ┌──────────┐ │ │ │ │ │ │ │ Group │1─────n│ Bank │1─────n│ Oracle │ │ │ │ │ │ │ └────────────┘ └───────────┘ └──────────┘ 1 1 │ │ │ │ n 1 ┌───────────┐ ┌────────────┐ │ Margin │ │ │ │ Account │1───≤16│ Balance │ │ │ │ │ └───────────┘ └────────────┘ ``` ## Group [#group] A collection of Banks. Each Group has a single administrator (typically a secure governance multisig) who has broad authority over it, and several delegate admins (limit, emode, etc.) who can perform lower-risk modifications. All the assets you see on [app.0.xyz](https://app.0.xyz) belong to a single Group overseen by the foundation. ## Bank [#bank] Each asset available to borrow and lend on P0 has a Bank. This account controls all the settings for a particular asset: interest rate curves, risk parameters (asset weights, liability weights), deposit and borrow caps, oracle configuration, and fee structure. Many Banks might exist for the same underlying token. Every asset listed on [app.0.xyz](https://app.0.xyz) is a Bank. ## Account [#account] Users can create as many Accounts as they want. Accounts are per-Group, and each Account can have up to 16 positions across any Banks in that Group. The Account contains various user-specific settings and cached values, along with a `LendingAccount` where Balances are stored. ## Balance [#balance] A Balance is an asset or liability position in a single Bank. Users cannot have both an asset and a liability in the same Bank, and can have at most one Balance per Bank. Each Account's `LendingAccount` holds a collection of up to 16 Balances. Balances can be blank/unused, and are always sorted in byte order by the corresponding Bank's public key. ## Asset Weight [#asset-weight] Each asset available to lend has two asset weight rates: Initial and Maintenance. The Maintenance rate is always higher. When executing a borrow, collateral is valued at `price * initial weight`. When a liquidator attempts a liquidation, collateral is valued at `price * maintenance weight`. The range between these is sometimes called the "health buffer." For example, if a user has collateral worth $10 and init/maint rates are 50% and 60% respectively, the user can borrow $10 \* 0.5 = $5. For liquidation purposes, their collateral is worth $10 \* 0.6 = $6. The LTV displayed on [app.0.xyz](https://app.0.xyz) is the Initial weight. The health shown on the portfolio page uses the Maintenance weight. ## Liability Weight [#liability-weight] Each borrowable asset also has a liability weight, split into Initial and Maintenance. The Maintenance rate is always lower. When executing a borrow, liabilities are valued at `price * initial weight`. When a liquidator attempts a liquidation, liabilities are valued at `price * maintenance weight`. On the borrowing page, the displayed "LTV" is `1 / Initial Liability Weight`, i.e. the LTV you would get if lending an asset with an Asset Weight of 1. ## Oracle [#oracle] Each Bank has an oracle used to determine the price of the asset it transacts in. The Group admin is responsible for picking and maintaining the Oracle. Typically, Switchboard is the oracle provider, but Pyth is also supported, and some Banks have a fixed price. An Oracle may use multiple accounts. For example, a Kamino Bank uses a price source and the Kamino reserve. For details on confidence intervals, EMA vs spot pricing, and staleness rules, see [Oracles](/protocol-overview/oracles). For detailed developer and integrator documentation (instruction construction, account packing, Kamino integration internals), see the [guides on GitHub](https://github.com/mrgnlabs/marginfi-v2/tree/main/guides). --- # Audits Security audit reports for the Project 0 (mrgnLendv2) protocol. Security is a core priority for Project 0. The mrgnLendv2 program has undergone professional security audits and maintains an active bug bounty program. ## Audit Reports [#audit-reports] The mrgnLendv2 program has been audited by multiple professional security firms, including **Ottersec**, **sec3**, **Hashlock**, **Ackee**, and **Accretion**. Audit reports are publicly available: View all audit reports on GitHub, including general reviews, feature audits, and subsequent reviews. ## Fuzz Testing [#fuzz-testing] The protocol maintains an extensive fuzz testing suite that continuously tests for edge cases and vulnerabilities: View the fuzz testing suite on GitHub. ## Responsible Disclosure [#responsible-disclosure] If you have a vulnerability report, send it to [security@mrgn.group](mailto:security@mrgn.group). Do not disclose vulnerabilities publicly before they have been addressed. For scope, severity classification, and disclosure process, see the [security policy](https://github.com/0dotxyz/marginfi-v2/blob/main/SECURITY.md). ## Open Source [#open-source] The mrgnLendv2 program is fully open source. Anyone can review the code, run the test suite, and verify the deployed program matches the published source: The complete source code for the mrgnLendv2 program, including tests, documentation, and deployment scripts. --- # Bank States Notable operational states that Banks can be placed into by the Group administrator. The Group administrator can configure Banks into several operational states that restrict or modify their behavior. ## Isolated [#isolated] Banks in the **Isolated** risk tier have an asset weight of zero: their deposits are worthless as collateral. However, Isolated assets can still: * Earn interest from borrowers. * Be borrowed (their liability weights are still active). **Restriction:** An Account can only borrow **one Isolated asset** at a time. Isolated status is typically assigned to riskier or less liquid assets where the protocol doesn't want to accept the price risk of using them as collateral. A planned (not yet implemented) extension to Emode will allow Isolated assets to receive a non-zero weight in specific pairings. See [Emode](/protocol-overview/emode) for details. ## Reduce-Only [#reduce-only] Banks in **Reduce-Only** mode can only process **withdrawals and repayments**. New deposits and borrows are disabled. * Liquidations continue to function normally. * All risk parameters and interest rates remain active. * Existing positions continue to accrue interest. Reduce-Only is typically used when a Bank is being **sunset**. Users should withdraw from Reduce-Only Banks as soon as possible, as the administrator may take more drastic measures (such as reducing asset weights to zero) to deleverage remaining positions. ## Paused [#paused] Banks in **Paused** mode cannot process any user operations: no deposits, withdrawals, borrows, or repayments. This state is usually reserved for **defunct Banks** that are empty. ## Globally Paused [#globally-paused] The **Global Fee State Admin** (which controls program-level fees) has the ability to pause the **entire protocol**: all Groups and Banks simultaneously. * Maximum pause duration: **30 minutes** per event. * Maximum frequency: **twice per day**. * During a global pause, all financial operations across all Groups and Banks are suspended. The global pause is reserved for extreme emergencies. As of November 2025, it has **never been used**. It exists as a last-resort circuit breaker to protect users during unprecedented events. --- # Cross-Venue Integrations How Project 0 integrates with Kamino, Drift, and other Solana DeFi venues as cross-venue collateral. P0 accepts collateral from third-party venues like Kamino and Drift, provided the Group administrator has created a Bank for that asset. This cross-venue integration is what makes P0 a prime broker rather than just another lending protocol. ## Supported Venues [#supported-venues] * **Kamino** -- Live. Lending positions across Kamino markets. * **Drift** -- Live. Positions from Drift protocol. * **Jupiter Lend** -- Coming soon. Each third-party venue asset has its own Bank. For example, the Kamino Main Market USDC reserve and the Kamino Maple Market USDC reserve are separate Banks with independent risk parameters. ## How Yield Works [#how-yield-works] Depositors into third-party venue Banks earn yield from **the originating venue**, not from P0 borrowers. There is no borrowing from third-party venue assets on P0; these positions exist purely as collateral. For example, when depositing into the Kamino Main Market USDC Bank on P0, lenders earn what any other depositor would earn on Kamino. The yield comes from borrowers on Kamino, not from P0. ## Kamino Integration [#kamino-integration] ### Terms [#terms] * **klend** -- The main Kamino lending program. * **Market** -- A "Group" in Kamino parlance. * **Reserve** -- A "Bank" in Kamino parlance. * **Obligation** -- A "user account" in Kamino parlance. * **Farms** -- A rewards emission mechanism used by Kamino to incentivize reserve deposits/borrows. Built as a separate program from klend. * **Refreshing Reserves** -- Kamino requires reserves to cache price data from their oracles. This must happen for any transaction that changes funds. A refresh is valid for one slot or one transaction that changes funds where that reserve is involved, whichever comes first. * **Refreshing Obligations** -- Kamino requires the user's account to refresh before any transaction that changes funds, after any reserves the account uses are refreshed. This syncs the balances and health of the user's account. Like refreshing reserves, this is valid for one slot or one transaction that changes funds, whichever comes first. In practice, every Kamino-related transaction looks like: refresh reserves, refresh obligation, everything else. * **Collateral tokens** -- When you deposit into Kamino, you receive "collateral tokens" representing your position. These appreciate in value relative to the liquidity token as interest accumulates, so the exchange rate of collateral/liquidity token goes up over time. ### Big Picture Overview [#big-picture-overview] Each P0-wrapped Kamino Bank corresponds to exactly one Kamino Reserve. It has exactly one Obligation, with a single position, that holds all the deposits in that Bank for all users. Users always deposit the raw asset into this Bank. For example, if the Reserve transacts in USDC, users deposit USDC. To transfer an existing Kamino position into a P0-wrapped one, withdraw the funds from Kamino and deposit them again into the P0-wrapped Bank. Under the hood, P0 Banks are like any other user/depositor of the Kamino platform, enjoying no special access or privileges. The asset share value of wrapped Banks is always 1 (they never earn interest in the P0 program). All interest is earned due to appreciation of the collateral token on Kamino's end. The wrapped Obligation earns all Kamino points, rewards, and farm emissions that any normal depositor would earn. The Group administrator is responsible for distributing these to depositors. ### Token Amount Types [#token-amount-types] | Instruction | Token Amount Type | Notes | | ----------- | ----------------------- | ------------------------------------------------------ | | Deposit | Liquidity token amount | Raw underlying token (e.g., USDC, SOL) | | Withdraw | Collateral token amount | Must convert from collateral to liquidity token amount | | Liquidate | Collateral token amount | Must convert from collateral to liquidity token amount | Since collateral tokens appreciate in value relative to the liquidity token as interest accumulates, liquidators and withdrawers must manually convert from collateral token amounts to liquidity token amounts using the current exchange rate. ### Notes for Liquidators [#notes-for-liquidators] If a user has a wrapped Kamino position, remember to refresh any reserves and obligations involved before attempting to liquidate. Liquidators may claim a wrapped Kamino position, which must be withdrawn using the separate withdraw instruction applicable only to wrapped Banks. The withdraw instruction requires refreshing the reserve/obligation and consumes a significant amount of CU: it is recommended to do this in a separate transaction from the liquidation instruction in most cases. For developer-level integration details, admin setup instructions, and ongoing maintenance considerations, see the [guides on GitHub](https://github.com/mrgnlabs/marginfi-v2/tree/main/guides). --- # Emissions How emissions campaigns distribute token rewards to lenders and borrowers on Project 0. Some Banks, including Banks from other venues, might offer a token incentive to depositors, or even to borrowers. A set of incentives over time is called a **Campaign**. ## How It Works [#how-it-works] For example, a Campaign might distribute 7 tokens of A to lenders per week (one per day). Each lender's share is determined on a pro-rata basis in real time. If there are two lenders, each depositing the same amount, each gets 3.5 tokens per week. ### Worked Example [#worked-example] Two users, where User 1 has $1 in deposits from the start. User 2 deposits $1 on Thursday, and $5 more on Saturday: * **Monday through Wednesday:** User 1 gets all emissions (1 token/day). * **Thursday and Friday:** User 1 and User 2 each get 0.5 tokens/day. * **Saturday and beyond:** User 1 gets `$1 / ($1 + $6) = 0.143` tokens/day. User 2 gets `$6 / ($1 + $6) = 0.857` tokens/day. Emissions are delivered by airdrop to the Account's authority, typically on Wednesday, in no particular order. Over the full week, User 1 would get `3 + 0.5 + 0.5 + 0.143 * 2 = 4.286` tokens and User 2 would get `0.5 + 0.5 + 0.857 * 2 = 2.714` tokens. ## Paired Emissions [#paired-emissions] In some Campaigns, users must **both** lend a particular asset **and** borrow a particular asset. A common Campaign is to lend some LST and borrow SOL. An Account only earns an airdrop if they perform both tasks. For example, if the Campaign is to lend LST\_A and borrow SOL, and a user is lending $70 in LST\_A and borrowing $50 in SOL, they earn rewards on $50: `min(lending_lst_a, borrowing_sol)`. ## Changing Emissions Destination [#changing-emissions-destination] Want emissions delivered somewhere specific? Set up an `emissions_destination_account` with `marginfi_account_update_emissions_destination_account`. This is highly recommended for PDA account authorities. ## FAQ [#faq]
I did not get any tokens, what happened? There is a minimum threshold to receive a drop in a given week, typically just above what it costs to open an ATA (usually less than a dollar). If what you earned in a given week is below that threshold, the amount is accumulated into the following week, where it will be dropped if you meet the threshold. At the end of a Campaign, one final drop of any accumulated dust amounts is typically performed within reason. Contact support if you think you should have gotten a drop and did not.
Do emissions always come in the same token as the Bank? No, they could be in any asset, or even several.
Is there a set time emissions are dropped on Wednesday? No. The time varies slightly each week to avoid gamesmanship. It is usually during ET business hours.
Can I earn multiple incentives at the same time? Yes. If there is a paired incentive to lend LST\_A or LST\_B and borrow SOL, you can count the same SOL borrow towards both Campaigns. For example, if you lend $50 LST\_A and $70 LST\_B, then borrow $60 SOL, you would earn $50 towards the LST\_A Campaign and $60 towards the LST\_B Campaign.
--- # Emode How Efficiency Mode (Emode) enhances borrowing power for correlated asset pairs on Project 0. **Efficiency Mode** (Emode) allows users to enjoy higher borrowing power when working with correlated asset pairs. For example, when using an LST as collateral to borrow SOL, Emode provides a much higher LTV than borrowing an unrelated asset like USDC. ## How Emode Works [#how-emode-works] Each Bank on P0 can be configured with Emode entries that specify preferential asset weights for specific borrowing pairings. When a user's lending and borrowing positions match an Emode configuration, the enhanced weights are automatically applied. ### Key Differences vs Aave [#key-differences-vs-aave] * **Opt-in by default.** P0 does not require users to enable Emode. Users automatically get the most preferential Emode setting that applies to their account. * **Opt-out by borrowing something else.** Aave requires you to disable Emode before borrowing a non-preferential asset. On P0, you can enter or exit Emode simply by changing which assets you are borrowing. All assets remain available to borrow, but borrowing a non-preferential asset can significantly reduce account health (and repaying one can significantly increase it). ### Worst-Case Across Liabilities [#worst-case-across-liabilities] Users who borrow multiple assets at the same time get the **least favorable** treatment across all borrowed assets, regardless of the amount of each asset borrowed. For example, if borrowing an LST and USDC against SOL, the user would normally get an Emode benefit for LST/SOL, but since they are also borrowing USDC, they get only standard rates on the SOL they are lending. A user currently lending SOL and borrowing LST who wants to also borrow USDT must have enough account health to support both the LST and USDT borrow without any Emode advantage. ## Emode Tags and Configuration [#emode-tags-and-configuration] ### Emode Tag [#emode-tag] An arbitrary number (1 to 65,535) assigned to each Bank by the admin. Multiple Banks can share the same tag. By convention, tags use the asset name in l337sp34k: SOL is 501, LST is 157, etc. If the name does not fit into range, use your imagination. ### Emode Config [#emode-config] A Bank's collection of Emode Entries. ### Emode Entry [#emode-entry] A tag to treat as a preferential asset, along with the initial and maintenance asset weights to use when lending that asset. ### The Key Nuance: Configure the BORROWING Bank [#the-key-nuance-configure-the-borrowing-bank] One of the biggest subtleties: **Emode is always configured on the BORROWING bank, not the LENDING bank.** For example, if you want users to borrow more SOL against some LST, you configure the SOL Bank. If you want users to borrow more WIF against BONK, you configure the WIF Bank. Emode can never reduce a Bank's weights. The Bank's configured weight is always the minimum; attempts to configure an Emode entry below the Bank's weights will simply do nothing. ### A Tag With No Entries Is Normal [#a-tag-with-no-entries-is-normal] A Bank can carry an Emode tag while having **no entries of its own**. This is the standard shape for collateral-only assets (PT tokens, for example): the Bank grants no preferential weights to anyone borrowing *it*, but its tag is what other Banks' entries match against. In the agent API this appears as `emode: { tag, entries: [] }` — an empty entries list does not mean the Bank sits outside Emode. To find the effective weights, look up that tag in the entries of the Bank you intend to borrow. ### Emode on Venue Banks [#emode-on-venue-banks] Emode is not limited to P0-native Banks. Venue-integration Banks (Kamino, Drift, JupLend) carry tags and entries like any other Bank — the Kamino SOL Banks, for instance, are configured with entries for the LST collateral tags — and collateral can receive Emode treatment against a venue-bank borrow the same way it does against a P0-native one. ## Configuration Matrix Example [#configuration-matrix-example] Consider this set of pairings (LENDING is what users deposit, BORROWING is what they borrow against it): ``` LENDING | NORMAL RATE | BORROWING | EMODE RATE SOL .8 LST_A .9 SOL .8 LST_B .9 SOL .8 LST_C .85 SOL .8 LST_D .85 LST_A .6 SOL .8 LST_B .6 SOL .8 LST_C .6 SOL .8 LST_D .6 SOL .8 USDC .9 USDT .95 USDT .9 USDC .95 ``` From the above, we have four unique pairings and need at least four tags: SOL, LST\_AB, LST\_CD, STABLE. We also have four unique rates: LEND\_SOL\_BORROW\_LST\_AB (.9), LEND\_SOL\_BORROW\_LST\_CD (.85), LEND\_LST\_BORROW\_SOL (.8), LEND\_STABLE\_BORROW\_STABLE (.95). The resulting Emode configuration: ``` bank: SOL_BANK tag: SOL entries: [LST_AB -> LEND_LST_BORROW_SOL, LST_CD -> LEND_LST_BORROW_SOL] bank: LST_A_BANK, tag: LST_AB, entries: [SOL -> LEND_SOL_BORROW_LST_AB] bank: LST_B_BANK, tag: LST_AB, entries: [SOL -> LEND_SOL_BORROW_LST_AB] bank: LST_C_BANK, tag: LST_CD, entries: [SOL -> LEND_SOL_BORROW_LST_CD] bank: LST_D_BANK, tag: LST_CD, entries: [SOL -> LEND_SOL_BORROW_LST_CD] bank: USDC_BANK, tag: STABLE, entries: [STABLE -> LEND_STABLE_BORROW_STABLE] bank: USDT_BANK, tag: STABLE, entries: [STABLE -> LEND_STABLE_BORROW_STABLE] ``` Note: A Bank can have an Emode entry for its own tag, but you can never borrow from a Bank you are already lending into. ## Worked Examples [#worked-examples]
User lending SOL and borrowing LST_A 1. Look at SOL\_BANK to see what tag it has: SOL 2. Look at LST\_A\_BANK to read the rate for tag SOL: LEND\_SOL\_BORROW\_LST\_AB (.9) **Result: .9**
User lending SOL and borrowing LST_A and LST_C 1. Look at SOL\_BANK to see what tag it has: SOL 2. Look at LST\_A\_BANK to read the rate for tag SOL: LEND\_SOL\_BORROW\_LST\_AB (.9) 3. Look at LST\_C\_BANK to read the rate for tag SOL: LEND\_SOL\_BORROW\_LST\_CD (.85) 4. Take the smallest of the two rates: min(.9, .85) = .85 **Result: .85**
User lending SOL and borrowing LST_A and USDT 1. Look at SOL\_BANK to see what tag it has: SOL 2. Look at LST\_A\_BANK to read the rate for tag SOL: LEND\_SOL\_BORROW\_LST\_AB (.9) 3. Look at USDT\_BANK to read the rate for tag SOL: it does not have one. 4. Since one of the borrowing assets does not have an entry, fall back to SOL Bank's NORMAL RATE (.8). **Result: .8**
User lending SOL and LST_A and borrowing LST_C 1. Look at SOL\_BANK to see what tag it has: SOL 2. Look at LST\_A\_BANK to see what tag it has: LST\_AB 3. Look at LST\_C\_BANK to read the rate for tag SOL: LEND\_SOL\_BORROW\_LST\_CD (.85) 4. Look at LST\_C\_BANK to read the rate for tag LST\_AB: it does not have one. 5. SOL is treated at LEND\_SOL\_BORROW\_LST\_CD (.85) while LST\_A is treated at the NORMAL RATE (.6) **Result: .85 for SOL and .6 for LST\_A**
## Using Multiple Accounts [#using-multiple-accounts] Because Emode benefits depend on the combination of all positions in an Account, users with diverse strategies may benefit from separate Accounts: * **Account 1:** Lend SOL, borrow LST (Emode active, high LTV) * **Account 2:** Lend USDC, borrow USDT (Emode active, high LTV) * **Account 3:** Lend SOL, borrow USDC (no Emode, standard weights) This prevents a non-preferential borrow in one strategy from degrading the Emode benefits in another. ## Emode for Isolated Assets (Not Yet Implemented) [#emode-for-isolated-assets-not-yet-implemented] Isolated assets normally have an asset weight of zero. Once implemented, Emode will be able to grant an Isolated asset a non-zero weight in specific preferential pairings. This would be useful for highly correlated but otherwise risky assets. For example, a prediction market's YES/NO tokens could receive Emode benefits against each other for position hedging while remaining worthless as general collateral. Do not confuse this with a pattern that is **already live**: a Collateral-tier Bank configured with base asset weights of zero (PT tokens use this). Such a Bank is worthless as general collateral, yet Emode entries on borrowing Banks can and do grant it non-zero weights today — that is ordinary Emode, not this feature. The difference is the Bank's risk tier: `Isolated` Banks are excluded from Emode entirely until the `APPLIES_TO_ISOLATED` entry flag ships, while zero-weight `Collateral` Banks participate normally. ## Emode Cloning [#emode-cloning] When listing a new asset that should inherit another asset's Emode settings (e.g. a new LST that should copy existing LST/SOL settings), the Emode admin or Group admin can use the `emode_clone` instruction to copy Emode settings from one Bank to another. The `configBankEmode` instruction is large: only two can fit in one transaction. When updating more than two Banks, use a Jito bundle of several transactions. For developer-level Emode configuration details and TypeScript examples, see the [guides on GitHub](https://github.com/mrgnlabs/marginfi-v2/tree/main/guides). --- # Fees Overview of all fees on Project 0, both at the program level and the front end. ## Program Fees [#program-fees] These fees are charged at the smart contract level and apply regardless of how you interact with the protocol. ### Deposits and Withdrawals [#deposits-and-withdrawals] There are **no fees** to deposit into or withdraw from P0. ### Borrowing [#borrowing] Borrowers pay interest to lenders. For most Banks, a small portion of borrow interest paid is a fee that goes to the administrator. A small portion also goes to the Bank's insurance fund. The protocol supports borrow origination fees, but as of November 2025, these have always been zero. ### Repayment [#repayment] There are **no fees** to repay a debt on P0. ### Flashloans [#flashloans] There are **no fees** to execute a flashloan on P0. ### Liquidation Fees [#liquidation-fees] If an Account is partially liquidated, it can lose up to 10% of the cash equity value liquidated. For example, if an account has $100 in assets and $90 in debt (net worth $10), a liquidator seizes $10 of collateral and repays $9 in debt. The account now has $90 in assets and $81 in debt: net worth declined by 10%. Accounts with a net worth of less than $10 can be fully liquidated: all remaining collateral on the Account is seized and all debt repaid, closing out the Account. ## Front-End Fees [#front-end-fees] These fees are specific to the [app.0.xyz](https://app.0.xyz) front end. You can avoid them by using a different front end, calling the program directly through scripts, CPI, etc. ### Slippage [#slippage] When swapping asset A for asset B, the value of B you get is slightly smaller than the value of A you give up. You can configure your maximum tolerated slippage for all swap operations in settings. Different asset pairs have different slippage expectations. ### Swap Fees [#swap-fees] When swapping asset A to asset B (for features such as collateral repay), a small fee is charged. You can see the fee in the preview breakdown before approving the transaction. Typically this is around 5 bps (0.05%). ### Looping Fees [#looping-fees] A "loop" is when a user lends A, borrows B, swaps B to A, and repeats. There is no fee to loop, but since a swap is involved, swap fees and slippage are incurred. The theoretical max leverage of a pair A/B, given Collateral Weight of A (CW) and Liability Weight of B (LW), is: ``` L = 1 / (1 - CW/LW) ``` **Example:** Asset Weight of A is 90%, Liability Weight of B is 100%. ``` User has $10 in Asset A * Deposits $10 in A (worth $9 in collateral), borrows $9 of B * Swaps B to A, incurring slippage + fees, getting ~$9 in A * Deposits $9 in A and repeats ``` Max leverage in this example is `1 / (1 - 0.9/1.0)` = **10x**. Under the hood, a loop is built in one step using a flashloan. For example, for 7x leverage: ``` Start flashloan Borrow ~$60 of B Swap B to A, netting ~$60 after fees Deposit A (initial $10 + $60 = $70) End flashloan ``` The user ends up with $70 deposited and $60 in debt. The portfolio net value is still $10. One swap + slippage fee is incurred when opening the position, and another set when closing it. ### Strategy Fees [#strategy-fees] Like loops, there is no fee to open a strategy except the swap fees and slippage incurred to create the looped position. Swapping from one loop pair (A/B) to another (C/D) may incur **two sets** of swap fees: one to close out the A/B loop, and another to open the C/D loop. ## Fees from Integrated Venues [#fees-from-integrated-venues] Currently, none of the venues integrated with P0 charge notable additional fees. --- # Interest Rates How interest rate curves, compounding, and APR vs APY work on Project 0. Banks are configured to charge progressively higher interest as the utilization rate increases. The utilization rate (UR) for a Bank is simply borrows/deposits. Interest typically climbs slowly until the "optimal utilization rate," which for most assets is around 80-90%. Beyond that level, interest grows rapidly. At 100% UR, lenders cannot withdraw because all funds have been borrowed out. High interest rates at 90%+ UR drive borrowers to repay their debts. ## Interest Rate Curves [#interest-rate-curves] Each Bank has a **piecewise linear interest rate curve** defined by: * A rate at 0% utilization (`zero_util_rate`) * Up to 5 intermediate points at specific utilization levels * A rate at 100% utilization (`hundred_util_rate`) The majority of Banks use only 2 points: zero at 0 UR, one intermediate point, and a value at 100 UR. The base rate `r` is linearly interpolated between the two nearest points (x0, y0) and (x1, y1): ``` r = (UR - x0) / (x1 - x0) * (y1 - y0) + y0 ``` **Example 1:** `zero_util_rate` = 10%, a point at (50%, 100%), current UR = 25%: ``` r = (25 - 0) / (50 - 0) * (100 - 10) + 10 = 55% ``` **Example 2:** A point at (50%, 100%) and (80%, 150%), current UR = 60%: ``` r = (60 - 50) / (80 - 50) * (150 - 100) + 100 = 116.67% ``` ## Derived Rates [#derived-rates] From the base rate `r`, two other rates follow (these are also reported in the bank cache): ``` Lending Rate = r * UR Borrow Rate = r * IR Fees + Fixed Fees ``` Fees include insurance and protocol fees. Borrowers always pay more than lenders receive, because there are fees, and more importantly because there are more lenders than borrowers (UR \< 100%), so the same interest is distributed across a larger deposit base. The difference between what borrowers pay and lenders receive is called the **spread**. When lending interest on one venue exceeds the borrow rate for the same asset on another venue, this creates an **interest arbitrage** (or "arb" for short). You can read a Bank's last spot interest rate from `bank.cache`. This updates any time a Bank has a balance change. You can also send a permissionless `accrue_interest` instruction to force it to update. ## APR vs APY [#apr-vs-apy] The rates described above are the **Annual Percentage Rate** (APR): simple interest, as if charged once per year. The **Annual Percentage Yield** (APY) uses compounding: interest on previously accrued interest. On P0, interest is computed just before any balance change, so the rate at which different Banks compound varies. Popular Banks like SOL may compound every few seconds during active periods. Less active Banks might compound just a few times per week, but these typically have very few borrows (and thus a low APR to compound). Since interest compounds based on usage, the more popular the protocol becomes, the more often interest compounds for everyone. In practice, Banks with meaningful borrows typically yield within 1% of continuously compounded APY. The exceptions are rare cases where an asset has a high APR but not much activity. **How different rates are displayed:** * Rates on [app.0.xyz](https://app.0.xyz) assume approximately hourly compounding for native Banks. * Some integrated venues generate on-chain APY compounded every 400ms (one Solana slot) using a Taylor series approximation, so they may have no APR to compare against. * When comparing rates across venues, be aware that APR and APY conversions vary by source. ## Interest and Position Management [#interest-and-position-management] Because interest accrues just before any balance-changing transaction: * The actual amount withdrawn may be slightly more than previewed (you earned interest since the preview). * The actual repayment amount may be slightly more than previewed (your debt grew since the preview). * To fully close a position, use the "withdraw all" or "repay all" flags, which account for interest accrued up to the moment of execution. --- # Lending & Borrowing How deposits, borrows, interest accumulation, LTV, and the share system work on Project 0. ## Lending [#lending] When you deposit assets into a Bank, you become a lender. Your deposit serves as liquidity that borrowers can draw from, and you earn interest over time. There are no fees to deposit or withdraw. Interest accrues passively through the share system (see below) with no interaction required beyond the initial deposit. ## Borrowing [#borrowing] To borrow, you must have sufficient collateral deposited in your Account. The protocol's [risk engine](/protocol-overview/risk-engine) evaluates your Account health to determine how much you can borrow. Borrowers pay interest to lenders, and a small portion of interest also goes to the Bank's insurance fund and administrator fees. ## Interest Accumulation [#interest-accumulation] Interest accumulates when users borrow an asset. It updates whenever any user performs a transaction that affects a Bank's balances. Borrowers are assessed interest, while lenders earn it. A portion of interest paid by borrowers also goes to fees. Interest accumulates on your position entirely within the program: no interaction is necessary beyond depositing, borrowing, withdrawing, and repaying. **Example:** ``` Sally deposits $20 in Token A Sally borrows $10 in Token B Sally's $10 debt accrues interest and grows to $11. The $1 difference is not debited until Sally repays: she must repay $11 to close this debt. Sally's net balance and borrowing power declines from (20 - 10) = $10 to (20 - 11) = $9. If she cannot repay, she may not be able to withdraw all of her A. Bob deposits $10 in Token B. Thanks to Sally's interest, Bob's Token B is now worth $11. Bob doesn't realize his $1 gain until he withdraws. Bob's net balance and borrowing power increased from $10 to $11. Bob can withdraw $11 at any time. ``` Interest accrues for **all** users of a Bank simultaneously. If anyone transacts with a Bank, the interest for every lender and borrower in that Bank compounds at that moment. ## How Interest Compounds [#how-interest-compounds] Under the hood, Balances are tracked internally such that your deposit represents a proportional ownership of the pool. As borrowers pay interest, the conversion rate increases, so your position becomes redeemable for more tokens over time. This is why interest compounds automatically with no interaction. The more frequently a Bank is used (any deposit, withdraw, borrow, or repay), the more often interest compounds for all users. Because interest accumulates just before any transaction, the actual amount withdrawn or repaid can be slightly different from what's previewed. To withdraw or repay your full balance, use the special "withdraw all" or "repay all" flag, which closes the balance in full **after** interest accrual. ## LTV and Weights [#ltv-and-weights] The Loan-to-Value ratio (LTV) determines how much you can borrow relative to your collateral. Some venues display "Weight" as the LTV, but these are related concepts, not identical. When you deposit $100 of Token A, an **Asset Weight** is applied. This weight discounts your asset: a $100 deposit with a weight of 90% is only worth $90 for borrowing purposes. Likewise, a **Liability Weight** applies to assets you borrow: the $50 you borrowed with a liability weight of 110% is actually worth $55 in risk terms. The effective LTV for any lending/borrowing pair is: ``` LTV = Asset Weight / Liability Weight ``` Each weight has two tiers: | Tier | Used For | Relative Value | | --------------- | ------------------ | ------------------------------------------------- | | **Initial** | Borrowing checks | More conservative (lower asset, higher liability) | | **Maintenance** | Liquidation checks | More permissive (higher asset, lower liability) | The gap between Initial and Maintenance weights creates a **health buffer** (also called the "liquidation buffer") that protects borrowers from immediate liquidation after borrowing. The LTV displayed on [app.0.xyz](https://app.0.xyz) uses the **Initial** asset weight. The health shown on the portfolio page uses the **Maintenance** weight. ## Comparing LTVs Across Venues [#comparing-ltvs-across-venues] Most venues (including P0) publish only the Asset Weight on the user-facing front end. The Liability Weight, while publicly available, is often less visible. This means a platform showing a higher Asset Weight may actually offer a lower effective LTV if its Liability Weight is also higher. **Example:** ``` Venue K: Asset Weight 90%, Liability Weight 120% Venue P: Asset Weight 85%, Liability Weight 105% Venue K LTV: 0.90 / 1.20 = 0.75 (75%) Venue P LTV: 0.85 / 1.05 = 0.81 (81%) ``` Despite showing a lower Asset Weight, Venue P offers better effective LTV because its Liability Weight is more favorable. ## Worked Example: Health and Liquidation [#worked-example-health-and-liquidation] ``` Token A: Asset Weight Initial = 90%, Maintenance = 95% Token B: Liability Weight Initial = 110%, Maintenance = 105% Susie deposits $100 of Token A. Susie's deposit is worth: 100 * 0.90 = $90 for borrowing purposes. Susie borrows as much B as possible: Max borrow of B = 90 / 1.10 = $81.82 For Maintenance purposes: Health = 100 * 0.95 - 81.82 * 1.05 = $9.09 remaining liquidation buffer. ``` Now Token A drops in value by 5%: ``` Susie's deposit in A is now worth $95. Health = 95 * 0.95 - 81.82 * 1.05 = $4.34 remaining liquidation buffer. ``` Token A drops 10% from original: ``` Susie's deposit in A is now worth $90. Health = 90 * 0.95 - 81.82 * 1.05 = -$0.41. Susie can be liquidated! ``` At any point, Susie could deposit more collateral or repay some debt to avoid liquidation and the premium it carries. --- # Liquidation How liquidation works on Project 0, including classic and receivership approaches, and the bankruptcy flow. Liquidation keeps Project 0 solvent. When a borrower's account becomes unhealthy (Maintenance health below zero), third-party **liquidators** can step in to repay some of the borrower's debt in exchange for seizing collateral at a discount. Liquidation is **open and permissionless**. Anyone can liquidate unhealthy accounts. ## When Can an Account Be Liquidated? [#when-can-an-account-be-liquidated] An account is eligible for liquidation when its **Maintenance health** drops below zero: ``` Maintenance Health = Sum(Asset Value * Maintenance Asset Weight) - Sum(Liability Value * Maintenance Liability Weight) < 0 ``` Asset weights discount collateral (below 1.0) and liability weights inflate debt (at or above 1.0), so this check is more permissive than the Initial check used for new borrows. At any point, the borrower can withdraw or repay themselves to avoid paying the liquidation premium. ## Classic Liquidation [#classic-liquidation] The original method, using the `lending_account_liquidate` instruction. The liquidator must have their own P0 Account. The liquidator seizes a portion of the borrower's collateral (Asset A) and in exchange assumes some of the borrower's debt (Liability B): * If a liquidator reclaims $A in assets, they only have to assume `(1 - 0.025) * A` in liabilities. The liquidator keeps this 2.5% premium. * The borrower only receives a debt repayment of `(1 - 0.025 - 0.025) * A`. The additional 2.5% goes to the insurance fund. Classic liquidation cannot raise an account's health above zero. The liquidator can only bring health up to zero at most, preventing over-liquidation.
How do I know when accounts are eligible to be liquidated? Most liquidators keep a running inventory of all accounts above some dollar threshold, along with current prices for all relevant assets. This is a considerable endeavor with 500k+ accounts.
How do I know how much asset to claim? Start conservatively, around 70-80% of the amount health is actually negative, since prices may change by the time the transaction lands. Since liquidation cannot raise health above zero, seizing too much will cause the instruction to fail.
I landed a liquidation, now what? Most liquidators rebalance their inventory as soon as possible: withdraw the asset, swap to the debt token, and repay the debt. Some attempt to do this within the same transaction, though this is not always possible with larger accounts due to transaction size and compute limits. Note that taking on a liability carries risk: there is no guarantee a profitable swap route exists to exit that liability.
Can I use a flashloan in liquidation? Yes. The liquidator can use a flashloan to avoid keeping collateral on hand. The transaction would look something like: ``` Start flashloan Liquidate user Withdraw asset A from liquidator account Swap A to debt B on e.g. Jupiter Repay debt B End flashloan ``` In practice, this is considerably difficult to pack into a single transaction.
Do I have to crank all Switchboard oracles before liquidating? Yes, no stale oracles are permitted for either the liquidatee or the liquidator. Typically, you send a crank transaction just before the liquidation. Some operators prefer to use a Jito bundle, but sending a crank instruction with a small delay before the liquidation typically suffices.
## Receivership Liquidation [#receivership-liquidation] A newer, more flexible approach using `start_liquidation` and `end_liquidation`. The liquidator temporarily takes control of the unhealthy account, enabling them to withdraw and repay as if they are the user. At the end, the protocol validates that account health actually improved. The process generally looks like this: ``` ComputeBudgetProgram (optional) Start Liquidation Withdraw A Swap A to B via Jupiter or similar Repay B End Liquidation ``` Within the transaction, the liquidator can withdraw, swap, and repay, keeping whatever is left over. This enables liquidators to operate without holding any funds or creating a P0 account at all. ### Why Use Receivership Over Classic? [#why-use-receivership-over-classic] * **No collateral needed.** No account and no starting capital required (just SOL for gas). * **Higher profit potential.** Up to 10% (configurable by the global fee admin via `FeeState.liquidation_max_fee`), vs 2.5% in classic. * **No slippage risk.** If no profitable swap route exists between A and B, abort the entire transaction. Profit is defined as: ``` Seized <= Repaid * (1 + max_fee) ``` Where Seized is the equity value withdrawn (in $) and Repaid is the equity value repaid (in $). Equity value is the price of the token without any weights applied, but inclusive of oracle confidence interval adjustments. ### Receivership Rules [#receivership-rules] * `start_liquidation` must be the first instruction in the transaction (after ComputeBudgetProgram and Kamino refreshes). * `end_liquidation` must be the last instruction. Nothing can come after it. * The only other P0 instructions allowed between start and end are Withdraw and Repay. * Account health must improve between start and end. All health checks are ignored during withdraw and repay; only the final health matters. This means you can withdraw BEFORE repaying. * The liquidator cannot open new positions. * The liquidator can freely fully withdraw or fully repay positions, but cannot close them. A position withdrawn or repaid in full therefore still requires passing the oracle for that position at end, unlike a `withdraw_all` or `repay_all` in normal circumstances. * Isolated positions (asset weight of zero) and positions with a price of 0 cannot be withdrawn during liquidation. * The liquidator pays a nominal fee in lamports to use this service, automatically billed to the global fee wallet at end. See `FeeState.liquidation_flat_sol_fee` for the current fee. The first liquidator to use receivership on any given account inits a **liquidation record** (1:1 for that account) and pays a nominal amount of rent to create it. This record stores historical information about the last few liquidation events on that account. Currently, the rent for this record cannot be recovered. ### Tips [#tips] * Maintaining a small balance of various assets reduces the need to swap within every liquidation transaction. * A large pool of LUTs is typically required to fit a liquidation + swap into transaction size limits, especially for large accounts. * Profit is ultimately defined by slippage. Pick asset A and liability B such that swapping A to B (and A to your preferred currency) incurs minimal slippage. For example, SOL to LST and SOL to USDC are typically highly liquid. * If liquidating an account with Kamino positions, include the refreshes. Kamino positions use a different withdraw instruction. ## Bankruptcy [#bankruptcy] Bankruptcy is rare. It can only occur when (1) liquidators have not been running properly (e.g. due to congestion), (2) an asset is too illiquid for liquidators to safely clear, or (3) an asset's price changes drastically before liquidators can respond. A user is technically bankrupt after all their remaining assets are liquidated, at which point they have 0 assets and a non-zero amount of liabilities. ### Discharging a Bankruptcy [#discharging-a-bankruptcy] First, liquidators consume all the remaining assets the user has. If the user has A dollars in assets and B dollars in liabilities (in equity value, excluding weights), B > A. After liquidation completes, assets = 0 and remaining liabilities = B - A + X, where X is the liquidation premium and insurance. Next, the Group administrator runs `handle_bankruptcy` on the user. For Banks where `PERMISSIONLESS_BAD_DEBT_SETTLEMENT_FLAG` is enabled, anyone can do this. The instruction performs the following: 1. If the Bank's **insurance fund** has sufficient reserves, it covers the bad debt. 2. If the insurance fund is insufficient, the remainder is covered by taking liquidity out of the Bank, reducing the asset share value. This **socializes the loss** to all remaining depositors. 3. If both the insurance fund and remaining liquidity are exhausted (**super-bankruptcy**), the Bank is killed. The asset share value is set to zero, wiping out all holdings for all depositors. This state is irrecoverable and the Bank is permanently disabled. Bankruptcy has never been executed in the main pool as of November 2025. Almost all bankruptcies occur in the Arena, where riskier assets experience extreme volatility.
What happens if bankruptcy is not executed? If `handle_bankruptcy` is never called on a bankrupt user, the remaining depositors can never withdraw the full balance in that Bank. The last few depositors who try to withdraw will find there are not enough funds, proportional to the liabilities held by bankrupt users.
When a user goes bankrupt with multiple liabilities, which banks absorb the bad debt? The liquidator gets to pick which liabilities stay on the user's books. This means the liquidator effectively chooses which Banks absorb the bad debt.
--- # Oracles How Project 0 prices assets with Pyth, Switchboard, Scope and exchange-rate oracles, and how confidence intervals, EMA and staleness apply. Each Bank is configured with an **oracle setup** by the Group administrator. The setup names the price source stored in `bank.config.oracle_keys[0]` and, for some setups, one or two extra on-chain accounts the program reads alongside it to derive the final price. ## Oracle setups [#oracle-setups] | Setup | Price source (`oracle_keys[0]`) | Extra accounts | How the price is formed | | ----------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Pyth** — `PythPushOracle`, and the `KaminoPythPush` / `DriftPythPull` / `SolendPythPull` / `JuplendPythPull` venue variants | Pyth price update account | Venue reserve / market in `oracle_keys[1]` for the venue variants | Pyth price and confidence, kept fresh by Pyth's own infrastructure; no caller action. | | **Switchboard** — `SwitchboardPull` and its venue variants | Switchboard pull feed | Venue account in `oracle_keys[1]` for the venue variants | Feed value; the caller must **crank** the feed just before use. | | **Scope** — `Scope` | A Scope `OraclePrices` account; `bank.config.scope_entry_index` selects one of its 512 entries | None | The entry's `value / 10^exp`. Scope carries **no confidence interval**; risk is expressed through the bank's weights. Program 0.1.11. | | **mSOL** — `PythMSOL`, `KaminoMSOL`, `JuplendMSOL` | Pyth SOL/USD | Marinade `State` (`oracle_keys[1]`; `oracle_keys[2]` on the venue variants, whose `[1]` is the venue account) | SOL/USD × the mSOL/SOL rate derived from Marinade's virtual staked balance over the mSOL supply. Program 0.1.11. | | **LST** — `PythLST`, `KaminoLST`, `JuplendLST` | Pyth SOL/USD | SPL or Sanctum stake pool (same slots as mSOL) | SOL/USD × `total_lamports / pool_token_supply`. The pool must have been updated in the current or previous epoch. Program 0.1.11. | | **Exponent PT** — `PTPyth`, `PTFixed` | Pyth base feed for `PTPyth`; none for `PTFixed` (the underlying is treated as $1) | Exponent vault (`oracle_keys[1]` for `PTPyth`, `oracle_keys[0]` for `PTFixed`) | Base × a linear rate that accretes from the bank's configured start price to par at the vault's maturity, capped by what the PT can actually redeem for. Program 0.1.11. | | **Fixed** — `Fixed` and its venue variants | None | Venue account for the venue variants | An administrator-set constant (rare, special cases). | For the mSOL, LST and PT setups the exchange rate is part of the **oracle price itself** — an mSOL bank's price *is* mSOL/USD. The receipt-token rate of a venue (a Kamino cToken, a JupLend fToken) is a separate step applied on top by the risk engine, exactly as it is for every other venue bank. ## Oracle Confidence Interval Adjustment [#oracle-confidence-interval-adjustment] Some oracles report a price with a **confidence interval** (P +/- c), representing the range within which the true price likely falls. P0 uses confidence intervals conservatively: * **Assets** (collateral) are priced at the **lower bound**: `P - P * c` * **Liabilities** (debt) are priced at the **upper bound**: `P + P * c` **Example:** If the oracle reports Token A at $20 with a 5% confidence interval: * As collateral, Token A is valued at $20 - $1 = **$19** * As debt, Token A is valued at $20 + $1 = **$21** The confidence interval is capped at a maximum of **5%**. If the oracle reports a wider confidence interval, the protocol may clamp it to 5% or abort the transaction entirely. This is relatively unique to P0 in the borrow-lending space and provides an additional layer of protection against oracle instability. Scope prices and fixed prices have no confidence interval, so their collateral and liability values coincide. ## EMA vs Spot Price [#ema-vs-spot-price] Some oracles report both a **spot price** (the current instantaneous price) and an **Exponential Moving Average** (EMA) price (a price-over-time instead of an instant price "right now"). When both are available, P0 uses them strategically: | Operation | Price Used | | ----------- | --------------------------------------------- | | Borrowing | EMA price (more stable, harder to manipulate) | | Liquidation | Spot price (more responsive to rapid changes) | If an EMA price is not available, all operations use the spot price. ## Staleness [#staleness] Oracle data must be **fresh**: every bank has an `oracle_max_age` (seconds), and a transaction that depends on an older price fails. * **Pyth** oracles are rarely stale since Pyth maintains its own update cadence. * **Switchboard** oracles require the caller to send a crank instruction just before the transaction to refresh the data. * **Scope** entries are refreshed by the feed operator; there is nothing for a caller to crank. A bank's `oracle_max_age` is applied to the entry's own timestamp with **no default** — an age of `0` is always stale. * **mSOL, LST and PT** setups inherit the freshness of their Pyth base feed. A stake pool additionally counts as stale if it has not been updated in the current or previous epoch. When composing transactions that use Switchboard oracles, either bundle the crank instruction in the same transaction (if compute and account limits allow) or send a separate crank transaction with a brief delay before the main transaction. Many operators use Jito bundles for this. ## Oracle Configuration [#oracle-configuration] Each Bank's oracle configuration is readable from `bank.config.oracle_setup`, `bank.config.oracle_keys` and, for Scope banks, `bank.config.scope_entry_index`. When constructing a transaction that requires a risk engine check, every active bank contributes its accounts to the remaining accounts in this order — the bank, then its oracle accounts exactly as the program expects them: | Setup | Accounts per bank | | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------- | | Pyth, Switchboard, Scope, Fixed (plain) | bank, `oracle_keys[0]` | | Any venue variant (Kamino, Drift, Solend, JupLend) | bank, `oracle_keys[0]`, `oracle_keys[1]` (venue account) | | `PythMSOL`, `PythLST`, `PTPyth` | bank, `oracle_keys[0]`, `oracle_keys[1]` (Marinade State / stake pool / vault) | | `KaminoMSOL`, `JuplendMSOL`, `KaminoLST`, `JuplendLST` | bank, `oracle_keys[0]`, `oracle_keys[1]` (venue account), `oracle_keys[2]` (Marinade State / stake pool) | | `PTFixed` | bank, `oracle_keys[0]` (the vault) | | Native stake (`StakedWithPythPush`) | bank, `oracle_keys[0]`, `oracle_keys[1]`, `oracle_keys[2]`, and the pool on-ramp | The TypeScript SDK builds this list for you (`computeHealthAccountMetas`); see [Program Upgrade 0.1.11](/typescript-sdk/program-upgrade-0-1-11) if you assemble it yourself. For Kamino specifically, `refresh_reserve` must also execute within the same slot. --- # Program Addresses On-chain Solana mainnet addresses for all Project 0 programs. All Project 0 programs are deployed on Solana mainnet. You can verify these addresses on any Solana block explorer. ## Core Programs [#core-programs] | Program | Address | | -------------------------- | --------------------------------------------- | | **marginfi v2 (mrgnLend)** | `MFv2hWf31Z9kbCa1snEPYctwafyhdvnV7FZnsebVacA` | | **Staging** | `stag8sTKds2h4KzjUw3zKTsxbqvT4XKHdaR9X9E6Rct` | Staging program is updated approximately two weeks before each mainnet release, allowing integrators to test against upcoming changes. When a pre-release is published on GitHub, the staging program is typically updated the same day. Only the last release candidate (e.g., rc3 if rc1, rc2, and rc3 exist) goes live on mainnet. ## Useful Links [#useful-links] | Resource | URL | | --------------------------- | -------------------------------------------------------------------------- | | **App** | [app.0.xyz](https://app.0.xyz) | | **GitHub (Protocol)** | [github.com/mrgnlabs/marginfi-v2](https://github.com/mrgnlabs/marginfi-v2) | | **GitHub (TypeScript SDK)** | [github.com/0dotxyz/p0-ts-sdk](https://github.com/0dotxyz/p0-ts-sdk) | | **Discord** | [discord.com/invite/c2kNj7y9w8](https://discord.com/invite/c2kNj7y9w8) | --- # Risk Engine How Project 0's on-chain risk engine evaluates account health and protects protocol solvency. Project 0 uses a **deterministic, fully on-chain risk engine** that evaluates every transaction in real-time. The risk engine ensures that borrowing and lending activities remain within acceptable parameters, protecting the protocol's solvency and lenders' deposits. ## How Health Is Calculated [#how-health-is-calculated] Account health is calculated as the weighted sum of assets minus the weighted sum of liabilities: ``` Health = Sum(Asset Value * Asset Weight) - Sum(Liability Value * Liability Weight) ``` Where asset values are priced using the **lower bound** of the oracle confidence interval (conservative for collateral), and liability values are priced using the **upper bound** (conservative for debt). ### Two Health Checks [#two-health-checks] The risk engine performs two kinds of health checks using different weight tiers: | Check | Weights Used | Purpose | | --------------- | --------------------------------------------- | --------------------------------------- | | **Initial** | Lower asset weights, higher liability weights | Determines if new borrows are allowed | | **Maintenance** | Higher asset weights, lower liability weights | Determines if account can be liquidated | An Account **cannot borrow** when its health is below zero using Initial weights. An Account is **subject to liquidation** when its health is below zero using Maintenance weights. Liquidations cannot make an account healthy again: a liquidator can only bring a user's health up to zero at most. ## Worked Example [#worked-example] Consider a user with 2 Token A (worth $10 each) as collateral and 5.05 USDC in debt: ``` Token A: price $10, confidence $0.212 Initial Asset Weight: 50%, Maintenance Asset Weight: 10% USDC: price $1, confidence $0.0212 Initial Liability Weight: 100%, Maintenance Liability Weight: 100% ``` **Initial health (can I borrow more?):** ``` Health = (2 * $9.788 * 0.5) - (5.05 * $1.0212 * 1.0) = $9.788 - $5.157 = $4.631 ``` This account is healthy with $4.63 in remaining borrowing power. **Maintenance health (can I be liquidated?):** ``` Health = (2 * $9.788 * 0.1) - (5.05 * $1.0212 * 1.0) = $1.958 - $5.157 = -$3.199 ``` The account is unhealthy under Maintenance weights. It can be liquidated, even though the raw asset value ($20) exceeds the raw debt ($5.05). This is by design: weights create a safety buffer that protects lenders. A partial liquidation can restore health: ``` Liquidator fee = 2.5% Insurance fee = 2.5% Liquidator seizes 0.2 Token A (worth $2) from the borrower's account. Liquidator must pay (in USDC): value of A minus liquidator fee (low bias): 0.2 * (1 - 0.025) * 9.788 = $1.90866 Borrower receives (in USDC debt repayment): value of A minus (liquidator fee + insurance) (low bias): 0.2 * (1 - 0.025 - 0.025) * 9.788 = $1.85972 Health after: ((2 - 0.2) * 9.788 * 0.1) - ((5.05 - 1.85972) * 1.0212 * 1.0) = -1.496 ``` The user's health improved, but they are still eligible to be partially liquidated. ## Just-In-Time Evaluation [#just-in-time-evaluation] Unlike some protocols that require "refresh" instructions before risk-sensitive operations, P0's risk engine evaluates **just in time**. No refresh cranks are needed (except for those required by integrated venues like Kamino). When a risk check is required, the caller passes the necessary Bank and Oracle accounts in the transaction's remaining accounts: ``` [bank0, oracle0_1], [bank1, oracle1_1, oracle1_2], [bank2, oracle2_1], ``` Banks and their oracles are passed in sorted bitwise order by Bank public key, matching the order they appear in the user's Balances. Some Banks have multiple oracle accounts (e.g., Kamino Banks require a price oracle and the Kamino reserve). Each oracle must not be stale. For Pyth oracles, the caller typically has no obligations since Pyth oracles are kept up-to-date by their administrator. For Switchboard oracles, the caller must send a crank instruction just before the transaction consuming that price. Some callers prefer to use bundles for this, but it typically suffices to send a crank instruction and briefly wait. When transaction size permits, callers might even prepend the Switchboard crank to the transaction consuming the oracle data, although this runs into account and CU constraints for larger transactions. For Kamino Banks, `refresh_reserve` must also execute within the same slot. When **borrowing**, the caller can pass stale oracles for collateral positions that are not needed to demonstrate sufficient health. For example, if a user has $1 in Token A and $1,000 in Token B, and wants to borrow $100 in Token C, a stale oracle for Token A is acceptable because Token B alone provides sufficient collateral. ## When Risk Checks Run [#when-risk-checks-run] | Operation | Risk Check Required | | ----------- | ------------------------------------------------ | | Deposit | No (deposits always improve health) | | Repay | No (repayments always improve health) | | Borrow | Yes (must pass Initial health check) | | Withdraw | Yes (must pass Initial health check) | | Liquidation | Yes (account must fail Maintenance health check) | ## Checking Your Health [#checking-your-health] You can query the risk engine's real-time assessment of your account by calling `lending_account_pulse_health`. This instruction uses the same oracles and weights that would be used in any actual risk check, giving you an accurate snapshot of your account's health for borrowing, liquidation, and bankruptcy purposes. --- # Accounts Create, derive, load and read marginfi accounts, and move or close them when you are done. A marginfi account holds a wallet's positions (up to 16 balance slots) in one group. This page covers the full account lifecycle through `Project0Client` and `MarginfiAccountWrapper`. It assumes you have a `client` from [Getting Started](/typescript-sdk/getting-started). ## Creating accounts [#creating-accounts] `client.createMarginfiAccountTx(authority, accountIndex = 0, thirdPartyId?)` returns a transaction that initializes a new account PDA: ```typescript const createAccountTx = await client.createMarginfiAccountTx( wallet.publicKey, 0 // account index (default: 0) ); const signature = await wallet.sendTransaction(createAccountTx, connection); await connection.confirmTransaction(signature); ``` | Parameter | Type | Description | | -------------- | ------------------------ | ---------------------------------------------------------------------------------------------------- | | `authority` | `PublicKey` | Wallet that will own the account | | `accountIndex` | `number` (u16) | Lets one wallet own several accounts; each index is a distinct PDA. Default `0` | | `thirdPartyId` | `number` (u16, optional) | Namespaces accounts for an integrator, so your index `0` never collides with another app's index `0` | ```typescript // A second account for the same wallet const secondAccountTx = await client.createMarginfiAccountTx(wallet.publicKey, 1); // An account namespaced under a third-party id const thirdPartyAccountTx = await client.createMarginfiAccountTx(wallet.publicKey, 0, 12345); ``` ### Composing creation with other instructions [#composing-creation-with-other-instructions] If you want to create an account and act on it in the same transaction (or before it is confirmed), use the projection method. It returns a wrapper for the *future* account plus the creation instruction: ```typescript const { wrappedAccount, ix } = await client.createMarginfiAccountWithProjection( wallet.publicKey, 0 ); // wrappedAccount can build deposit/borrow transactions now; prepend `ix` // to the transaction you send so the account exists when they execute. ``` ## Deriving and discovering accounts [#deriving-and-discovering-accounts] Account addresses are PDAs of `(programId, group, authority, accountIndex, thirdPartyId)`, so you can derive them offline: ```typescript import { deriveMarginfiAccount } from "@0dotxyz/p0-ts-sdk"; const [accountAddress] = deriveMarginfiAccount( client.program.programId, client.group.address, wallet.publicKey, 0 // account index // , 12345 // third-party id (defaults to 0) ); ``` When you don't know the index, scan the chain instead. All three helpers use filtered `getProgramAccounts` calls and return addresses only, so they stay cheap even across hundreds of thousands of accounts: ```typescript // Every account owned by a wallet in this group const addresses = await client.getAccountAddresses(wallet.publicKey); // Every account holding a position in a bank (any authority) const holders = await client.getAccountAddressesHoldingBank(usdcBank.address, { concurrency: 4, // optional: batch the 16 slot scans to stay under RPC limits }); // Every (account, bank) row holding a mint, with authority and UI amounts const rows = await client.getAuthorityBalancesForMint(USDC_MINT, { assetTag: AssetTag.DEFAULT, // optional: restrict to one bank flavour concurrency: 4, }); for (const row of rows) { // row: { authority, accountAddress, bank, assets, liabilities } (BigNumber, UI units) console.log(row.authority.toBase58(), row.assets.toFixed(2), row.liabilities.toFixed(2)); } ``` A mint can map to several banks (for example a standard USDC bank and a Kamino USDC bank), so `getAuthorityBalancesForMint` resolves all of them via `getBanksByMint` and emits one row per (account, bank). The balances call decodes full accounts and is heavier than the address-only scans; see [`examples/18-mint-holders.ts`](https://github.com/0dotxyz/p0-ts-sdk/tree/main/examples/18-mint-holders.ts). ## Loading an account [#loading-an-account] The convenience path is `client.fetchAccount(address, skipHealthCache?)`, which fetches, parses and wraps in one call: ```typescript import { PublicKey } from "@solana/web3.js"; const accountAddress = new PublicKey("YOUR_MARGINFI_ACCOUNT_ADDRESS"); // Fetch, simulate the health cache, and wrap const wrappedAccount = await client.fetchAccount(accountAddress); // Skip the health-cache simulation (one fewer RPC round-trip) when you // only need the raw balances, not health-based numbers const rawWrapped = await client.fetchAccount(accountAddress, true); ``` `fetchAccount` runs `simulateHealthCache` by default so `computeMaxBorrowForBank`, `computeFreeCollateralFromCache` and friends are fresh. See [Health & Max Amounts](/typescript-sdk/health-and-max-amounts) for what the cache contains and how to refresh it later. If you want control over each step (for example to wrap an account you already fetched elsewhere): ```typescript import { MarginfiAccount, MarginfiAccountWrapper } from "@0dotxyz/p0-ts-sdk"; const account = await MarginfiAccount.fetch(accountAddress, client.program); const wrappedAccount = new MarginfiAccountWrapper(account, client); ``` The wrapper is a thin layer over `MarginfiAccount` that auto-injects the client's banks, oracle prices, lookup tables and integration metadata into every method, so you only pass what is specific to the action. ## Reading positions [#reading-positions] `wrappedAccount.balances` is the fixed 16-slot array; `wrappedAccount.activeBalances` filters it to slots that are in use. Each `Balance` stores **shares**, not token amounts — convert with the bank: ```typescript import { MarginRequirementType } from "@0dotxyz/p0-ts-sdk"; for (const balance of wrappedAccount.activeBalances) { const bank = client.getBank(balance.bankPk); const oraclePrice = client.oraclePriceByBank.get(balance.bankPk.toBase58()); if (!bank || !oraclePrice) continue; // Integration banks (Kamino, Drift, Jupiter Lend) hold shares of a share; // the multiplier converts them to underlying tokens. Undefined for standard banks. const multiplier = client.assetShareValueMultiplierByBank.get(balance.bankPk.toBase58()); // Token amounts in UI units const { assets, liabilities } = balance.computeQuantityUi(bank, multiplier); // USD value. MarginRequirementType.Equity = unweighted; Initial/Maintenance // apply the bank's asset/liability weights. const usd = balance.computeUsdValue(bank, oraclePrice, MarginRequirementType.Equity, multiplier); console.log( `${bank.tokenSymbol}: ${assets.toFixed(4)} deposited ($${usd.assets.toFixed(2)}), ` + `${liabilities.toFixed(4)} borrowed ($${usd.liabilities.toFixed(2)})` ); } ``` Other useful accessors: | Accessor | Description | | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | `wrappedAccount.getBalance(bankPk)` | The `Balance` for one bank (an empty balance if none) | | `balance.assetShares` / `balance.liabilityShares` | Raw share amounts (`BigNumber`) | | `bank.getAssetQuantity(shares)` / `bank.getLiabilityQuantity(shares)` | Shares to native token amount, before decimals | | `balance.computeQuantity(bank)` | Native token amounts (no decimals applied) | | `balance.getUsdValueWithPriceBias(bank, oraclePrice, ...)` | USD value using the oracle's low/high confidence bound, as the program does for health | | `wrappedAccount.authority`, `.group`, `.address` | Account identity | | `wrappedAccount.healthCache` | The on-chain/simulated health cache; see [Health & Max Amounts](/typescript-sdk/health-and-max-amounts) | Account flags are exposed as booleans: * `wrappedAccount.isDisabled` — the account has been disabled (for example after it was transferred to a new account) and rejects further actions. * `wrappedAccount.isFlashLoanEnabled` — the account is currently *inside* a flash loan (the flag is set between `begin` and `end`; see [Flash Loans](/typescript-sdk/flash-loans)). * `wrappedAccount.isTransferAccountAuthorityEnabled` — authority transfer has been allowed for this account. ## Account lifecycle [#account-lifecycle] ### Close an account [#close-an-account] An account with no active balances can be closed to reclaim rent. `makeCloseAccountIx()` returns an `InstructionsWrapper` (`{ instructions, keys }`): ```typescript import { Transaction } from "@solana/web3.js"; if (wrappedAccount.activeBalances.length > 0) { throw new Error("Withdraw and repay everything before closing"); } const { instructions } = await wrappedAccount.makeCloseAccountIx(); const closeTx = new Transaction().add(...instructions); const sig = await wallet.sendTransaction(closeTx, connection); await connection.confirmTransaction(sig); ``` ### Transfer an account to a new authority [#transfer-an-account-to-a-new-authority] `makeAccountTransferToNewAccountTx(newMarginfiAccount, newAuthority, feePayer?)` migrates every position into a brand-new account owned by `newAuthority` and leaves the old account disabled. The new account keypair must sign (it creates itself), the current authority signs to authorize, and `feePayer` (a `PublicKey` for adapter signing or a `Keypair`; defaults to the current authority) pays the program's account-transfer fee and rent. ```typescript import { Keypair } from "@solana/web3.js"; const newMarginfiAccount = Keypair.generate(); const transferTx = await wrappedAccount.makeAccountTransferToNewAccountTx( newMarginfiAccount, newAuthority.publicKey ); // ExtendedV0Transaction: the new account keypair is a required signer transferTx.sign([newMarginfiAccount]); const sig = await wallet.sendTransaction(transferTx, connection); await connection.confirmTransaction(sig); ``` This instruction's account layout changes with program 0.1.10 — the SDK handles it for you, see [Program Upgrade 0.1.10](/typescript-sdk/program-upgrade). ### Move selected positions to another account [#move-selected-positions-to-another-account] `makeTransferPositionsTx` moves a chosen subset of positions (collateral or debt) from this account to a destination account atomically inside a flash loan. The side is inferred from this account's balance in each bank; debt positions are re-borrowed on the destination with a small interest padding. ```typescript const result = await wrappedAccount.makeTransferPositionsTx({ bankAddresses: [solBank.address, usdcBank.address], // destinationAccount: otherAccount, // omit to create a fresh account in the same tx // createDestinationOpts: { accountIndex: 1 }, // only used when destinationAccount is omitted // borrowPaddingBps: 10, // head-room over estimated debt; default 10 // maxPositions: 5, // reject larger selections; default 5 }); // result: TransferPositionsResult // transactions: ExtendedV0Transaction[] ordered [setup/crank txs…, flashloan tx] // actionTxIndex: number index of the flashloan tx // destinationAccount: MarginfiAccountType // mustBeAtomicBundle: boolean true → send as one Jito bundle ``` When `mustBeAtomicBundle` is `true` the transactions must land in one atomic bundle — see [Transactions](/typescript-sdk/transactions) for sending bundles. Failures throw a `TransactionBuildingError` with a `TRANSFER_POSITIONS_*` code; see [Errors](/typescript-sdk/errors). ### Bulk withdraw and repay [#bulk-withdraw-and-repay] For draining an account in one go, the Layer 3 actions `makeBulkWithdrawTx` and `makeBulkRepayTx` (from `@0dotxyz/p0-ts-sdk`, not on the wrapper) withdraw the full position of every listed bank, or repay the full debt of every listed bank, packing as many instructions per transaction as fit. Both return `{ transactions, actionTxIndex, mustBeAtomicBundle }`; bulk withdraws must land as one bundle because the integration refreshes live in a single prelude transaction. Being Layer 3, they take the client data explicitly (`program`, `connection`, `marginfiAccount`, `bankMap`, `tokenProgramsByBank`, and for withdraws also `oraclePrices`, `bankMetadataMap`, `assetShareValueMultiplierByBank`, `luts`), so build them from `client.*` fields as described in [Architecture](/typescript-sdk/architecture). ## Related [#related] * [Getting Started](/typescript-sdk/getting-started) — client setup and first deposit * [Health & Max Amounts](/typescript-sdk/health-and-max-amounts) — what `fetchAccount` simulates and how to read it * [Banks & Prices](/typescript-sdk/banks-and-prices) — the `bank` and `oraclePrice` objects used above * [Transactions](/typescript-sdk/transactions) — multi-transaction results and atomic bundles * Examples: [`18-mint-holders.ts`](https://github.com/0dotxyz/p0-ts-sdk/tree/main/examples/18-mint-holders.ts), [`06a-account-health-simulated.ts`](https://github.com/0dotxyz/p0-ts-sdk/tree/main/examples/06a-account-health-simulated.ts) --- # 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](/typescript-sdk/getting-started). ## The four layers [#the-four-layers] | Layer | What | Example | When to use | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | 4 — Account wrapper | `MarginfiAccountWrapper` methods that auto-fill every parameter from the account and client, then call Layer 3 | `wrappedAccount.makeBorrowTx(bankAddress, "100")` | Default. Standard lending UIs, bots, scripts. | | 3 — Transaction builders | Free functions that do the heavy lifting: oracle cranks, integration refreshes, lookup tables, transaction splitting | `makeBorrowTx({ ...params })` | You manage your own account/client state or need to pass parameters the wrapper does not expose. | | 2 — Instruction builders | Free functions (and wrapper `make*Ix` methods) that return `TransactionInstruction[]` with accounts derived for you, and nothing else | `makeBorrowIx({ ...params })` | Composing with other programs, flash-loan bodies, custom fee payers. | | 1 — Raw instructions | Thin Anchor wrappers from the `/instructions` entry; you derive every account yourself | `instructions.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 [#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. ```typescript 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 [#method-families] | Family | Methods | Documented on | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------- | | Core lending | `makeDepositTx`, `makeWithdrawTx`, `makeBorrowTx`, `makeRepayTx` | [Deposit, Withdraw, Borrow & Repay](/typescript-sdk/lending-and-borrowing) | | Looping | `makeLoopTx`, `makeBridgedLoopTx` | [Looping](/typescript-sdk/looping) | | Position swaps | `makeSwapCollateralTx`, `makeSwapDebtTx`, `makeBridgedSwapCollateralTx`, `makeBridgedSwapDebtTx`, `makeRepayWithCollatTx`, `makeRollPtTx` | [Position Swaps](/typescript-sdk/position-swaps) | | Flash loans | `makeFlashLoanTx`, `makeBeginFlashLoanIx`, `makeEndFlashLoanIx` | [Flash Loans](/typescript-sdk/flash-loans) | | Integration banks | `makeKaminoDepositTx`, `makeKaminoWithdrawTx`, `makeDriftDepositTx`, `makeDriftWithdrawTx` | [Integration Banks & Native Stake](/typescript-sdk/integrations) | | Account lifecycle | `makeCloseAccountIx`, `makeAccountTransferToNewAccountTx`, `makeTransferPositionsTx`, `makePulseHealthIx` | [Accounts](/typescript-sdk/accounts) | | Health, max amounts, e-mode | `simulateHealthCache`, `computeHealthComponentsFromCache`, `computeFreeCollateralFromCache`, `computeAccountValue`, `computeNetApy`, `computeMaxBorrowForBank`, `computeMaxWithdrawForBank`, `computeMaxDepositForBank`, `getActiveEmodePairs`, `getActiveEmodeWeightsByBank`, `getEmodeImpacts` | [Health, Max Amounts & E-mode](/typescript-sdk/health-and-max-amounts) | | Single instructions | `makeDepositIx`, `makeWithdrawIx`, `makeBorrowIx`, `makeRepayIx` | Layer 2, below | ## Layer 3: Transaction builders [#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](/typescript-sdk/transactions). Calling a builder directly looks like this — the parameter names are `MakeBorrowTxParams`: ```typescript 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 [#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: ```typescript 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): ```typescript 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: ```typescript 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](/typescript-sdk/flash-loans), 3. batching several account operations, or 4. using a custom fee payer or your own priority-fee / compute-budget instructions. ## Layer 1: Raw 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): ```typescript 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 inserted a required account into six instructions, and the Layer 2+ builders handled the switch while raw instructions did not; see [Program Upgrade 0.1.10](/typescript-sdk/program-upgrade). Program 0.1.11 changes what goes into `remainingAccounts` instead: banks on the new Scope and exchange-rate oracle setups need their pricing account in the health pack. `computeHealthAccountMetas` builds the right list for every setup — if you assemble it by hand, read [Program Upgrade 0.1.11](/typescript-sdk/program-upgrade-0-1-11). ### Sync instructions [#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 [#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](/typescript-sdk/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 [#summary] ```typescript // 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. ## Related [#related] * [Transactions](/typescript-sdk/transactions) — what Layer 3 adds: result types, cranking, LUTs, bundles, size limits * [Deposit, Withdraw, Borrow & Repay](/typescript-sdk/lending-and-borrowing) — the Layer 4 core methods * [Flash Loans](/typescript-sdk/flash-loans) — the most common reason to use Layer 2 * [Program Upgrade 0.1.10](/typescript-sdk/program-upgrade) — what changed for Layer 1 users on 0.1.10 * [Program Upgrade 0.1.11](/typescript-sdk/program-upgrade-0-1-11) — health-pack accounts for the new oracle setups * Examples: [`02-borrow.ts`](https://github.com/0dotxyz/p0-ts-sdk/tree/main/examples/02-borrow.ts) --- # Banks & Prices Find banks, understand asset tags, read bank metrics and rate limiters, and work with oracle prices. A bank is a lending pool for one mint with its own interest curve, caps, weights and oracle. This page is the home for everything bank- and price-related in the SDK. It assumes you have a `client` from [Getting Started](/typescript-sdk/getting-started). ## Finding banks [#finding-banks] `Project0Client.initialize` loads every bank in the group once. You then read them from memory: ```typescript import { PublicKey } from "@solana/web3.js"; import { AssetTag } from "@0dotxyz/p0-ts-sdk"; // All banks as an array, or as a Map keyed by base58 address const all = client.banks; const byAddress = client.bankMap; // Map // One bank by address (returns `Bank | undefined`) const bank = client.getBank(new PublicKey("BANK_ADDRESS")); if (!bank) throw new Error("Bank not found"); // All banks for a mint, optionally filtered by asset tag const USDC_MINT = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); const usdcBanks = client.getBanksByMint(USDC_MINT); // may include Kamino/Drift/JupLend wrappers const [usdcBank] = client.getBanksByMint(USDC_MINT, AssetTag.DEFAULT); ``` Useful fields on a `Bank`: `address`, `mint`, `mintDecimals`, `tokenSymbol`, `config` (asset tag, weights, caps, `operationalState`, `interestRateConfig`, `oracleKeys`), `rateLimiter`, `lastUpdate`, and the share-conversion helpers `getAssetQuantity(shares)` / `getLiabilityQuantity(shares)`. ## Asset tags and comingling rules [#asset-tags-and-comingling-rules] The SDK uses `AssetTag` to determine what kinds of assets can be held together in a single account. This prevents incompatible positions from being comingled: | Tag | Value | Meaning | | ------------------ | ----- | ---------------------------------------------------------------------------------------------------------------------- | | `AssetTag.DEFAULT` | 0 | Regular assets (USDC, USDT, …) that can be comingled with other DEFAULT assets or with SOL | | `AssetTag.SOL` | 1 | Only the native SOL bank. Accounts with SOL can hold **either** DEFAULT assets **or** STAKED assets, not both | | `AssetTag.STAKED` | 2 | Native-stake LSTs. Accounts with STAKED positions can only deposit other STAKED assets or SOL, and can only borrow SOL | | `AssetTag.KAMINO` | 3 | Kamino integration banks | | `AssetTag.DRIFT` | 4 | Drift integration banks | | `AssetTag.SOLEND` | 5 | Solend integration banks (not yet active) | | `AssetTag.JUPLEND` | 6 | Jupiter Lend integration banks | **Comingling rules:** * DEFAULT assets can mix with other DEFAULT assets and SOL * SOL can mix with DEFAULT assets or STAKED assets (but not both in the same account) * STAKED assets can only mix with other STAKED assets or SOL * You cannot hold both DEFAULT and STAKED assets in the same account (unless only SOL is present) ```typescript const usdcBank = client.getBanksByMint(USDC_MINT, AssetTag.DEFAULT)[0]; const solBank = client.getBanksByMint(SOL_MINT, AssetTag.SOL)[0]; const lstBanks = client.banks.filter((b) => b.config.assetTag === AssetTag.STAKED); const kaminoBanks = client.banks.filter((b) => b.config.assetTag === AssetTag.KAMINO); const driftBanks = client.banks.filter((b) => b.config.assetTag === AssetTag.DRIFT); const juplendBanks = client.banks.filter((b) => b.config.assetTag === AssetTag.JUPLEND); ``` Integration banks (Kamino, Drift, Jupiter Lend) reuse the same mint as a standard bank but are deposit-only wrappers with their own instructions — see [Integrations](/typescript-sdk/integrations). ### Depositable and borrowable predicates [#depositable-and-borrowable-predicates] Two helpers tell you whether a bank accepts the *standard* deposit/borrow instructions, which is what you want when choosing a bank for a loop, swap or bridge hop: ```typescript import { isStandardDepositable, isStandardBorrowable } from "@0dotxyz/p0-ts-sdk"; // DEFAULT/SOL tag and Operational state isStandardDepositable(bank); // DEFAULT/SOL tag, Operational state, and borrowLimit > 0 isStandardBorrowable(bank); ``` Integration wrappers set `borrowLimit = 0` and reject a standard borrow with program error `6200 WrongAssetTagForStandardInstructions`; `ReduceOnly`/`Paused` banks reject new deposits with `6017 BankReduceOnly`. ## Bank metrics [#bank-metrics] `computeBankMetrics` aggregates every number a bank UI usually shows. It is pure: pass whatever `(bank, oraclePrice)` snapshot you want reflected. ```typescript import { computeBankMetrics } from "@0dotxyz/p0-ts-sdk"; const key = usdcBank.address.toBase58(); const metrics = computeBankMetrics({ bank: usdcBank, oraclePrice: client.oraclePriceByBank.get(key)!, assetShareValueMultiplier: client.assetShareValueMultiplierByBank.get(key), // Kamino/Drift/JupLend only symbol: usdcBank.tokenSymbol, }); console.log(`${metrics.symbol}: supply ${(metrics.supplyApy * 100).toFixed(2)}% / borrow ${(metrics.borrowApy * 100).toFixed(2)}%`); console.log(`utilization ${(metrics.utilizationRate * 100).toFixed(1)}%, TVL $${metrics.totalDepositsUsd.toFixed(0)}`); ``` All amounts are UI-scaled numbers; USD values use the realtime oracle price with no margin weight. When you only need one or two numbers, call the per-metric helpers directly (no oracle math is run for rate or capacity helpers): | Helper | Returns | | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | | `computeBankSupplyApy(bank)` / `computeBankBorrowApy(bank)` | APY as a `number` fraction | | `computeInterestRates(bank)` | `{ lendingRate, borrowingRate }` APRs as `BigNumber` | | `computeUtilizationRate(bank)` | `BigNumber` in \[0, 1] | | `computeRemainingCapacity(bank)` | `{ depositCapacity, borrowCapacity }` in native units, interest-buffered | | `computeBankDepositCapRemaining(bank)` / `computeBankBorrowCapRemaining(bank)` | UI `number`, `Infinity` when the cap is inactive | | `computeBankAvailableLiquidity(bank, multiplier?)` | `BigNumber` UI units: max(0, deposits − borrows) — what the vault can pay out now | | `computeBankProjectedAvailableLiquidity(bank, multiplier?)` | Same, minus the liquidity that interest accrual will consume before your transaction lands | | `computeBankTotalDeposits(bank, multiplier?)` / `computeBankTotalBorrows(bank)` | UI `number` | `computeBankProjectedAvailableLiquidity` is the one the wrapper's max-withdraw/max-borrow helpers use, because the program accrues interest before its liquidity check. The interest curve and caps are described on [Lending & Borrowing](/protocol-overview/lending-and-borrowing) and [Caps & Rate Limits](/protocol-overview/caps-and-rate-limits). ## Rate limiters [#rate-limiters] Banks and the group can carry net-outflow rate limiters (hourly and daily sliding windows) that cap withdrawals plus borrows. The SDK mirrors the on-chain window math read-only: ```typescript import { computeBankRateLimitRemaining, computeGroupRateLimitRemainingUsd, } from "@0dotxyz/p0-ts-sdk"; // Remaining bank-level outflow in UI units of the bank's mint, or null if no limiter is enabled const bankRemaining = computeBankRateLimitRemaining(usdcBank); // Remaining group-level outflow in USD, or null if the group has no limiter const groupRemainingUsd = computeGroupRateLimitRemainingUsd(client.group.rateLimiter); console.log(bankRemaining?.toFixed(2) ?? "no bank limiter", groupRemainingUsd?.toFixed(2) ?? "no group limiter"); ``` Both accept an optional `nowSeconds` second argument (defaults to `Date.now() / 1000`). `computeMaxBorrowForBank` and `computeMaxWithdrawForBank` already take these into account — see [Health & Max Amounts](/typescript-sdk/health-and-max-amounts). Which flows are exempt and how windows roll over is explained on [Caps & Rate Limits](/protocol-overview/caps-and-rate-limits). ## Oracle prices [#oracle-prices] The client fetches a price for every bank at initialization and keeps it in `client.oraclePriceByBank` (keyed by base58 bank address): ```typescript const oraclePrice = client.oraclePriceByBank.get(usdcBank.address.toBase58()); if (!oraclePrice) throw new Error("No price for bank"); console.log(`Realtime: $${oraclePrice.priceRealtime.price.toFixed(4)} ±${oraclePrice.priceRealtime.confidence.toFixed(4)}`); console.log(`Weighted (EMA): $${oraclePrice.priceWeighted.price.toFixed(4)}`); console.log(`Updated: ${new Date(oraclePrice.timestamp.toNumber() * 1000).toISOString()}`); ``` An `OraclePrice` has two `PriceWithConfidence` legs plus a timestamp: `PriceWithConfidence` is `{ price, confidence, lowestPrice, highestPrice }` (all `BigNumber`), where `lowestPrice`/`highestPrice` are `price ∓ confidence` after the SDK applies the same confidence-interval scaling and cap as the program. The program values collateral at the low bound and liabilities at the high bound; use `getPrice` to pick the same way: ```typescript import { getPrice, getPriceWithConfidence, PriceBias } from "@0dotxyz/p0-ts-sdk"; const collateralPrice = getPrice(oraclePrice, PriceBias.Lowest); // realtime, low bound const debtPrice = getPrice(oraclePrice, PriceBias.Highest, true); // weighted, high bound const mid = getPrice(oraclePrice); // PriceBias.None, realtime const realtimeLeg = getPriceWithConfidence(oraclePrice, false); // === oraclePrice.priceRealtime ``` ### Refreshing prices [#refreshing-prices] The cached prices go stale in a long-running process. Refresh them with `fetchOracleData` and use the returned maps (the client's map is read-only, so either rebuild the client or keep your own map for computations): ```typescript import { fetchOracleData } from "@0dotxyz/p0-ts-sdk"; const { bankOraclePriceMap, mintOraclePriceMap } = await fetchOracleData(client.banks, { pythOpts: { mode: "on-chain", connection }, // or { mode: "api", pythOnchainData: {...}, stakedCollatData: {...} } swbOpts: { mode: "on-chain", connection }, scopeOpts: { mode: "on-chain", connection }, // or { mode: "api", scopeOnchainData: {...} } oracleMultiplierOpts: { mode: "on-chain", connection }, // or { mode: "api", multiplierData: {...} } isolatedBanksOpts: { fetchPrices: true }, }); const freshUsdc = bankOraclePriceMap.get(usdcBank.address.toBase58()); ``` `fetchOracleData` routes each bank by its oracle setup: zero-oracle banks get a zero price, fixed-price banks their constant, and Pyth, Switchboard and Scope banks are fetched from chain (`mode: "on-chain"`) or from your own price API (`mode: "api"`). Oracle *cranking* — writing fresh Switchboard updates on-chain before a borrow or withdraw — is handled by the action builders and documented on [Transactions](/typescript-sdk/transactions). How the protocol picks and validates oracles is on [Oracles](/protocol-overview/oracles). ### Scope and exchange-rate oracles (program 0.1.11) [#scope-and-exchange-rate-oracles-program-0111] Program 0.1.11 adds two families of oracle setup, and `fetchOracleData` takes one optional option block for each: * `scopeOpts` — banks with `OracleSetup.Scope`. The SDK reads the bank's entry (`config.scopeEntryIndex`) out of the Scope `OraclePrices` account in `oracleKeys[0]` and applies the bank's `oracleMaxAge` to the entry timestamp. Scope prices carry **no confidence interval**, so `lowestPrice === price === highestPrice`. * `oracleMultiplierOpts` — banks priced as a Pyth base feed times an on-chain exchange rate: `PythMSOL` / `KaminoMSOL` / `JuplendMSOL` (Marinade), `PythLST` / `KaminoLST` / `JuplendLST` (SPL and Sanctum stake pools) and `PTPyth` / `PTFixed` (Exponent vaults). The SDK fetches the rate account, mirrors the program's validation (rate bounds, stake-pool epoch staleness, PT maturity schedule) and **bakes the rate into the price**: an mSOL bank's `OraclePrice` is mSOL/USD, a jitoSOL bank's is jitoSOL/USD, a PT bank's is the PT's own USD price. This matches the price the program caches on the bank. A venue's receipt-token rate (Kamino cToken, JupLend fToken) is *not* in the price — it stays in `client.assetShareValueMultiplierByBank`, as for every other venue bank. Both options are optional so existing callers keep compiling. If you omit one while such banks exist, those banks are priced at **zero** and the SDK logs a warning — they are never silently priced from the bare base feed. `Project0Client.initialize` passes both in on-chain mode for you. A bank whose setup this SDK version does not know parses as `OracleSetup.Unknown` (source `"unknown"`, zero price) instead of failing to decode, so a newer program release cannot break bank loading. ## Bank state [#bank-state] `bank.config.operationalState` is an `OperationalState` enum value: | State | Effect | | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | | `Operational` | Normal operation | | `ReduceOnly` | No new deposits or borrows; withdraws and repays allowed | | `ReduceOnlyWithBorrowingPower` | Reduce-only, but existing deposits still count as collateral | | `Paused` | All operations blocked | | `Uninitialized` | Awaiting a Jupiter Lend seed deposit; all operations blocked | | `KilledByBankruptcy` | Bank was wound down after a bankruptcy | | `CircuitBroken` | Halted by the per-bank oracle circuit breaker (0.1.10+); risk-increasing operations are rejected until the price settles | `CircuitBroken` only decodes with SDK `>= 2.7.0`; see [Program Upgrade 0.1.10](/typescript-sdk/program-upgrade). The protocol-level description of each state is on [Bank States](/protocol-overview/bank-states). ```typescript import { OperationalState } from "@0dotxyz/p0-ts-sdk"; const active = client.banks.filter((b) => b.config.operationalState === OperationalState.Operational); ``` ## Related [#related] * [Lending & Borrowing](/typescript-sdk/lending-and-borrowing) — using a bank once you have picked one * [Health & Max Amounts](/typescript-sdk/health-and-max-amounts) — bank-aware max borrow/withdraw/deposit * [Integrations](/typescript-sdk/integrations) — Kamino, Drift and Jupiter Lend banks * [Oracles](/protocol-overview/oracles) and [Caps & Rate Limits](/protocol-overview/caps-and-rate-limits) — protocol background * Example: [`05-oracle-prices.ts`](https://github.com/0dotxyz/p0-ts-sdk/tree/main/examples/05-oracle-prices.ts) --- # Error Handling Recognise and recover from SDK build errors, bundle simulation failures and the program errors you will actually meet. Errors show up at three points: while the SDK **builds** a transaction (typed `TransactionBuildingError`), while you **simulate** it (`BundleSimulationError` or a failed simulation result), and when the **program** rejects it on-chain (Anchor error codes in the logs). This page covers how to tell them apart and what to do next. This page assumes you have a `client` and `wrappedAccount` from [Getting Started](/typescript-sdk/getting-started). ## TransactionBuildingError [#transactionbuildingerror] Every `make*Tx` builder throws `TransactionBuildingError` when it cannot produce a valid transaction for a reason it understands. The class carries a machine-readable `code` and typed `details`: ```typescript class TransactionBuildingError extends Error { readonly name: "TransactionBuildingError"; readonly code: T; // e.g. "ORACLE_CRANK_FAILED" readonly details: TransactionBuildingErrorDetails[T]; // shape depends on code } ``` `error.message` is diagnostic text for logs, **not** user-facing copy. Map `error.code` to your own strings and use `details` to fill in bank symbols or amounts. ### Error codes [#error-codes] | Code | When it happens | `details` | | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | `SWAP_SIZE_EXCEEDED_LOOP` | `makeLoopTx`: the swap route does not fit the flash-loan transaction (bytes or account keys). | `{ bytes, accountKeys, provider? }` | | `SWAP_SIZE_EXCEEDED_REPAY` | `makeRepayWithCollatTx`: same, for the repay-with-collateral flash loan. | `{ bytes, accountKeys, provider? }` | | `SWAP_SIZE_EXCEEDED_POSITION_SWAP` | `makeSwapCollateralTx` / `makeSwapDebtTx` / `makeRollPtTx`: same, for a position swap. | `{ bytes, accountKeys, provider? }` | | `SWAP_QUOTE_FAILED` | No swap provider (including `fallbackProviders`) returned a usable quote for the pair. | `{ provider, inputMint, outputMint, reason }` | | `ORACLE_CRANK_FAILED` | A borrow/withdraw/flash-loan needs fresh Switchboard prices for one of the account's banks and the SDK could not build a crank for them. | `{ uncrankableLiabilities: [{ bankAddress, mint, symbol?, reason }], uncrankableAssets: [...] }` | | `SWITCHBOARD_FEED_UPDATE_FAILED` | Reserved for Switchboard feed-update failures (not thrown by the current builders; cranks surface as `ORACLE_CRANK_FAILED`). | `{ oracleKeys, reason }` | | `KAMINO_RESERVE_NOT_FOUND` | An action touches a Kamino bank but `client.bankIntegrationMap` has no `kaminoStates.reserveState` for it (client built without integration data, or stale). | `{ bankAddress, bankMint, bankSymbol? }` | | `DRIFT_STATE_NOT_FOUND` | Same for a Drift bank (`driftStates` missing). | `{ bankAddress, bankMint, bankSymbol? }` | | `JUPLEND_STATE_NOT_FOUND` | Same for a Jupiter Lend bank (`jupLendStates` missing). | `{ bankAddress, bankMint, bankSymbol? }` | | `TRANSFER_POSITIONS_INVALID_SELECTION` | `makeTransferPositionsTx`: the selected banks are not valid to move (inactive on the source, overlap/slot conflict on the destination, group or authority mismatch). | `{ reason, bankAddresses }` | | `TRANSFER_POSITIONS_UNSUPPORTED_BANK` | `makeTransferPositionsTx`: a selected bank's asset tag is not supported (v1 moves `DEFAULT` and `STAKED` only). | `{ bankAddress, assetTag, bankSymbol? }` | | `TRANSFER_POSITIONS_UNSPLITTABLE` | `makeTransferPositionsTx`: even at the position cap the transfer does not fit one v0 transaction. Retry with fewer positions. | `{ reason, sizeBytes?, accountCount? }` | | `BRIDGE_CONFLICT` | A `makeBridged*Tx` builder could not pick a bridge token: every candidate bank already holds an opposite-side position on the account. | `{ conflictingBanks: [{ bankAddress, mint, symbol? }], bridgeTokenSide: "deposit" \| "borrow" }` | ### Predicates [#predicates] Two helpers classify the errors you can recover from automatically: * `isDecomposableSwapError(e)` — true for `SWAP_SIZE_EXCEEDED_*` and `SWAP_QUOTE_FAILED`. The direct single-route swap failed, but the same action can usually succeed as a two-leg swap through a bridge token. Retry with the bridged builder (`makeBridgedLoopTx`, `makeBridgedSwapCollateralTx`, `makeBridgedSwapDebtTx`). * `isBridgeConflictError(e)` — true for `BRIDGE_CONFLICT`, narrowed to its typed details. The bridged builder also failed because every bridge candidate conflicts with an existing position. Either exclude those banks by passing your own `bridgeOpts.bridgeCandidateMints`, or tell the user to close the conflicting position / pick a different pair. ```typescript import { isDecomposableSwapError, isBridgeConflictError } from "@0dotxyz/p0-ts-sdk"; try { result = await wrappedAccount.makeLoopTx(params); } catch (e) { if (!isDecomposableSwapError(e)) throw e; try { result = await wrappedAccount.makeBridgedLoopTx(params); } catch (bridged) { if (isBridgeConflictError(bridged)) { const blocked = bridged.details.conflictingBanks.map((b) => b.symbol ?? b.mint); throw new Error(`Close your ${bridged.details.bridgeTokenSide} position in ${blocked.join(", ")} first`); } throw bridged; } } ``` The `makeBridged*Tx` builders already attempt the direct route first and only fall back to a bridge when the direct build throws a decomposable error, so calling them up front gives you the same behaviour in one call. The full flow is on [Looping](/typescript-sdk/looping). ### A catch-and-switch pattern [#a-catch-and-switch-pattern] ```typescript import { TransactionBuildingError, TransactionBuildingErrorCode, type TransactionBuildingErrorDetails, } from "@0dotxyz/p0-ts-sdk"; function describeBuildError(e: unknown): string { if (!(e instanceof TransactionBuildingError)) { return "Something went wrong while preparing the transaction."; } switch (e.code) { case TransactionBuildingErrorCode.ORACLE_CRANK_FAILED: { const d = e.details as TransactionBuildingErrorDetails[TransactionBuildingErrorCode.ORACLE_CRANK_FAILED]; const banks = [...d.uncrankableLiabilities, ...d.uncrankableAssets].map((b) => b.symbol ?? b.mint); return `Price feeds for ${banks.join(", ")} could not be refreshed. Try again in a moment.`; } case TransactionBuildingErrorCode.SWAP_QUOTE_FAILED: return "No swap route is available for this pair right now."; case TransactionBuildingErrorCode.SWAP_SIZE_EXCEEDED_LOOP: case TransactionBuildingErrorCode.SWAP_SIZE_EXCEEDED_REPAY: case TransactionBuildingErrorCode.SWAP_SIZE_EXCEEDED_POSITION_SWAP: return "The swap route is too large for a single transaction. Try a smaller amount."; case TransactionBuildingErrorCode.KAMINO_RESERVE_NOT_FOUND: case TransactionBuildingErrorCode.DRIFT_STATE_NOT_FOUND: case TransactionBuildingErrorCode.JUPLEND_STATE_NOT_FOUND: return "Integration data for this market is not loaded. Refresh and try again."; case TransactionBuildingErrorCode.BRIDGE_CONFLICT: return "This swap needs a bridge asset you already hold on the other side of your account."; case TransactionBuildingErrorCode.TRANSFER_POSITIONS_INVALID_SELECTION: case TransactionBuildingErrorCode.TRANSFER_POSITIONS_UNSUPPORTED_BANK: case TransactionBuildingErrorCode.TRANSFER_POSITIONS_UNSPLITTABLE: return "These positions cannot be transferred together. Select fewer positions."; default: return "Could not prepare the transaction."; } } ``` `TransactionBuildingErrorDetails` is the exported map from code to details type. `e.details` is a union across all codes, so narrow it with a cast (as above) or use the predicates, which return the narrowed generic. ## BundleSimulationError and HealthCacheSimulationError [#bundlesimulationerror-and-healthcachesimulationerror] `simulateBundle(rpcEndpoint, transactions, includeAccounts?)` throws when the request itself fails — empty input, a transaction that cannot be encoded, an HTTP error, or an RPC that does not support `simulateBundle` (the underlying `SolanaJSONRPCError` is rethrown as-is). The error has `name === "BundleSimulationError"`, plus optional `logs` and `cause`. The class itself is not exported, so match on `name`: ```typescript try { const results = await simulateBundle(connection.rpcEndpoint, result.transactions); results.forEach((r, i) => { if (r.err) console.error(`tx ${i} failed`, r.err, r.logs); }); } catch (e) { if (e instanceof Error && e.name === "BundleSimulationError") { console.error("Bundle simulation unavailable:", e.message); } throw e; } ``` A successful call can still return entries whose `err` is set — that is the program rejecting the transaction in simulation, and the `logs` contain the Anchor error (see below). `HealthCacheSimulationError` (exported) is what `simulateHealthCache()` / `simulateAccountHealthCacheWithFallback` return in their `error` field when the on-chain health simulation failed and the SDK fell back to a local computation. It carries `mrgnErr` and `internalErr` (program error numbers or `null`). It is returned, not thrown — see [Health, Max Amounts & E-mode](/typescript-sdk/health-and-max-amounts). ## Program errors worth recognizing [#program-errors-worth-recognizing] When a transaction fails in simulation or on-chain, the logs contain `Error Code: . Error Number: `. The SDK exports the IDL, so you can resolve a number to a name with `MARGINFI_IDL.errors.find((e) => e.code === n)`. The ones you will see most: | Code | Name | Meaning and fix | | --------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `6009` | `RiskEngineInitRejected` | The action would leave Initial health negative, or an oracle used in the health check is stale. Re-check `computeMaxBorrowForBank` / `computeMaxWithdrawForBank` and retry. | | `6003` / `6027` | `BankAssetCapacityExceeded` / `BankLiabilityCapacityExceeded` | Deposit or borrow cap hit. `computeMaxDepositForBank` / `computeMaxBorrowForBank` already clamp to the caps. | | `6016` | `BankPaused` | Bank is paused. Check `bank.config.operationalState`. | | `6017` | `BankReduceOnly` | Bank is in reduce-only mode: deposits and borrows are rejected, withdrawals and repays still work. Filter with `isStandardDepositable` / `isStandardBorrowable`. | | `6029` | `IsolatedAccountIllegalState` | An account with isolated-tier debt can hold only that one liability. `computeMaxBorrowForBank` returns `0` in this case. | | `6115`–`6118` | `Bank/GroupHourly/DailyRateLimitExceeded` | Net-outflow rate limiter hit. `computeMax*ForBank` clamp to it unless `ignoreBankLimits` was passed; see [Caps & Rate Limits](/protocol-overview/caps-and-rate-limits). | | `6200` | `WrongAssetTagForStandardInstructions` | A plain deposit/withdraw/borrow/repay was sent to a Kamino, Drift or Jupiter Lend bank. Use the integration builders from [Integrations](/typescript-sdk/integrations). | | `6600` | `BankCircuitBreakerHalted` | The bank is halted by its oracle circuit breaker (`OperationalState.CircuitBroken`); user actions are rejected until it is cleared. See [Banks & Prices](/typescript-sdk/banks-and-prices). | | `6136`–`6139` | `MarinadeStateValidationFailed` / `ExponentVaultValidationFailed` / `InvalidPtStartPrice` / `StakePoolStale` | The exchange-rate account of an mSOL, LST or PT bank failed validation, or the LST's stake pool has not been updated in the current or previous epoch. Nothing to do client-side beyond retrying once the pool is cranked; the SDK prices such a bank at zero in the meantime. | | `6800`–`6802` | `ScopeInvalidAccount` / `ScopeInvalidEntry` / `ScopeStalePrice` | A Scope bank's `OraclePrices` account is malformed, the configured entry was never refreshed, or the entry is older than the bank's `oracleMaxAge`. Retry once the feed operator has refreshed the entry; see [Program Upgrade 0.1.11](/typescript-sdk/program-upgrade-0-1-11). | | — | `TypeError: Cannot read properties of null (reading 'property')` while decoding banks | SDK `<= 2.7.4` decoding a bank on a 0.1.11 oracle setup — the old IDL has no variant for it. Upgrade to SDK `>= 2.8.0`; see [Program Upgrade 0.1.11](/typescript-sdk/program-upgrade-0-1-11#the-breaking-change-decoding). | | — | `AccountNotEnoughKeys` (Anchor, on `group`) | An instruction built for program 0.1.9 hit the 0.1.10 program (or vice versa), typically `end_flashloan` or `pulse_health`. Upgrade to SDK `>= 2.7.0`; see [Program Upgrade troubleshooting](/typescript-sdk/program-upgrade#troubleshooting). | ```typescript import { MARGINFI_IDL } from "@0dotxyz/p0-ts-sdk"; function programErrorName(logs: string[]): string | undefined { const match = logs.join("\n").match(/Error Number: (\d+)/); if (!match) return undefined; const code = Number(match[1]); return MARGINFI_IDL.errors.find((e) => e.code === code)?.name; } ``` ## Simulate before you send [#simulate-before-you-send] Build errors are cheap; on-chain failures cost a fee and a confirmation round-trip. Simulate every result before signing — `connection.simulateTransaction` for a single transaction, `simulateBundle` for a `TransactionBuilderResult` with several — and treat a non-null `err` as a program error using the table above. Simulation, bundles and fee settings are covered on [Transactions](/typescript-sdk/transactions). ## Related [#related] * [Transactions](/typescript-sdk/transactions) — `simulateBundle`, oracle cranking, bundles * [Looping](/typescript-sdk/looping) — the direct-then-bridged retry that uses `isDecomposableSwapError` * [Health, Max Amounts & E-mode](/typescript-sdk/health-and-max-amounts) — avoid `RiskEngineInitRejected` before it happens * [Program Upgrade 0.1.10](/typescript-sdk/program-upgrade) — `AccountNotEnoughKeys` and the 0.1.10 flip * [Program Upgrade 0.1.11](/typescript-sdk/program-upgrade-0-1-11) — the Scope and exchange-rate oracle errors, and why SDK `< 2.8.0` fails to decode banks * Examples: [`16a-loop.ts`](https://github.com/0dotxyz/p0-ts-sdk/tree/main/examples/16a-loop.ts) (`isDecomposableSwapError`), [`10-swap-collateral.ts`](https://github.com/0dotxyz/p0-ts-sdk/tree/main/examples/10-swap-collateral.ts) (`isBridgeConflictError`), [`02-borrow.ts`](https://github.com/0dotxyz/p0-ts-sdk/tree/main/examples/02-borrow.ts) (bundle simulation with error logs) --- # Flash Loans How marginfi flash loans work, how the SDK uses them under the hood, and how to wrap your own instruction sequence with makeFlashLoanTx. A marginfi flash loan lets you borrow without collateral as long as the account is healthy again by the end of the same transaction. The SDK's loop, swap and repay-with-collateral builders are all flash loans internally; this page shows how to compose one yourself. It assumes you have a `client` and `wrappedAccount` from [Getting Started](/typescript-sdk/getting-started). For the protocol-level rules see the [Flashloans guide](/guides/flashloans). ## How flash loans work [#how-flash-loans-work] A flash loan is a pair of instructions that bookend a transaction: | Position | Instruction | What it does | | -------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | first | `lending_account_start_flashloan` (`makeBeginFlashLoanIx`) | Flags the account as in-flashloan and records `endIndex`, the instruction index where the matching end must sit. | | middle | any instructions | Borrows, withdraws, swaps, deposits, repays, CPIs into other programs. No health check runs here. | | last | `lending_account_end_flashloan` (`makeEndFlashLoanIx`) | Clears the flag and runs the risk engine's health check with the oracle accounts for every bank the account holds. | If the account is unhealthy at `end_flashloan`, or the end instruction is missing or in the wrong slot, the whole transaction reverts. Flash loans cannot be nested, cannot be called through CPI, and have no fee. ## How the SDK uses them [#how-the-sdk-uses-them] You rarely need to build flash loans by hand. These wrapper methods already produce a flash-loan transaction (plus any setup transactions for oracle cranks or integration refreshes): * [`makeLoopTx` / `makeBridgedLoopTx`](/typescript-sdk/looping) — borrow → swap → deposit * [`makeSwapCollateralTx`, `makeSwapDebtTx`, `makeRepayWithCollatTx`, `makeRollPtTx`](/typescript-sdk/position-swaps) — rebalance an open position * [`makeTransferPositionsTx`](/typescript-sdk/accounts#move-selected-positions-to-another-account) — move collateral and debt between accounts atomically Reach for `makeFlashLoanTx` when you need a sequence the SDK does not offer as a builder: a custom swap venue, a liquidation, arbitrage, or a multi-step rebalance. ## Building your own [#building-your-own] `wrappedAccount.makeFlashLoanTx(params)` takes your inner instructions and returns an `ExtendedV0Transaction` with the layout `[begin, ...ixs, end]`. It fills in `program` and `marginfiAccount`; you supply the rest: Under the hood it runs `computeProjectedActiveBanksNoCpi` over `ixs` — decoding the marginfi deposit/borrow/repay/withdraw instructions it finds and ignoring everything else — to know which banks will be active after your sequence, and feeds that list to `makeEndFlashLoanIx` as the health-check accounts. `endIndex` is `ixs.length + 1`. ### Inner instructions [#inner-instructions] Build marginfi instructions with the Layer-2 wrapper methods; each returns an `InstructionsWrapper` (`{ instructions, keys }`) with the bank, token program and metadata looked up for you: | Method | Signature | | ---------------- | ----------------------------------------------------------------------- | | `makeDepositIx` | `(bankAddress, amount, opts?: MakeDepositIxOpts)` | | `makeBorrowIx` | `(bankAddress, amount, opts?: MakeBorrowIxOpts)` | | `makeRepayIx` | `(bankAddress, amount, repayAll = false, opts?: MakeRepayIxOpts)` | | `makeWithdrawIx` | `(bankAddress, amount, withdrawAll = false, opts?: MakeWithdrawIxOpts)` | Amounts are UI units (`number`, `string` or `BigNumber`). Any non-marginfi instruction (a DEX swap, a CPI into your own program) goes in between as-is. The lower-level `wrappedAccount.makeBeginFlashLoanIx(endIndex)` and `makeEndFlashLoanIx(projectedActiveBanks)` are exposed too if you want to assemble the message yourself. ### Complete example [#complete-example] Borrow USDC, run an arbitrary instruction against it, and deposit the proceeds as SOL collateral, all in one flash loan: ```typescript import { ComputeBudgetProgram, TransactionInstruction } from "@solana/web3.js"; import { AssetTag, simulateBundle } from "@0dotxyz/p0-ts-sdk"; const usdcBank = client.getBanksByMint(USDC_MINT, AssetTag.DEFAULT)[0]; const solBank = client.getBanksByMint(SOL_MINT, AssetTag.DEFAULT)[0]; // 1. Marginfi legs via the wrapper (UI units) const borrowIxs = await wrappedAccount.makeBorrowIx(usdcBank.address, "100"); const depositIxs = await wrappedAccount.makeDepositIx(solBank.address, "0.5"); // 2. Whatever happens in between — a swap, a CPI, ... (placeholder) const yourIxs: TransactionInstruction[] = await buildYourSwapIxs({ from: USDC_MINT, to: SOL_MINT, amountUi: 100, owner: wallet.publicKey, }); // 3. Wrap: [begin, cu, borrow, ...yourIxs, deposit, end] const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash(); const flashLoanTx = await wrappedAccount.makeFlashLoanTx({ ixs: [ ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }), ...borrowIxs.instructions, ...yourIxs, ...depositIxs.instructions, ], bankMap: client.bankMap, blockhash, addressLookupTableAccounts: [...client.addressLookupTables, ...yourLuts], }); // 4. Simulate, sign, send const [sim] = await simulateBundle(connection.rpcEndpoint, [flashLoanTx]); if (sim.err) throw new Error(`flash loan failed: ${JSON.stringify(sim.err)}\n${sim.logs?.join("\n")}`); const signed = await wallet.signTransaction(flashLoanTx); const signature = await connection.sendRawTransaction(signed.serialize()); await connection.confirmTransaction({ signature, blockhash, lastValidBlockHeight }); ``` The deposit amount here is fixed up front. The SDK's own loop builder instead sizes the deposit from the swap's guaranteed min-out after the route is known; if you need the same, compute your expected output before building `depositIxs`. Compute-budget instructions belong inside `ixs` — the SDK's own builders put them first. Do not prepend them outside the flash loan, since `begin_flashloan` must be the first instruction and `endIndex` is computed from `ixs.length`. ## Constraints and gotchas [#constraints-and-gotchas] * **One authority.** Every inner marginfi instruction must act on `wrappedAccount`'s account and be signed by its authority; the transaction's fee payer is that authority. * **End healthy.** Whatever happens in the middle, the account must pass the initial-margin health check at `end_flashloan`. Use [`simulateHealthCache`](/typescript-sdk/health-and-max-amounts) on the expected end state if you want to pre-check. * **Stale oracles.** `makeFlashLoanTx` does not crank oracles for you. If a bank the account holds has a stale Switchboard feed the end health check fails; the Layer-4 builders handle this by emitting crank transactions, and you can do the same — see [Transactions](/typescript-sdk/transactions). * **Account flag.** `wrappedAccount.isFlashLoanEnabled` reflects the on-chain `ACCOUNT_IN_FLASHLOAN` flag; it is only set between begin and end, so a `true` read outside a transaction means a previous flash loan was left in a bad state. * **Transaction size.** A v0 transaction is capped at 1232 bytes and 64 account locks; begin/end plus the health-check accounts eat into that. Keep LUTs comprehensive. If you are splicing a swap and want the exact remaining budget before quoting, `computeFlashLoanNonSwapBudget({ program, marginfiAccount, ixs, bankMap, addressLookupTableAccounts })` returns `{ sizeConstraint, maxSwapTotalAccounts }`, and `compileFlashloanPrecheck(...)` compiles the full message and reports `fullTxSize` / `overshoot` before you build. * **Program 0.1.10.** The `end_flashloan` account layout changed in program 0.1.10; `makeEndFlashLoanIx` (and therefore `makeFlashLoanTx`) builds the right layout for the live program, see [Program Upgrade 0.1.10](/typescript-sdk/program-upgrade). ## Related [#related] * [Looping](/typescript-sdk/looping) and [Collateral Swaps, Debt Swaps & Repay with Collateral](/typescript-sdk/position-swaps) — the SDK's built-in flash-loan flows * [Transactions](/typescript-sdk/transactions) — LUTs, oracle cranks, bundles, priority fees * [Architecture & API Layers](/typescript-sdk/architecture) — where `makeFlashLoanTx` and the `make*Ix` methods sit * [Flashloans guide](/guides/flashloans) — protocol rules and use cases * Examples: [16a-loop.ts](https://github.com/0dotxyz/p0-ts-sdk/tree/main/examples/16a-loop.ts) shows a flash-loan flow end to end --- # Getting Started Install the Project 0 TypeScript SDK, pick an environment, and run your first deposit and borrow. The **p0-ts-sdk** (`@0dotxyz/p0-ts-sdk`) is the official SDK for the marginfi protocol on Solana. This page gets you from an empty project to a funded account with a deposit and a borrow. Everything after that lives in the topic pages linked at the end. ## Installation [#installation] The minimum supported version is **2.8.0** (required for the [0.1.11 program upgrade](/typescript-sdk/program-upgrade-0-1-11) npm pnpm yarn bun ```bash npm install @0dotxyz/p0-ts-sdk@^2.8.0 ``` ```bash pnpm add @0dotxyz/p0-ts-sdk@^2.8.0 ``` ```bash yarn add @0dotxyz/p0-ts-sdk@^2.8.0 ``` ```bash bun add @0dotxyz/p0-ts-sdk@^2.8.0 ``` The SDK is built against these peer dependencies — match them in your project to avoid duplicate `PublicKey`/`Transaction` classes: | Package | Version | | ------------------- | -------- | | `@solana/web3.js` | `1.98.4` | | `@coral-xyz/anchor` | `0.30.1` | The package exposes four entry points: | Import path | What it contains | | --------------------------------- | ---------------------------------------------------------------------------------------------------------- | | `@0dotxyz/p0-ts-sdk` | Everything you normally need: `Project0Client`, `MarginfiAccountWrapper`, bank/price/health helpers, types | | `@0dotxyz/p0-ts-sdk/vendor` | Heavy oracle and venue integrations (Pyth, Switchboard, Kamino, Drift) kept out of the main bundle | | `@0dotxyz/p0-ts-sdk/jupiter` | Jupiter swap helpers | | `@0dotxyz/p0-ts-sdk/instructions` | Raw Anchor instruction builders — see [Architecture](/typescript-sdk/architecture) | ## Environments [#environments] `getConfig(environment, overrides?)` returns a `Project0Config` for one of the deployed groups: ```typescript import { getConfig } from "@0dotxyz/p0-ts-sdk"; import type { Environment, Project0Config } from "@0dotxyz/p0-ts-sdk"; // Environment = "production" | "staging" | "staging-mainnet-clone" | "staging-alt" const config: Project0Config = getConfig("production"); // { environment: "production", programId: PublicKey, groupPk: PublicKey } console.log(config.programId.toBase58(), config.groupPk.toBase58()); // Override the program or group (e.g. a local fork) while keeping the rest const custom = getConfig("staging", { groupPk: myGroupPk }); ``` | Environment | Cluster | Use it for | | ----------------------- | ------- | --------------------------------------------------------------------------- | | `production` | Mainnet | Real funds | | `staging` | Mainnet | Integration testing against the staging group (already runs program 0.1.10) | | `staging-mainnet-clone` | Mainnet | Staging group mirroring production bank configs | | `staging-alt` | Mainnet | Secondary staging group | `getConfig()` with no argument defaults to `"production"`. The program and group addresses are listed on [Program Addresses](/protocol-overview/program-addresses). The client never holds a signer. `Project0Client` is constructed with an empty Anchor wallet and only **reads** chain state and **builds** transactions. Signing and sending is always your job — with a wallet adapter in the browser or a `Keypair` on the server. ## Quickstart [#quickstart] This walks through the class-based API (`Project0Client` + `MarginfiAccountWrapper`). The lower, functional layers are covered in [Architecture](/typescript-sdk/architecture). ### Initialize the client [#initialize-the-client] The `Project0Client` is the entry point. It loads the group, every bank, oracle prices, mint data, lookup tables and integration metadata at initialization. ```typescript import { Connection } from "@solana/web3.js"; import { Project0Client, getConfig } from "@0dotxyz/p0-ts-sdk"; const connection = new Connection( "https://api.mainnet-beta.solana.com", "confirmed", ); const config = getConfig("production"); const client = await Project0Client.initialize(connection, config); console.log(`Loaded ${client.banks.length} banks`); ``` Initialization fetches all protocol data upfront. Reuse one client instance across your application instead of re-initializing per request. ### Create or load an account [#create-or-load-an-account] A marginfi account is a PDA derived from your wallet, an account index and an optional third-party id. Create one if you don't have it, otherwise load it: ```typescript // First time: create an account at index 0 const createTx = await client.createMarginfiAccountTx(wallet.publicKey, 0); const sig = await wallet.sendTransaction(createTx, connection); await connection.confirmTransaction(sig); // Afterwards: discover and load it const [accountAddress] = await client.getAccountAddresses(wallet.publicKey); const wrappedAccount = await client.fetchAccount(accountAddress); ``` `fetchAccount` also simulates the account's health cache so health-based numbers are fresh. Multiple accounts per wallet, PDA derivation, projection and account lifecycle are covered on [Accounts](/typescript-sdk/accounts). ### Find a bank [#find-a-bank] Banks are lending pools for a single mint. A mint can have several banks (a standard bank plus Kamino/Drift/Jupiter Lend wrappers), so filter by `AssetTag` to get the standard one: ```typescript import { PublicKey } from "@solana/web3.js"; import { AssetTag } from "@0dotxyz/p0-ts-sdk"; const USDC_MINT = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); const [usdcBank] = client.getBanksByMint(USDC_MINT, AssetTag.DEFAULT); if (!usdcBank) throw new Error("USDC bank not found"); ``` See [Banks & Prices](/typescript-sdk/banks-and-prices) for asset tags, bank metrics and oracle prices. ### Deposit [#deposit] `makeDepositTx(bankAddress, amount, opts?)` returns a single legacy `Transaction` (an `ExtendedTransaction`). Amounts are in UI units: ```typescript const depositTx = await wrappedAccount.makeDepositTx(usdcBank.address, "100"); const sig = await wallet.sendTransaction(depositTx, connection); await connection.confirmTransaction(sig); ``` ### Borrow [#borrow] Borrows (and withdrawals) are health-checked on-chain, which can require oracle cranks in separate transactions. `makeBorrowTx` therefore returns a `TransactionBuilderResult` — `{ transactions, actionTxIndex }` — and you send every transaction in order. Check the limit first with `computeMaxBorrowForBank`: ```typescript import { simulateBundle } from "@0dotxyz/p0-ts-sdk"; // Largest borrow the account can take from this bank right now (UI units) const maxBorrow = wrappedAccount.computeMaxBorrowForBank(usdcBank.address); console.log(`Max borrow: ${maxBorrow.toString()} USDC`); const borrowResult = await wrappedAccount.makeBorrowTx(usdcBank.address, "50"); console.log(`Transactions: ${borrowResult.transactions.length}`); console.log(`Borrow is at index: ${borrowResult.actionTxIndex}`); // Optional: simulate the whole set at once (needs an RPC that supports simulateBundle) const results = await simulateBundle( connection.rpcEndpoint, borrowResult.transactions, ); results.forEach((r, i) => { if (r.err) throw new Error(`tx ${i} failed: ${JSON.stringify(r.err)}`); }); // Sign and send in order for (const tx of borrowResult.transactions) { const sig = await wallet.sendTransaction(tx, connection); await connection.confirmTransaction(sig); } ``` Always send **all** transactions in `borrowResult.transactions`, in order. The extra ones crank stale oracles; skipping them makes the borrow fail its health check. Repay, withdraw, the per-operation options and the canonical signing snippets are on [Lending & Borrowing](/typescript-sdk/lending-and-borrowing). Multi-transaction handling, bundles and priority fees are on [Transactions](/typescript-sdk/transactions). ### Next steps [#next-steps] Create, derive, load and read accounts; close and transfer them. Find banks, asset tags, bank metrics, rate limiters and oracle prices. Deposit, withdraw, borrow and repay with all options. Health cache, free collateral, max borrow/withdraw/deposit, e-mode. Leverage loops with swap-provider config and bridged routes. ## TypeScript support [#typescript-support] The SDK is written in TypeScript and exports all of its types from the root entry point: ```typescript import type { MarginfiAccountType, BankType, BalanceType, OraclePrice, Project0Config, MarginRequirementType, } from "@0dotxyz/p0-ts-sdk"; ``` Enable strict mode in your `tsconfig.json` for the best experience: ```json { "compilerOptions": { "strict": true, "moduleResolution": "bundler", "target": "ES2022" } } ``` ## Related [#related] * [Overview](/typescript-sdk/overview) — package entry points, running the examples, getting help * [Accounts](/typescript-sdk/accounts) — everything about marginfi accounts * [Lending & Borrowing](/typescript-sdk/lending-and-borrowing) — the four core operations in depth * [Program Upgrade 0.1.11](/typescript-sdk/program-upgrade-0-1-11) — why `>= 2.8.0` is required * Examples: [`01-deposit.ts`](https://github.com/0dotxyz/p0-ts-sdk/tree/main/examples/01-deposit.ts), [`02-borrow.ts`](https://github.com/0dotxyz/p0-ts-sdk/tree/main/examples/02-borrow.ts) --- # Health, Max Amounts & E-mode Read account health from the simulated health cache, size actions with computeMax*ForBank, and inspect e-mode state. Every borrow and withdraw is gated by the on-chain risk engine, so before building one you usually want three numbers: how healthy the account is, how much it can move for a given bank, and whether e-mode changes the picture. All three are one call away on `MarginfiAccountWrapper`. This page assumes you have a `client` and `wrappedAccount` from [Getting Started](/typescript-sdk/getting-started). USD values are `BigNumber`s; amounts are `BigNumber`s in token UI units. For the protocol rules behind the numbers see [Risk Engine](/protocol-overview/risk-engine). ## The health cache [#the-health-cache] The program keeps a `healthCache` on every account: weighted asset and liability values at the three margin requirement levels, a timestamp, status flags and the prices it used. It is only refreshed when an instruction runs, so the stored copy goes stale between your actions. The SDK therefore **simulates** the cache for you: `client.fetchAccount(address)` runs a `pulse_health` simulation against current oracle prices and returns a wrapper whose `healthCache` reflects right now. Pass `skipHealthCache = true` to get the raw on-chain values instead. To refresh later (after prices moved, or after one of your transactions landed), re-simulate and rebuild the wrapper — accounts are immutable, `simulateHealthCache()` returns a new one: ```typescript import { MarginfiAccountWrapper } from "@0dotxyz/p0-ts-sdk"; const { account, error } = await wrappedAccount.simulateHealthCache(); if (error) { // HealthCacheSimulationError: the on-chain simulation failed and the SDK fell back // to a local computation. Values are still usable, just not program-verified. console.warn("Health simulation fell back:", error.message, error.mrgnErr, error.internalErr); } wrappedAccount = new MarginfiAccountWrapper(account, client); ``` `wrappedAccount.account.healthCache.simulationStatus` tells you where the numbers came from: `HealthCacheStatus.ONCHAIN` (simulation succeeded), `COMPUTED` (local fallback) or `UNSET` (nothing simulated yet — the cache-based getters below log a warning). If you work with a bare `MarginfiAccount` instead of the wrapper, the same logic is exported as `simulateAccountHealthCacheWithFallback({ program, banksMap, oraclePricesByBank, marginfiAccount, bankIntegrationMap, assetShareValueMultiplierByBank?, activeEmodeWeightsByBank? })` which returns `{ marginfiAccount, error? }`. `MarginfiAccount.simulateHealthCache(params)` is the same call minus `marginfiAccount`. ## Reading health [#reading-health] Once the cache is simulated, these getters are synchronous and cheap: ```typescript import { MarginRequirementType } from "@0dotxyz/p0-ts-sdk"; // Weighted USD values at each requirement level const init = wrappedAccount.computeHealthComponentsFromCache(MarginRequirementType.Initial); const maint = wrappedAccount.computeHealthComponentsFromCache(MarginRequirementType.Maintenance); const equity = wrappedAccount.computeHealthComponentsFromCache(MarginRequirementType.Equity); // Health factor: assets / liabilities (> 1 is healthy; < 1 at Maintenance means liquidatable) const maintHealthFactor = maint.liabilities.gt(0) ? maint.assets.div(maint.liabilities) : null; // Free collateral (Initial assets - Initial liabilities), clamped at 0 by default const freeCollateral = wrappedAccount.computeFreeCollateralFromCache(); const signedFreeCollateral = wrappedAccount.computeFreeCollateralFromCache({ clamped: false }); // Equity (unweighted assets - liabilities) and blended APY across positions const accountValue = wrappedAccount.computeAccountValue(); const netApy = wrappedAccount.computeNetApy(); // decimal, e.g. 0.0421 = 4.21% console.log(`Init ${init.assets.toFixed(2)} / ${init.liabilities.toFixed(2)}`); console.log(`Maint ${maint.assets.toFixed(2)} / ${maint.liabilities.toFixed(2)} (HF ${maintHealthFactor?.toFixed(3) ?? "n/a"})`); console.log(`Equity ${accountValue.toFixed(2)} USD, free collateral ${freeCollateral.toFixed(2)} USD, net APY ${(netApy * 100).toFixed(2)}%`); ``` | Requirement | Used for | Cache fields | | ------------- | ----------------------------------------------- | ------------------------------------------ | | `Initial` | Opening or growing positions (borrow, withdraw) | `assetValue`, `liabilityValue` | | `Maintenance` | Liquidation threshold | `assetValueMaint`, `liabilityValueMaint` | | `Equity` | Actual value, no risk weights | `assetValueEquity`, `liabilityValueEquity` | A borrow or withdraw is accepted when Initial assets stay at or above Initial liabilities afterwards; an account becomes liquidatable when Maintenance assets drop below Maintenance liabilities. `computeNetApy` weights each position's supply/borrow APY by its USD value and includes any active e-mode weights. `computeHealthComponentsFromCache` reads the cache exactly as the program would, including the program's conservative price bias. For a purely local calculation from balances and prices (no RPC), use `computeHealthComponentsFromBalances` and `computeFreeCollateralFromBalances` on the underlying `MarginfiAccount`. ## Max amounts [#max-amounts] Three helpers return the largest amount the account can move for a bank right now, as a `BigNumber` in **token UI units** (not USD). Since SDK 2.7.1 they are bank-aware: they combine the health-based limit with the bank's caps, available liquidity and the bank/group rate limiters, mirroring the checks the program runs. ```typescript computeMaxBorrowForBank(bankAddress: PublicKey, opts?: { volatilityFactor?: number; ignoreBankLimits?: boolean }): BigNumber computeMaxWithdrawForBank(bankAddress: PublicKey, opts?: { volatilityFactor?: number; ignoreBankLimits?: boolean }): BigNumber computeMaxDepositForBank(bankAddress: PublicKey, opts?: { walletBalance?: BigNumber | number }): BigNumber ``` ```typescript // Largest borrow the account can take from this bank right now const maxBorrow = wrappedAccount.computeMaxBorrowForBank(usdcBank.address, { volatilityFactor: 0.95, // optional haircut on free collateral to leave a buffer }); // Largest withdrawal that keeps the account healthy const maxWithdraw = wrappedAccount.computeMaxWithdrawForBank(solBank.address); // Remaining deposit cap, optionally min'd with the wallet balance const maxDeposit = wrappedAccount.computeMaxDepositForBank(usdcBank.address, { walletBalance: 1_000, }); ``` What each one computes: ``` maxBorrow = max(0, min(healthBased, remainingBorrowCap, projectedAvailableLiquidity, bankNetOutflowRateLimit, groupUsdRateLimit)) maxWithdraw = max(0, min(healthBased, projectedAvailableLiquidity, bankNetOutflowRateLimit, groupUsdRateLimit)) maxDeposit = min(remainingDepositCap, walletBalance?) ``` * **Health-based borrow** starts from free collateral (times `volatilityFactor`, default 1) and converts it to tokens at the bank's liability weight and highest-bias price. Every liability-denominated bound is divided by `(1 + protocolOriginationFee)` because the program books the origination fee as extra liability. If the account already holds this asset, the collateral it frees up is added. Isolated-tier rules are applied first: borrowing an isolated asset while holding other debt, or a new asset while holding isolated debt, returns `0`. * **Health-based withdraw** is the amount whose removal keeps Initial health at or above zero, again with `volatilityFactor` applied to free collateral. * **Caps** are strict (`limit - total - 1`) and buffered for interest; **liquidity** is projected through interest accrual; the **rate limiters** are the bank's net-outflow limiter (tokens) and the group's USD limiter converted at the unbiased realtime price. Deposits are not health-constrained, so `maxDeposit` is just cap and wallet. * The wrapper passes `client.group.rateLimiter` for you and applies any active e-mode weights (see below). The standalone utils (`computeMaxBorrowForBank` etc. exported from the SDK) take those as explicit params. Pass `{ ignoreBankLimits: true }` to `computeMaxBorrowForBank` / `computeMaxWithdrawForBank` to get the purely health-based number. Use it for liquidation and flash-loan flows, which skip the rate limiters on-chain, or when you want to show "your collateral allows X, the pool currently allows Y" separately: ```typescript const healthOnly = wrappedAccount.computeMaxBorrowForBank(bankAddress, { ignoreBankLimits: true }); const poolLimited = wrappedAccount.computeMaxBorrowForBank(bankAddress); if (poolLimited.lt(healthOnly)) { console.log("Borrow is capped by bank limits, not by your collateral"); } ``` The bank-side building blocks are exported if you want the individual terms: `computeBankBorrowCapRemaining`, `computeBankDepositCapRemaining`, `computeBankProjectedAvailableLiquidity`, `computeBankRateLimitRemaining(bank, nowSeconds?)` and `computeGroupRateLimitRemainingUsd(client.group.rateLimiter, nowSeconds?)` — see [Banks & Prices](/typescript-sdk/banks-and-prices) and [Caps & Rate Limits](/protocol-overview/caps-and-rate-limits). ## E-mode [#e-mode] E-mode raises the asset weights of a collateral bank when the account only borrows assets that are "paired" with it (for example SOL-family collateral against SOL debt). Pairs come from bank configuration; the wrapper derives the account's current state from `client.emodePairs` and its balances. ```typescript // Pairs currently active for this account (empty array = e-mode off) const activePairs = wrappedAccount.getActiveEmodePairs(); // EmodePair { collateralBanks, collateralBankTag, liabilityBank, liabilityBankTag, assetWeightInit, assetWeightMaint } // Effective (lowest) e-mode weights per collateral bank, keyed by bank address const weights = wrappedAccount.getActiveEmodeWeightsByBank(); // Map ``` These weights already flow into `simulateHealthCache`, `computeNetApy` and the max-amount helpers, so you rarely need them directly. What you do want before an action is to know whether it will **change** e-mode — that is what `getEmodeImpacts()` answers, for every bank the client knows: ```typescript getEmodeImpacts(): Record interface ActionEmodeImpact { borrowImpact?: EmodeImpact; // if you open a new borrow on this bank supplyImpact?: EmodeImpact; // if you deposit into this (collateral-eligible) bank repayAllImpact?: EmodeImpact; // if you fully repay this bank (only set for active liabilities) withdrawAllImpact?: EmodeImpact; // if you fully withdraw this bank (only set for active assets) } interface EmodeImpact { status: EmodeImpactStatus; resultingPairs: EmodePair[]; // pairs that would be active after the action activePair?: ActiveEmodePair; // merged view of those pairs } enum EmodeImpactStatus { ActivateEmode, ExtendEmode, IncreaseEmode, ReduceEmode, RemoveEmode, InactiveEmode } ``` | Status | Meaning | | --------------- | ---------------------------------------------------------------- | | `ActivateEmode` | E-mode is off today and this action turns it on | | `ExtendEmode` | E-mode stays on with the same weights (same tag, more pairs) | | `IncreaseEmode` | E-mode stays on and the minimum initial weight goes up (better) | | `ReduceEmode` | E-mode stays on but the minimum initial weight goes down (worse) | | `RemoveEmode` | E-mode is on today and this action turns it off | | `InactiveEmode` | E-mode is off before and after | A typical use is to list which borrows would activate or hurt e-mode before the user picks one: ```typescript import { EmodeImpactStatus, isStandardBorrowable } from "@0dotxyz/p0-ts-sdk"; const impacts = wrappedAccount.getEmodeImpacts(); for (const bank of client.banks.filter(isStandardBorrowable)) { const impact = impacts[bank.address.toBase58()]?.borrowImpact; if (!impact) continue; // already held as collateral, cannot borrow switch (impact.status) { case EmodeImpactStatus.ActivateEmode: case EmodeImpactStatus.IncreaseEmode: console.log(`${bank.tokenSymbol}: borrowing activates/improves e-mode (init weight ${impact.activePair?.assetWeightInit.toString()})`); break; case EmodeImpactStatus.ReduceEmode: case EmodeImpactStatus.RemoveEmode: console.log(`${bank.tokenSymbol}: borrowing reduces/removes e-mode`); break; default: break; // ExtendEmode / InactiveEmode: no change worth flagging } } ``` `computeMaxBorrowForBank` already uses `borrowImpact` internally: when the borrow would activate or change e-mode it recomputes free collateral from balances with the resulting weights instead of reading the cache, so the returned maximum reflects the post-action weights. For custom pair sets (for example a hypothetical configuration) the wrapper also exposes `computeActiveEmodePairs(emodePairs)` and `computeEmodeImpacts(emodePairs, banks)`. Protocol background: [E-mode](/protocol-overview/emode) and the [E-mode guide](/guides/emode). ## Related [#related] * [Deposit, Withdraw, Borrow & Repay](/typescript-sdk/lending-and-borrowing) — use the max amounts when building actions * [Banks & Prices](/typescript-sdk/banks-and-prices) — caps, liquidity and rate-limiter helpers per bank * [Risk Engine](/protocol-overview/risk-engine) and [E-mode](/protocol-overview/emode) — the protocol rules * [Liquidating accounts](/guides/liquidating-accounts) — where `ignoreBankLimits` and Maintenance health matter * Examples: [`06a-account-health-simulated.ts`](https://github.com/0dotxyz/p0-ts-sdk/tree/main/examples/06a-account-health-simulated.ts), [`06b-account-health-calculated.ts`](https://github.com/0dotxyz/p0-ts-sdk/tree/main/examples/06b-account-health-calculated.ts) --- # Integration Banks & Native Stake Deposit into and withdraw from Kamino, Drift and Jupiter Lend banks, and turn native stake accounts into LSTs you can deposit. Some Project 0 banks do not hold the token themselves: they route deposits into an external venue — Kamino, Drift or Jupiter Lend — and account for your position in that venue's receipt token. Native stake banks work the other way round: you convert a stake account into a single-validator LST first, then deposit the LST like any other asset. This page assumes you have a `client` and `wrappedAccount` from [Getting Started](/typescript-sdk/getting-started). ## What integration banks are [#what-integration-banks-are] A bank's `config.assetTag` tells you which venue backs it: `AssetTag.KAMINO` (3), `AssetTag.DRIFT` (4) or `AssetTag.JUPLEND` (6). They are separate banks from the `AssetTag.DEFAULT` bank of the same mint, so use the tag when you look them up: ```typescript import { AssetTag, USDC_MINT } from "@0dotxyz/p0-ts-sdk"; const [kaminoUsdc] = client.getBanksByMint(USDC_MINT, AssetTag.KAMINO); const [driftUsdc] = client.getBanksByMint(USDC_MINT, AssetTag.DRIFT); const [jupUsdc] = client.getBanksByMint(USDC_MINT, AssetTag.JUPLEND); ``` Each integration bank exposes the venue accounts it needs on the bank itself — `bank.kaminoIntegrationAccounts` (`kaminoReserve`, `kaminoObligation`), `bank.driftIntegrationAccounts` (`driftSpotMarket`, `driftUser`, `driftUserStats`), `bank.jupLendIntegrationAccounts` (`jupLendingState`, `jupFTokenVault`, `jupFTokenAta`). Positions in these banks are denominated in the venue's share token (cTokens, fTokens), and `client.assetShareValueMultiplierByBank` converts them back to the underlying; see [Banks & Prices](/typescript-sdk/banks-and-prices) for tags and multipliers, and [Cross-venue Integrations](/protocol-overview/cross-venue-integrations) for the protocol mechanics and comingling rules. ## Reading integration state [#reading-integration-state] `Project0Client.initialize()` fetches the live venue state for every integration bank into `client.bankIntegrationMap`, a plain object keyed by bank address (`BankIntegrationMetadataMap`): ```typescript const meta = client.bankIntegrationMap[kaminoUsdc.address.toBase58()]; meta?.kaminoStates; // { reserveState: KaminoReserve, obligationState: KaminoObligation, farmState? } meta?.driftStates; // { spotMarketState: DriftSpotMarket, userState: DriftUser, userRewards: DriftRewards[], userStatsState? } meta?.jupLendStates; // { jupLendingState, jupTokenReserveState, jupRewardsRateModel, jupRateModel, fTokenTotalSupply } ``` The deposit and withdraw methods below take these objects as arguments, so you read them from the map and pass them through. The map is a snapshot from when the client was initialised; to refresh it without rebuilding the client, call `fetchBankIntegrationMetadata({ connection, banks: client.banks, integrations?: ["kamino" | "drift" | "juplend"] })` and merge the result with `Object.assign(client.bankIntegrationMap, fresh)`. ## Deposit and withdraw [#deposit-and-withdraw] ```typescript const bankKey = kaminoUsdc.address.toBase58(); const reserve = client.bankIntegrationMap[bankKey]!.kaminoStates!.reserveState; // makeKaminoDepositTx(bankAddress, amount, reserve, opts?) -> ExtendedV0Transaction const depositTx = await wrappedAccount.makeKaminoDepositTx(kaminoUsdc.address, "100", reserve); // makeKaminoWithdrawTx(bankAddress, amount, reserve, withdrawAll?, opts?) -> TransactionBuilderResult const withdrawResult = await wrappedAccount.makeKaminoWithdrawTx( kaminoUsdc.address, "50", reserve, false ); ``` Amounts are UI units of the underlying token; the SDK divides by the bank's share multiplier to get the cToken amount the program expects. The Layer 3 `makeKaminoWithdrawTx` additionally accepts a `TypedAmount` (`{ value, type: "cToken" }`) when you already hold a cToken quantity. `opts` are the usual `MakeDepositIxOpts` / `MakeWithdrawIxOpts` (`wrapAndUnwrapSol`, `createAtas`, ...). ```typescript const bankKey = driftUsdc.address.toBase58(); const { spotMarketState, userRewards } = client.bankIntegrationMap[bankKey]!.driftStates!; // makeDriftDepositTx(bankAddress, amount, spotMarket, opts?) -> ExtendedV0Transaction const depositTx = await wrappedAccount.makeDriftDepositTx(driftUsdc.address, "100", spotMarketState); // makeDriftWithdrawTx(bankAddress, amount, spotMarket, userRewards, withdrawAll?, opts?) -> TransactionBuilderResult const withdrawResult = await wrappedAccount.makeDriftWithdrawTx( driftUsdc.address, "50", spotMarketState, userRewards, false ); ``` The wrapper reads `marketIndex` and `oracle` from the spot market for the deposit; the withdraw needs the full `DriftSpotMarket` plus the bank's `DriftRewards[]` so it can pass the reward oracles (up to two) the Drift withdraw requires. There is no wrapper method for Jupiter Lend yet, so call the Layer 3 builders directly. Their params mirror the other integration builders (`MakeJuplendDepositTxParams` / `MakeJuplendWithdrawTxParams`): ```typescript import { makeJuplendDepositTx, makeJuplendWithdrawTx } from "@0dotxyz/p0-ts-sdk"; const bankKey = jupUsdc.address.toBase58(); const { tokenProgram } = client.mintDataByBank.get(bankKey)!; const { jupLendingState } = client.bankIntegrationMap[bankKey]!.jupLendStates!; // -> ExtendedV0Transaction const depositTx = await makeJuplendDepositTx({ program: client.program, bank: jupUsdc, tokenProgram, amount: "100", accountAddress: wrappedAccount.address, authority: wrappedAccount.authority, group: client.group.address, luts: client.addressLookupTables, connection, }); // -> TransactionBuilderResult const withdrawResult = await makeJuplendWithdrawTx({ program: client.program, bank: jupUsdc, bankMap: client.bankMap, tokenProgram, amount: "50", marginfiAccount: wrappedAccount.getUnderlyingAccount(), authority: wrappedAccount.authority, jupLendingState, bankMetadataMap: client.bankIntegrationMap, withdrawAll: false, oraclePrices: client.oraclePriceByBank, assetShareValueMultiplierByBank: client.assetShareValueMultiplierByBank, luts: client.addressLookupTables, connection, }); ``` Layer 2 builders `makeJuplendDepositIx` / `makeJuplendWithdrawIx` exist as well if you compose your own transaction. Deposits return a single `ExtendedV0Transaction`; withdrawals return `{ transactions, actionTxIndex }` because, like any withdraw, they may need an oracle crank first. Send them exactly as you would a normal [borrow or withdraw](/typescript-sdk/lending-and-borrowing#sending). You never add venue refresh instructions yourself. The builders prepend a Kamino reserve refresh, Drift market update or Jupiter Lend rate update to the action transaction whenever the account touches an integration bank — including normal borrows and withdraws on other banks while you hold an integration position. Because that state goes stale within a slot, flash-loan flows that include one set `mustBeAtomicBundle: true`. See [Transactions](/typescript-sdk/transactions#integration-refreshes). Other operations — borrowing against an integration position, looping into a Kamino bank, swapping collateral — use the regular methods; the only integration-specific entry points are the deposit and withdraw pairs above. ## Native stake [#native-stake] `AssetTag.STAKED` banks accept a single-validator LST minted by the SPL single-pool program. Moving a native stake account into Project 0 is therefore two steps: mint the LST from the stake account, then deposit the LST. The native-stake helpers are standalone functions (not wrapper methods) exported from the package root, in `src/services/native-stake/`. ### Find the bank and validator [#find-the-bank-and-validator] The SDK ships a hardcoded map of staked banks so you can go from a bank to its validator vote account and LST mint without extra RPC calls: ```typescript import { getStakedBankMetadataMap, getValidatorVoteAccountByBank } from "@0dotxyz/p0-ts-sdk"; import { PublicKey } from "@solana/web3.js"; const stakedMeta = getStakedBankMetadataMap(); // Map const entry = stakedMeta.get(stakedBankAddress.toBase58())!; // entry: { bankAddress, validatorVoteAccount, tokenAddress (LST mint), tokenName, tokenSymbol } const validator = new PublicKey(getValidatorVoteAccountByBank()[stakedBankAddress.toBase58()]!); ``` Your stake account must be delegated to that validator; stake on a different validator cannot be minted into this bank's LST. ### Mint the LST from a stake account [#mint-the-lst-from-a-stake-account] `makeMintStakedLstTx` creates the LST ATA if needed, splits the stake account when `amount` is less than its delegated stake, re-authorises staker and withdrawer to the pool, and deposits the stake into the single-validator pool. You receive LST in your wallet. ```typescript import { makeMintStakedLstTx } from "@0dotxyz/p0-ts-sdk"; const mintTx = await makeMintStakedLstTx({ amount: "1.5", // SOL (UI units); pass a value >= the delegated stake to convert all of it authority: wrappedAccount.authority, stakeAccountPk: new PublicKey(""), validator, connection, luts: client.addressLookupTables, }); // ExtendedV0Transaction, type DEPOSIT_STAKE; mintTx.signers holds the split keypair for partial amounts ``` ### Deposit the LST [#deposit-the-lst] The LST is an ordinary SPL token, so the deposit is a normal `makeDepositTx` into the staked bank: ```typescript const depositTx = await wrappedAccount.makeDepositTx(stakedBankAddress, "1.5"); ``` ### Redeem and merge [#redeem-and-merge] `makeRedeemStakedLstTx` does the reverse — creates a fresh stake account, burns LST and withdraws the stake from the pool into it (the new keypair is in `tx.signers`) — and `makeMergeStakeAccountsTx` merges two of your stake accounts (same authority and validator) so you can mint from one: ```typescript import { makeRedeemStakedLstTx, makeMergeStakeAccountsTx } from "@0dotxyz/p0-ts-sdk"; const redeemTx = await makeRedeemStakedLstTx({ amount: "1.5", // LST amount (UI units) authority: wrappedAccount.authority, validator, connection, luts: client.addressLookupTables, }); const mergeTx = await makeMergeStakeAccountsTx({ authority: wrappedAccount.authority, sourceStakeAccount: new PublicKey(""), destinationStakeAccount: new PublicKey(""), connection, luts: client.addressLookupTables, }); ``` All three return an `ExtendedV0Transaction` (types `DEPOSIT_STAKE`, `WITHDRAW_STAKE`, `MERGE_STAKE_ACCOUNTS`) and accept an optional `blockhash`. Withdrawing from the staked bank is a normal `makeWithdrawTx`; redeem afterwards if you want the stake account back. Instruction-level variants `makeMintStakedLstIx` / `makeRedeemStakedLstIx` are exported too. For the user-facing flow and the list of supported validators see the [Staking guide](/guides/staking). The `examples/12`–`14` scripts in the SDK repository call these as wrapper methods (`wrappedAccount.makeMintStakedLstTx(...)`); in 2.7.x the exported API is the standalone functions shown above. ## Gamma LP vaults [#gamma-lp-vaults] The root export also contains Layer 3 builders for Gamma LP vaults (`src/services/vaults/`): `makeVaultDepositTx({ user, lpVault, connection, amount, tokenProgram?, luts?, blockhash? })` (amount in raw base units of the vault asset), `makeVaultDepositWithSwapTx` (swap `inputMint` into the vault asset, then deposit), `makeVaultWithdrawTx({ ..., sharesAmount })`, `makeVaultCompleteWithdrawalTx`, their `*Ix` counterparts, and the readers `fetchGammaLpVault`, `fetchGammaWithdrawReceipt`, `resolveVaultTokenProgram`. These act on the vault program directly rather than on a marginfi account. ## Related [#related] * [Banks & Prices](/typescript-sdk/banks-and-prices) — asset tags, comingling rules, share multipliers * [Transactions](/typescript-sdk/transactions) — integration refresh instructions and `mustBeAtomicBundle` * [Cross-venue Integrations](/protocol-overview/cross-venue-integrations) — how integration banks work on-chain * [Staking guide](/guides/staking) — native stake in the app * Examples: [`12-mint-staked-lst.ts`](https://github.com/0dotxyz/p0-ts-sdk/tree/main/examples/12-mint-staked-lst.ts), [`13-redeem-staked-lst.ts`](https://github.com/0dotxyz/p0-ts-sdk/tree/main/examples/13-redeem-staked-lst.ts), [`14-merge-stake-accounts.ts`](https://github.com/0dotxyz/p0-ts-sdk/tree/main/examples/14-merge-stake-accounts.ts) --- # Deposit, Withdraw, Borrow & Repay Build and send the four core account operations with the MarginfiAccountWrapper. The four core operations — deposit, withdraw, borrow and repay — are one-line calls on `MarginfiAccountWrapper`. The wrapper looks up the bank, the mint's token program, oracle prices and lookup tables from the client, builds the instructions, and hands you back transactions ready to sign. This page assumes you have a `client` and `wrappedAccount` from [Getting Started](/typescript-sdk/getting-started). Amounts are always in **token UI units** (`Amount = BigNumber | number | string`, e.g. `"100"` for 100 USDC), never native lamports. ## Return shapes [#return-shapes] Deposits and repays never need an oracle crank, so they come back as a single legacy transaction. Borrows and withdrawals run a health check on-chain, which may require cranking Switchboard oracles first, so they come back as a list. | Method | Returns | Transaction type | | ---------------- | -------------------------------------------------------------- | --------------------------------------------------- | | `makeDepositTx` | `ExtendedTransaction` | legacy `Transaction` (`feePayer` set, no blockhash) | | `makeRepayTx` | `ExtendedTransaction` | legacy `Transaction` (`feePayer` set, no blockhash) | | `makeBorrowTx` | `TransactionBuilderResult` = `{ transactions, actionTxIndex }` | `ExtendedV0Transaction[]` (blockhash already set) | | `makeWithdrawTx` | `TransactionBuilderResult` = `{ transactions, actionTxIndex }` | `ExtendedV0Transaction[]` (blockhash already set) | `ExtendedTransaction` / `ExtendedV0Transaction` are plain web3.js transactions with extra metadata: `type` (`TransactionType.DEPOSIT`, `BORROW`, `CRANK`, ...), optional `signers` (extra keypairs the SDK generated, e.g. temporary wSOL accounts), `addressLookupTables` and `unitsConsumed`. `actionTxIndex` points at the transaction that holds your actual action; anything before it is an oracle crank. See [Transactions](/typescript-sdk/transactions) for the full result-type reference. Borrow and withdraw may return **more than one** transaction. Always iterate over `result.transactions` and send them in order — never just the last one. ## Deposit [#deposit] ```typescript makeDepositTx(bankAddress: PublicKey, amount: Amount, opts?: MakeDepositIxOpts): Promise ``` `MakeDepositIxOpts`: ```typescript import { AssetTag } from "@0dotxyz/p0-ts-sdk"; import { PublicKey } from "@solana/web3.js"; const USDC_MINT = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); const usdcBank = client.getBanksByMint(USDC_MINT, AssetTag.DEFAULT)[0]; // Deposit 100 USDC const depositTx = await wrappedAccount.makeDepositTx(usdcBank.address, "100"); // Legacy transaction: set a blockhash before signing depositTx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash; depositTx.feePayer = wallet.publicKey; // Optional: simulate first const sim = await connection.simulateTransaction(depositTx); console.log(`Compute units: ${sim.value.unitsConsumed}`); ``` `computeMaxDepositForBank` tells you how much the bank still accepts (its deposit cap, optionally min'd with your wallet balance) — see [Max amounts](/typescript-sdk/health-and-max-amounts#max-amounts). ## Withdraw [#withdraw] ```typescript makeWithdrawTx(bankAddress: PublicKey, amount: Amount, withdrawAll?: boolean, opts?: MakeWithdrawIxOpts): Promise ``` Pass `withdrawAll = true` to close the position entirely; the SDK then excludes the bank from the health check (its balance goes to zero) and `amount` is ignored by the program. `MakeWithdrawIxOpts`: ```typescript // Largest withdrawal that keeps the account healthy (token UI units) const maxWithdraw = wrappedAccount.computeMaxWithdrawForBank(solBank.address); const withdrawResult = await wrappedAccount.makeWithdrawTx( solBank.address, "0.5", // Withdraw 0.5 SOL false // withdrawAll ); console.log(`Transactions: ${withdrawResult.transactions.length}`); ``` If the account has no liabilities the SDK skips the oracle crank entirely and you get exactly one transaction. ## Borrow [#borrow] ```typescript makeBorrowTx(bankAddress: PublicKey, amount: Amount, opts?: MakeBorrowIxOpts): Promise ``` `MakeBorrowIxOpts`: ```typescript // Max borrow in token UI units (health, caps, liquidity and rate limiters) const maxBorrow = wrappedAccount.computeMaxBorrowForBank(usdcBank.address); console.log(`Max borrow: ${maxBorrow.toString()} USDC`); const borrowResult = await wrappedAccount.makeBorrowTx( usdcBank.address, "100" // Borrow 100 USDC ); console.log(`Transactions in bundle: ${borrowResult.transactions.length}`); console.log(`Main action at index: ${borrowResult.actionTxIndex}`); ``` Check `computeMaxBorrowForBank` before building — the program rejects a borrow that breaches initial health, a bank cap or a rate limiter, and the health-based number already accounts for the bank's origination fee. Details on [Health, Max Amounts & E-mode](/typescript-sdk/health-and-max-amounts#max-amounts). ## Repay [#repay] ```typescript makeRepayTx(bankAddress: PublicKey, amount: Amount, repayAll?: boolean, opts?: MakeRepayIxOpts): Promise ``` `repayAll = true` repays the whole liability including interest accrued up to execution. Still pass a realistic `amount`: the program settles the full debt regardless, but for the SOL bank the SDK uses `amount` to decide how much native SOL to wrap, and your token balance must cover the final figure. `MakeRepayIxOpts` is identical to `MakeDepositIxOpts` (`wrapAndUnwrapSol`, `wSolBalanceUi`, `overrideInferAccounts`). ```typescript // Repay 50 USDC const repayTx = await wrappedAccount.makeRepayTx(usdcBank.address, "50"); // ...or close the debt entirely (amount is an estimate; repayAll settles the exact figure) const repayAllTx = await wrappedAccount.makeRepayTx(usdcBank.address, "100.5", true); repayTx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash; repayTx.feePayer = wallet.publicKey; ``` To repay debt with collateral you already hold in the account (no wallet funds), use `makeRepayWithCollatTx` — see [Position Swaps](/typescript-sdk/position-swaps). ## Sending [#sending] Whatever the operation, the same loop works: normalise to an array, sign each transaction (plus any `signers` the SDK attached), send in order, and wait for confirmation before sending the next one. `wallet.sendTransaction` from `@solana/wallet-adapter-react` accepts both legacy and versioned transactions. Legacy transactions (deposit/repay) need a blockhash; versioned ones (borrow/withdraw) already have one. ```typescript import { VersionedTransaction } from "@solana/web3.js"; import type { SolanaTransaction } from "@0dotxyz/p0-ts-sdk"; async function sendAll(txs: SolanaTransaction[]) { const signatures: string[] = []; for (const tx of txs) { if (!(tx instanceof VersionedTransaction)) { tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash; tx.feePayer = wallet.publicKey; } const sig = await wallet.sendTransaction(tx, connection, { signers: tx.signers, // extra keypairs generated by the SDK, if any }); await connection.confirmTransaction(sig, "confirmed"); signatures.push(sig); } return signatures; } await sendAll([depositTx]); await sendAll(borrowResult.transactions); ``` ```typescript import { Keypair, VersionedTransaction } from "@solana/web3.js"; import type { SolanaTransaction } from "@0dotxyz/p0-ts-sdk"; async function sendAll(txs: SolanaTransaction[], signer: Keypair) { const signatures: string[] = []; for (const tx of txs) { const extraSigners = tx.signers ?? []; let raw: Buffer | Uint8Array; if (tx instanceof VersionedTransaction) { tx.sign([signer, ...extraSigners]); raw = tx.serialize(); } else { tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash; tx.feePayer = signer.publicKey; tx.sign(signer, ...extraSigners); raw = tx.serialize(); } const sig = await connection.sendRawTransaction(raw, { skipPreflight: false }); await connection.confirmTransaction(sig, "confirmed"); signatures.push(sig); } return signatures; } await sendAll([depositTx], wallet); await sendAll(borrowResult.transactions, wallet); ``` Before sending a multi-transaction result you can simulate the whole list in one call with `simulateBundle(connection.rpcEndpoint, result.transactions)`. Ordering guarantees, Jito bundles, priority fees and lookup tables are covered on [Transactions](/typescript-sdk/transactions). ## FAQ [#faq] The program re-prices every position during the health check and rejects stale Switchboard feeds. When one of your active banks uses a pull-style Switchboard oracle that is stale, the SDK prepends a `TransactionType.CRANK` transaction that updates those feeds, then puts the borrow in the next one (`actionTxIndex`). Send both, in order. Withdrawals only crank when the account has liabilities. Details under "Oracle cranking" on [Transactions](/typescript-sdk/transactions). The SOL bank holds wSOL. With the default `wrapAndUnwrapSol: true`, deposit and repay prepend instructions that create a wSOL account and wrap `amount - wSolBalanceUi` native SOL; borrow and withdraw append an unwrap so you receive native SOL. Set `wrapAndUnwrapSol: false` if you already hold wSOL and want to move it as-is, and pass `wSolBalanceUi` when you hold some wSOL but not enough. `makeRepayTx` always pulls tokens from your wallet. To repay using a deposit in the same account the SDK builds a flash loan that withdraws the collateral, swaps it and repays in one atomic transaction: `makeRepayWithCollatTx`. See [Position Swaps](/typescript-sdk/position-swaps). Common program errors are `RiskEngineInitRejected` (6009, health too low or stale oracle), `BankReduceOnly` (6017) and the rate-limit family (6115–6118). See [Error Handling](/typescript-sdk/errors) for how to recognise them and [Caps & Rate Limits](/protocol-overview/caps-and-rate-limits) for the protocol side. ## Related [#related] * [Health, Max Amounts & E-mode](/typescript-sdk/health-and-max-amounts) — `computeMax*ForBank` and what limits them * [Transactions](/typescript-sdk/transactions) — result types, oracle cranking, bundles, priority fees * [Position Swaps](/typescript-sdk/position-swaps) — repay with collateral, collateral and debt swaps * [Lending and Borrowing](/protocol-overview/lending-and-borrowing) — protocol mechanics * Examples: [`01-deposit.ts`](https://github.com/0dotxyz/p0-ts-sdk/tree/main/examples/01-deposit.ts), [`02-borrow.ts`](https://github.com/0dotxyz/p0-ts-sdk/tree/main/examples/02-borrow.ts), [`03-withdraw.ts`](https://github.com/0dotxyz/p0-ts-sdk/tree/main/examples/03-withdraw.ts), [`04-repay.ts`](https://github.com/0dotxyz/p0-ts-sdk/tree/main/examples/04-repay.ts) --- # Looping Build a leveraged position in one flash-loan transaction with makeLoopTx and makeBridgedLoopTx, configure the swap engine, and handle routing failures. A loop borrows, swaps the borrowed token into the collateral token, and deposits it again, all inside one flash loan. The result is a leveraged position opened in a single transaction instead of repeated deposit/borrow round-trips. This page assumes you have a `client` and `wrappedAccount` from [Getting Started](/typescript-sdk/getting-started); for the product-level explanation see the [Looping & Strategies guide](/guides/looping-and-strategies). ## What a loop is [#what-a-loop-is] The SDK builds one flash-loan transaction in the shape `begin_flashloan → borrow X → swap X to the deposit asset → deposit → end_flashloan`. Health is only checked at `end_flashloan`, so the intermediate borrow never has to be collateralized on its own. `depositOpts.loopMode` decides where the principal comes from: | `loopMode` | What happens | | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `"DEPOSIT"` | `inputDepositAmount` is pulled from the wallet and deposited as new principal, then the borrow→swap→deposit leg adds leverage on top. | | `"BORROW"` | No new principal is deposited. Only the borrow→swap→deposit leg runs, levering up collateral the account already holds. Pass `inputDepositAmount: 0` and size `borrowAmount` from the existing position. | Two builders exist: * `wrappedAccount.makeLoopTx(...)` — the **direct** loop. One flash-loan transaction with a single borrow→deposit swap route. If that route does not fit one transaction (bytes or account locks) or cannot be quoted, the build throws. * `wrappedAccount.makeBridgedLoopTx(...)` — the same call with a transparent **bridged** fallback. It tries the direct loop first; if that fails for a routing reason it loops against a value-equivalent borrow of a bridge token (USDC, wSOL, USDT by default), then debt-swaps that bridge debt into the requested borrow asset, as one atomic Jito bundle. ## Building a loop [#building-a-loop] ### Pick the banks [#pick-the-banks] Only standard, operational banks accept deposits and borrows in a loop. Use the bank predicates rather than filtering on asset tags yourself, and read each bank's token program through `getMintDataFromBank`: ```typescript import { isStandardDepositable, isStandardBorrowable } from "@0dotxyz/p0-ts-sdk"; import { PublicKey } from "@solana/web3.js"; const SOL_MINT = new PublicKey("So11111111111111111111111111111111111111112"); const USDC_MINT = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); const depositBank = client.banks.find( (bank) => bank.mint.equals(SOL_MINT) && isStandardDepositable(bank) ); const borrowBank = client.banks.find( (bank) => bank.mint.equals(USDC_MINT) && isStandardBorrowable(bank) ); if (!depositBank || !borrowBank) throw new Error("No loopable bank pair found"); const depositMintData = await wrappedAccount.getMintDataFromBank(depositBank); const borrowMintData = await wrappedAccount.getMintDataFromBank(borrowBank); ``` `isStandardDepositable` excludes reduce-only banks and Kamino/Drift/JupLend wrapper banks (depositing into those reverts). `isStandardBorrowable` keeps only standard banks with a non-zero borrow limit. See [Banks & Prices](/typescript-sdk/banks-and-prices) for the full set of predicates. ### Size the position [#size-the-position] Leverage `L` on principal `P` means borrowing `(L - 1) × P` worth of the borrow asset. Price both sides in USD — the examples use the client's realtime oracle prices; the app seeds this with a tradeable market price and falls back to the oracle: ```typescript const DEPOSIT_AMOUNT = 0.5; // principal, UI units const LEVERAGE = 2; const priceOf = (bankAddress: PublicKey): number => { const price = client.oraclePriceByBank .get(bankAddress.toBase58()) ?.priceRealtime.price.toNumber(); if (!price || price <= 0) throw new Error(`Missing oracle price for ${bankAddress.toBase58()}`); return price; }; const depositPrice = priceOf(depositBank.address); const borrowPrice = priceOf(borrowBank.address); const borrowAmount = (DEPOSIT_AMOUNT * (LEVERAGE - 1) * depositPrice) / borrowPrice; ``` To check how far you can go, `computeMaxLeverage(depositBank, borrowBank)` returns `{ maxLeverage, ltv }` from the banks' initial weights, and `computeLoopingParams(principal, targetLeverage, depositBank, borrowBank, depositPriceUsd, borrowPriceUsd)` returns `{ totalDepositAmount, totalBorrowAmount }` with the target leverage clamped to the valid range: ```typescript import { computeMaxLeverage, computeLoopingParams } from "@0dotxyz/p0-ts-sdk"; const { maxLeverage } = computeMaxLeverage(depositBank, borrowBank); const { totalBorrowAmount } = computeLoopingParams( DEPOSIT_AMOUNT, Math.min(LEVERAGE, maxLeverage), depositBank, borrowBank, depositPrice, borrowPrice ); // totalBorrowAmount is a BigNumber equal to the borrowAmount formula above, rounded to mint decimals ``` Both ignore e-mode weights unless you pass `opts.assetWeightInit` / `opts.liabilityWeightInit`; see [Health, Max Amounts & E-mode](/typescript-sdk/health-and-max-amounts). ### Configure the swap [#configure-the-swap] The swap leg is routed by the SDK's swap engine, driven by a `SwapProviderConfig` passed as `swapOpts.swapConfig`. `provider` plus `fallbackProviders` are queried as co-equal candidates and the engine picks the best route that fits the flash-loan transaction. Only providers you give credentials for are reachable. Jupiter only (the public endpoint is rate-limited; set `apiKey` for production): ```typescript import { SwapProvider, type SwapProviderConfig } from "@0dotxyz/p0-ts-sdk"; const swapConfig: SwapProviderConfig = { provider: SwapProvider.JUPITER, slippageMode: "DYNAMIC", slippageBps: 50, platformFeeBps: 0, directRoutesOnly: false, apiConfig: { basePath: "https://api.jup.ag/swap/v1", apiKey: process.env.JUPITER_API_KEY, }, }; ``` Titan primary with a Jupiter fallback (the Project 0 app default): ```typescript const swapConfig: SwapProviderConfig = { provider: SwapProvider.TITAN, slippageMode: "DYNAMIC", slippageBps: 50, platformFeeBps: 0, apiConfig: { basePath: "https:///api/v1", wsUrl: "wss:///api/v1/ws", apiKey: process.env.TITAN_API_KEY, }, fallbackProviders: [ { provider: SwapProvider.JUPITER, apiConfig: { basePath: "https://api.jup.ag/swap/v1", apiKey: process.env.JUPITER_API_KEY }, }, ], }; ``` This `swapConfig` shape is shared by every swap-backed builder ([collateral/debt swaps, repay with collateral](/typescript-sdk/position-swaps)); this page is its home. ### Build the transaction [#build-the-transaction] Both builders take the same `depositOpts` / `borrowOpts` / `swapOpts`. The one difference to watch: `makeLoopTx` requires you to pass `assetShareValueMultiplierByBank` (use `client.assetShareValueMultiplierByBank`), while `makeBridgedLoopTx` injects it for you and additionally accepts `bridgeOpts`. ```typescript const result = await wrappedAccount.makeLoopTx({ connection, depositOpts: { inputDepositAmount: DEPOSIT_AMOUNT, depositBank, tokenProgram: depositMintData.tokenProgram, loopMode: "DEPOSIT", // "BORROW" levers an existing position without new principal marketPrice: depositPrice, }, borrowOpts: { borrowAmount, borrowBank, tokenProgram: borrowMintData.tokenProgram, marketPrice: borrowPrice, }, swapOpts: { swapConfig }, assetShareValueMultiplierByBank: client.assetShareValueMultiplierByBank, }); ``` Returns `{ transactions, actionTxIndex, quoteResponse, mustBeAtomicBundle }`. ```typescript const result = await wrappedAccount.makeBridgedLoopTx({ connection, depositOpts: { inputDepositAmount: DEPOSIT_AMOUNT, depositBank, tokenProgram: depositMintData.tokenProgram, loopMode: "DEPOSIT", marketPrice: depositPrice, }, borrowOpts: { borrowAmount, borrowBank, tokenProgram: borrowMintData.tokenProgram, marketPrice: borrowPrice, }, swapOpts: { swapConfig }, // Optional. Defaults to USDC → wSOL → USDT, highest priority first. bridgeOpts: { bridgeCandidateMints: [USDC_MINT, SOL_MINT] }, }); ``` Returns a `BridgedTxResult`: `{ transactions, actionTxIndex, quoteResponse, mustBeAtomicBundle, bridgeMint? }`. `bridgeOpts` is `{ bridgeCandidateMints?, tokenProgramByMint?, maxBundleTxs?, abortSignal? }` — source and destination mints are always skipped as candidates, and `tokenProgramByMint` lets you skip the per-mint owner lookup. Optional params shared by both: `additionalIxs` (spliced into the flash loan), `crossbarUrl` (Switchboard crossbar override for oracle cranks), `overrideInferAccounts` (`{ group?, authority? }`), and `swapEngineRunner` (see below). Lookup tables come from `client.addressLookupTables` automatically. ### Inspect the result [#inspect-the-result] | Field | Meaning | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `transactions` | Ordered `ExtendedV0Transaction[]`. Setup transactions (oracle cranks, integration refreshes) come before `actionTxIndex`; the flash loan is at `actionTxIndex`. | | `actionTxIndex` | Index of the transaction that completes the loop. For a bridged bundle it is the last leg. | | `quoteResponse` | The swap engine's `SwapQuoteResult` (`inAmount`, `outAmount`, `otherAmountThreshold`, `slippageBps`, `priceImpactPct?`, `provider?`, ...). `inAmount` is the borrow in native units, `outAmount` the extra collateral. Bridged results carry a merged quote across both legs. `undefined` when no swap was needed (same mint). | | `mustBeAtomicBundle` | `true` means the transactions must land as one atomic Jito bundle (bridged legs, integration refreshes); `false` means sequential sends are safe. | | `bridgeMint` | Bridged builder only. Set to the bridge token's mint when the fallback path was used, `undefined` on a direct build. | ```typescript if (result.quoteResponse) { const borrowedUi = Number(result.quoteResponse.inAmount) / 10 ** borrowBank.mintDecimals; const depositedUi = Number(result.quoteResponse.outAmount) / 10 ** depositBank.mintDecimals; console.log(`borrow ~${borrowedUi} ${borrowBank.tokenSymbol} → +${depositedUi} ${depositBank.tokenSymbol}`); console.log(`price impact: ${result.quoteResponse.priceImpactPct ?? "n/a"}%`); } ``` ### Simulate and send [#simulate-and-send] ```typescript import { simulateBundle } from "@0dotxyz/p0-ts-sdk"; const sims = await simulateBundle(connection.rpcEndpoint, result.transactions); sims.forEach((sim, i) => { if (sim.err) throw new Error(`tx ${i} failed: ${JSON.stringify(sim.err)}`); }); if (result.mustBeAtomicBundle) { // sign every tx and submit them together as one Jito bundle } else { for (const tx of result.transactions) { const sig = await wallet.sendTransaction(tx, connection); await connection.confirmTransaction(sig); } } ``` Bundle submission, priority fees and the other send-side details live on [Transactions](/typescript-sdk/transactions). ## Controlling the route [#controlling-the-route] Integrators that must review or whitelist the exact swap they sign have two supported paths. ### `swapEngineRunner` (recommended) [#swapenginerunner-recommended] `swapEngineRunner` replaces the in-process `runSwapEngine`. It receives a `SwapEngineRequest` (mints, exact native input amount, remaining byte and account budget, the providers to query) and returns a `SwapEngineResult` (`swapInstructions`, `setupInstructions`, `swapLuts`, `quoteResponse`, `outputAmountNative`, `provider`). Wrap the default runner to inspect or veto the route before it is spliced into the flash loan: ```typescript import { runSwapEngine } from "@0dotxyz/p0-ts-sdk"; const reviewingRunner: typeof runSwapEngine = async (req) => { const route = await runSwapEngine(req); const programs = new Set(route.swapInstructions.map((ix) => ix.programId.toBase58())); for (const program of programs) { if (!ALLOWED_PROGRAMS.has(program)) throw new Error(`route touches disallowed program ${program}`); } return route; }; const result = await wrappedAccount.makeLoopTx({ ...loopParams, swapEngineRunner: reviewingRunner, }); ``` The same option exists on the collateral-swap, debt-swap and repay-with-collateral builders. ### Pinned `swapOpts.swapIxs` [#pinned-swapoptsswapixs] For a fully static, pre-reviewed route, pass the instructions yourself and omit `swapConfig`: ```typescript const result = await wrappedAccount.makeLoopTx({ ...loopParams, swapOpts: { swapIxs: { instructions: reviewedRoute.instructions, lookupTables: reviewedRoute.lookupTables, quoteResponse: reviewedRoute.quoteResponse, // required }, }, }); ``` Rules when pinning: * `quoteResponse` is required. Its `otherAmountThreshold` (guaranteed min-out, native units) sizes the deposit, and its `inAmount` must equal the loop's borrow amount. The SDK validates both and throws otherwise. * The route must pay out to the flow's destination token account (the deposit ATA of the account authority), and any extra ATAs the route needs are yours to create. * A pinned route belongs to the direct pair: `makeBridgedLoopTx` (and the other `makeBridged*Tx` builders) never attempt the bridged fallback when `swapIxs` is supplied. `examples/16c-loop-pinned-route.ts` shows the full capture → review → pin workflow. ## Handling errors [#handling-errors] Loop builders throw `TransactionBuildingError` with a typed `code`; two helpers cover the routing cases: ```typescript import { isDecomposableSwapError, isBridgeConflictError } from "@0dotxyz/p0-ts-sdk"; try { result = await wrappedAccount.makeLoopTx(loopParams); } catch (e) { if (isDecomposableSwapError(e)) { // SWAP_SIZE_EXCEEDED_* or SWAP_QUOTE_FAILED: the single route did not fit or had no quote. // The pair may still be loopable through a bridge token. result = await wrappedAccount.makeBridgedLoopTx(loopParams); } else { throw e; } } ``` `makeBridgedLoopTx` already does this retry internally. If the direct build fails and every bridge candidate is blocked, it throws `BRIDGE_CONFLICT`: ```typescript try { result = await wrappedAccount.makeBridgedLoopTx(loopParams); } catch (e) { if (isBridgeConflictError(e)) { // A token cannot be both collateral and debt on one bank. A loop borrows the bridge, // so candidates the account already supplies are skipped (bridgeTokenSide === "borrow"). const blocked = e.details.conflictingBanks.map((b) => b.symbol ?? b.mint).join(", "); throw new Error(`Loop must route through ${blocked}, but the account already supplies them`); } throw e; } ``` `e.details` for `BRIDGE_CONFLICT` is `{ conflictingBanks: Array<{ bankAddress, mint, symbol? }>, bridgeTokenSide: "deposit" | "borrow" }`. Any other error is the direct build's own failure (health, oracle crank, Kamino reserve lookup, ...); the full code table is on [Error Handling](/typescript-sdk/errors). ## Related [#related] * [Collateral Swaps, Debt Swaps & Repay with Collateral](/typescript-sdk/position-swaps) — the other swap-backed flash-loan builders * [Flash Loans](/typescript-sdk/flash-loans) — what the loop is built on, and how to compose your own * [Transactions](/typescript-sdk/transactions) — bundles, `simulateBundle`, priority fees * [Looping & Strategies guide](/guides/looping-and-strategies) * Examples: [16a-loop.ts](https://github.com/0dotxyz/p0-ts-sdk/tree/main/examples/16a-loop.ts), [16b-loop-bridged.ts](https://github.com/0dotxyz/p0-ts-sdk/tree/main/examples/16b-loop-bridged.ts), [16c-loop-pinned-route.ts](https://github.com/0dotxyz/p0-ts-sdk/tree/main/examples/16c-loop-pinned-route.ts) --- # Migration Guide Migrate from marginfi-client-v2 to p0-ts-sdk **Deprecation Notice**: The `@mrgnlabs/marginfi-client-v2` package has been deprecated since **March 15th, 2026** and no longer receives updates, bug fixes or new features. It was not updated for the 0.1.10 program upgrade (2026-08-25, see the [upgrade page](/typescript-sdk/program-upgrade)), so its flash-loan, health simulation and account-transfer paths are broken, and it cannot decode the oracle setups introduced by [program 0.1.11](/typescript-sdk/program-upgrade-0-1-11). Migrate to `@0dotxyz/p0-ts-sdk@^2.8.0`. This guide will help you migrate from `marginfi-client-v2` to the new `p0-ts-sdk`. The new SDK has been completely rebuilt with a cleaner API, better TypeScript support, and improved developer experience. ## Why Migrate? [#why-migrate] * **🏗️ Modern Architecture**: Built from scratch with TypeScript-first design * **🔍 Transparent Transactions**: You control when and how transactions are sent (no more hidden execution) * **🎯 Dual API**: Use classes (familiar) or standalone functions (flexible) * **🔧 Unified Structure**: Consistent patterns across all operations * **🔀 Cross-venue Support**: Native support for Kamino, Drift, and future venue integrations * **📊 Better DX**: Improved error handling, better types, clearer documentation ## Quick Comparison [#quick-comparison] | Feature | marginfi-client-v2 | p0-ts-sdk | | --------------------- | ------------------------------------------ | ------------------------------------------------- | | **Client Init** | `MarginfiClient.fetch()` requires wallet | `Project0Client.initialize()` - wallet-free | | **Account Creation** | Keypair-based (random addresses) | PDA-based (deterministic addresses) | | **Transactions** | Auto-sends with `processTransaction()` | Returns transactions for you to sign/send | | **Account Loading** | `client.getMarginfiAccount()` | `client.fetchAccount()` (convenience) | | **Account Discovery** | Manual program query | `client.getAccountAddresses(authority)` | | **Bank Access** | `client.banks` Map | `client.banks` array + helper methods | | **Deposits/Borrows** | `account.deposit()` sends tx | `account.makeDepositTx()` returns tx | | **Vendor Exports** | `import { ... } from "marginfi-client-v2"` | `import { ... } from "@0dotxyz/p0-ts-sdk/vendor"` | ## Installation [#installation] ```bash # Remove old SDK npm uninstall @mrgnlabs/marginfi-client-v2 # Install new SDK (2.8.0 is the minimum for the 0.1.11 program upgrade) npm install @0dotxyz/p0-ts-sdk@^2.8.0 ``` ## Breaking Changes [#breaking-changes] ### 1. Client Initialization [#1-client-initialization] **Old SDK (marginfi-client-v2):** ```typescript import { MarginfiClient, getConfig } from "@mrgnlabs/marginfi-client-v2"; import { Connection } from "@solana/web3.js"; import { Wallet } from "@coral-xyz/anchor"; const connection = new Connection("https://api.mainnet-beta.solana.com"); const config = getConfig("production"); // ❌ Wallet is REQUIRED const client = await MarginfiClient.fetch( config, wallet, // Must provide wallet connection ); ``` **New SDK (p0-ts-sdk):** ```typescript import { Project0Client, getConfig } from "@0dotxyz/p0-ts-sdk"; import { Connection } from "@solana/web3.js"; const connection = new Connection("https://api.mainnet-beta.solana.com"); const config = getConfig("production"); // Options changed, see below // ✅ Wallet NOT required - read-only by default const client = await Project0Client.initialize(connection, config); ``` **Config Options Changed:** * Old: `"production"`, `"staging"`, `"dev"` * New: `"production"`, `"staging"`, `"staging-mainnet-clone"`, `"staging-alt"` **Environment Descriptions:** * **`production`** - Production program and mainnet config * **`staging`** - Staging program updated 2 weeks before each release, allowing integrators to account for changes in advance * **`staging-mainnet-clone`** - Same staging program as `staging`, but with identical bank configuration as production (useful for testing against production-like data) * **`staging-alt`** - Internal deployment for testing unreleased & early versions of program iterations (not recommended for integrators) ### 2. Account Creation [#2-account-creation] This is the **biggest breaking change**. The old SDK used random keypairs for accounts, while the new SDK uses **PDAs (Program Derived Addresses)** for deterministic account addresses. **Old SDK (marginfi-client-v2):** ```typescript // ❌ Creates account with random keypair const { marginfiAccount, signature } = await client.createMarginfiAccount(); // Account address is random: DfG8h...xyz (different every time) ``` **New SDK (p0-ts-sdk):** ```typescript // ✅ Creates account with deterministic PDA const createAccountTx = await client.createMarginfiAccountTx( wallet.publicKey, 0 // account index // 12345 // third party ID ); // You sign and send the transaction const signature = await wallet.sendTransaction(createAccountTx, connection); await connection.confirmTransaction(signature); // Account address is deterministic: // Same wallet + same index = same account address every time ``` **Migration Impact:** * 🔴 **Existing accounts**: Your old keypair-based accounts will continue to work! You can still load and use them. * 🟢 **New accounts**: Will use the PDA system with deterministic addresses. * 🟡 **Account Index**: You can now create multiple accounts per wallet using different indices. **Deriving Account Addresses:** ```typescript import { deriveMarginfiAccount } from "@0dotxyz/p0-ts-sdk"; // Derive account address without RPC call const [accountAddress] = deriveMarginfiAccount( client.program.programId, client.group.address, wallet.publicKey, 0 // account index // 12345 // third party ID ); // This address is the same every time for: // same wallet + same index + same program ``` ### 3. Loading Accounts [#3-loading-accounts] **Old SDK (marginfi-client-v2):** ```typescript // ❌ Method on client const account = await client.getMarginfiAccount(accountAddress); // Returns MarginfiAccountWrapper directly await account.deposit(100, bankAddress); ``` **New SDK (p0-ts-sdk):** ```typescript // ✅ Convenience method (recommended) const wrappedAccount = await client.fetchAccount(accountAddress); // Or manual approach import { MarginfiAccount, MarginfiAccountWrapper } from "@0dotxyz/p0-ts-sdk"; const account = await MarginfiAccount.fetch(accountAddress, client.program); const wrappedAccount = new MarginfiAccountWrapper(account, client); // Now you can use it (but transactions work differently) const depositTx = await wrappedAccount.makeDepositTx(bankAddress, "100"); ``` **Account Discovery:** The new SDK makes it easy to discover all accounts for a wallet: ```typescript // Find all accounts owned by a wallet const accountAddresses = await client.getAccountAddresses(wallet.publicKey); console.log(`Found ${accountAddresses.length} accounts`); // Load the first account if (accountAddresses.length > 0) { const wrappedAccount = await client.fetchAccount(accountAddresses[0]); } ``` ### 4. Transaction Handling (CRITICAL) [#4-transaction-handling-critical] The new SDK **never sends transactions automatically**. You always get a transaction back that you must sign and send yourself. **Old SDK (marginfi-client-v2):** ```typescript // ❌ Automatically signs and sends transaction const signature = await account.deposit( 100, bankAddress, {}, // deposit options {}, // process options {} // tx options ); console.log("Already sent:", signature); ``` **New SDK (p0-ts-sdk):** ```typescript // ✅ Returns transaction for you to send const depositTx = await wrappedAccount.makeDepositTx(bankAddress, "100"); // YOU must sign and send depositTx.feePayer = wallet.publicKey; const signature = await wallet.sendTransaction(depositTx, connection); await connection.confirmTransaction(signature); ``` **Why This Change?** * ✅ **More Control**: You decide when and how to send transactions * ✅ **Batch Operations**: Easier to compose multiple operations * ✅ **Custom Signing**: Support for hardware wallets, multisig, etc. * ✅ **Simulation**: You can simulate before sending * ✅ **No Hidden Behavior**: Transparent and predictable ### 5. Bank Access [#5-bank-access] **Old SDK (marginfi-client-v2):** ```typescript // Access banks Map directly const bank = client.banks.get(bankAddress.toBase58()); // Helper methods const bank = client.getBankByMint(USDC_MINT); const bank = client.getBankByTokenSymbol("USDC"); const bank = client.getBankByPk(bankAddress); ``` **New SDK (p0-ts-sdk):** ```typescript // Banks available as array or via helpers const bank = client.getBank(bankAddress); // by address const banks = client.getBanksByMint(USDC_MINT); // returns array const allBanks = client.banks; // array of all banks // AssetTag filtering (new feature) import { AssetTag } from "@0dotxyz/p0-ts-sdk"; const usdcBanks = client.getBanksByMint(USDC_MINT, AssetTag.DEFAULT); const solBanks = client.getBanksByMint(SOL_MINT, AssetTag.SOL); const kaminoBanks = client.banks.filter( (b) => b.config.assetTag === AssetTag.KAMINO ); ``` ### 6. Deposits [#6-deposits] **Old SDK (marginfi-client-v2):** ```typescript // Sends transaction automatically const signature = await account.deposit( 100, // amount bankAddress, {}, // depositOpts {}, // processOpts {} // txOpts ); ``` **New SDK (p0-ts-sdk):** ```typescript // Returns transaction const depositTx = await wrappedAccount.makeDepositTx( bankAddress, "100" // amount as string ); // Simulate (optional) depositTx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash; depositTx.feePayer = wallet.publicKey; const simulation = await connection.simulateTransaction(depositTx); // Send const signature = await wallet.sendTransaction(depositTx, connection); await connection.confirmTransaction(signature); ``` ### 7. Withdrawals [#7-withdrawals] **Old SDK (marginfi-client-v2):** ```typescript const signature = await account.withdraw( 50, bankAddress, false // withdrawAll ); ``` **New SDK (p0-ts-sdk):** ```typescript const withdrawResult = await wrappedAccount.makeWithdrawTx( bankAddress, "50", // amount as string false // withdrawAll ); // ⚠️ Like borrow, withdraw may return multiple transactions for (const tx of withdrawResult.transactions) { tx.feePayer = wallet.publicKey; const sig = await wallet.sendTransaction(tx, connection); await connection.confirmTransaction(sig); } ``` ### 8. Borrows [#8-borrows] **Old SDK (marginfi-client-v2):** ```typescript const signature = await account.borrow(100, bankAddress); ``` **New SDK (p0-ts-sdk):** ```typescript // Check max borrow first (token UI units; since 2.7.1 also clamped by the // bank's borrow cap, available liquidity and rate limiters — pass // { ignoreBankLimits: true } for the purely health-based number) const maxBorrow = wrappedAccount.computeMaxBorrowForBank(bankAddress); console.log(`Max borrow: ${maxBorrow.toString()}`); // Build borrow transaction(s) const borrowResult = await wrappedAccount.makeBorrowTx(bankAddress, "100"); // ⚠️ May return MULTIPLE transactions console.log(`Transactions: ${borrowResult.transactions.length}`); // Send all transactions in order for (const tx of borrowResult.transactions) { tx.feePayer = wallet.publicKey; const sig = await wallet.sendTransaction(tx, connection); await connection.confirmTransaction(sig); } ``` **Important**: Some operations (borrow, repay with collateral, loops) may return **multiple transactions** that must be executed in order. ### 9. Repayment [#9-repayment] **Old SDK (marginfi-client-v2):** ```typescript // Repay all const signature = await account.repay(0, bankAddress, true); // Repay specific amount const signature = await account.repay(50, bankAddress, false); ``` **New SDK (p0-ts-sdk):** ```typescript // Repay specific amount const repayTx = await wrappedAccount.makeRepayTx( bankAddress, "50", false // repayAll ); // Repay all const repayAllTx = await wrappedAccount.makeRepayTx( bankAddress, "0", // amount ignored when repayAll = true true // repayAll ); const signature = await wallet.sendTransaction(repayTx, connection); await connection.confirmTransaction(signature); ``` ### 10. Vendor Exports [#10-vendor-exports] Heavy dependencies (oracles, protocol integrations) are now in a separate entry point to reduce bundle size. **Old SDK (marginfi-client-v2):** ```typescript import { fetchPythPrice, fetchSwitchboardPrice, // ... all vendor exports mixed in } from "@mrgnlabs/marginfi-client-v2"; ``` **New SDK (p0-ts-sdk):** ```typescript // Main exports (lightweight) — includes the oracle price service import { Project0Client, MarginfiAccount, fetchOracleData, } from "@0dotxyz/p0-ts-sdk"; // Vendor exports (separate) — raw oracle / venue decoders, IDLs, constants import { decodeSwitchboardPullFeedData, getSwitchboardProgram, // ... Pyth, Kamino (klend), Drift, JupLend, Exponent, Gamma internals } from "@0dotxyz/p0-ts-sdk/vendor"; ``` Only reach for `/vendor` when you need the raw integration internals; the high-level helpers (including `fetchOracleData`) live on the main entry. ## Common Patterns [#common-patterns] ### Pattern: Deposit Flow [#pattern-deposit-flow] ```typescript // Complete deposit example import { Project0Client } from "@0dotxyz/p0-ts-sdk"; // 1. Initialize client (once) const client = await Project0Client.initialize(connection, config); // 2. Load account const wrappedAccount = await client.fetchAccount(accountAddress); // 3. Get bank const usdcBanks = client.getBanksByMint(USDC_MINT); const usdcBank = usdcBanks[0]; // 4. Build deposit transaction const depositTx = await wrappedAccount.makeDepositTx(usdcBank.address, "100"); // 5. Sign and send depositTx.feePayer = wallet.publicKey; const signature = await wallet.sendTransaction(depositTx, connection); await connection.confirmTransaction(signature); ``` ### Pattern: Borrow Flow [#pattern-borrow-flow] ```typescript // Complete borrow example const usdcBank = client.getBanksByMint(USDC_MINT)[0]; // 1. Check borrow capacity const maxBorrow = wrappedAccount.computeMaxBorrowForBank(usdcBank.address); console.log(`Max borrow: ${maxBorrow.toString()} USDC`); // 2. Build borrow transaction(s) const borrowResult = await wrappedAccount.makeBorrowTx(usdcBank.address, "50"); // 3. Handle multi-transaction case console.log(`Transactions to send: ${borrowResult.transactions.length}`); // 4. Send all transactions in order for (const tx of borrowResult.transactions) { tx.feePayer = wallet.publicKey; const sig = await wallet.sendTransaction(tx, connection); await connection.confirmTransaction(sig); } ``` ### Pattern: Create and Fund New Account [#pattern-create-and-fund-new-account] ```typescript // 1. Create account transaction const createAccountTx = await client.createMarginfiAccountTx( wallet.publicKey, 0 ); // 2. Send creation transaction const createSig = await wallet.sendTransaction(createAccountTx, connection); await connection.confirmTransaction(createSig); // 3. Derive the account address import { deriveMarginfiAccount } from "@0dotxyz/p0-ts-sdk"; const [accountAddress] = deriveMarginfiAccount( client.program.programId, client.group.address, wallet.publicKey, 0 ); // 4. Load the newly created account const wrappedAccount = await client.fetchAccount(accountAddress); // 5. Deposit into it const depositTx = await wrappedAccount.makeDepositTx(bankAddress, "100"); const depositSig = await wallet.sendTransaction(depositTx, connection); await connection.confirmTransaction(depositSig); ``` ## Getting Help [#getting-help] * **Examples**: [View runnable examples](https://github.com/0dotxyz/p0-ts-sdk/tree/main/examples) * **GitHub Issues**: [Report issues or ask questions](https://github.com/0dotxyz/p0-ts-sdk/issues) * **Discord**: [Join our developer community](https://discord.com/invite/c2kNj7y9w8) ## Summary Checklist [#summary-checklist] Use this checklist to track your migration progress: * [ ] Installed `@0dotxyz/p0-ts-sdk` and removed `@mrgnlabs/marginfi-client-v2` * [ ] Updated client initialization (removed wallet requirement) * [ ] Updated account loading (use `client.fetchAccount()` convenience method) * [ ] Updated account discovery (use `client.getAccountAddresses()` if needed) * [ ] Changed all `account.deposit()` → `account.makeDepositTx(bankAddress, amount)` * [ ] Changed all `account.withdraw()` → `account.makeWithdrawTx(bankAddress, amount, withdrawAll)` * [ ] Changed all `account.borrow()` → `account.makeBorrowTx(bankAddress, amount)` * [ ] Changed all `account.repay()` → `account.makeRepayTx(bankAddress, amount, repayAll)` * [ ] Added manual transaction signing and sending for all operations * [ ] Handle multi-transaction operations (borrow, repay with collateral) * [ ] Updated bank access patterns (arrays + AssetTag) * [ ] Moved raw vendor imports to `@0dotxyz/p0-ts-sdk/vendor` (high-level helpers stay on the main entry) * [ ] Updated account creation to use PDAs * [ ] Tested all critical user flows * [ ] Updated error handling for transaction confirmation * [ ] Documented account indices for future reference ## Timeline [#timeline] * **March 15th, 2026**: `marginfi-client-v2` deprecated — no further updates. The migration period has ended; only `@0dotxyz/p0-ts-sdk` receives updates and support. * **August 25th, 2026, 17:00 CEST (15:00 UTC)**: mainnet program flips to **0.1.10**. `marginfi-client-v2` and `p0-ts-sdk` `< 2.7.0` break on flash loans (loops, swaps, repay-with-collateral), health simulation and account transfers after this point. Reads keep working. See [Program Upgrade 0.1.10](/typescript-sdk/program-upgrade). * **Program 0.1.11 (oracle upgrade)**: banks start moving to Scope and exchange-rate oracles from **September 4, 2026**. `p0-ts-sdk` `< 2.8.0` and `marginfi-client-v2` throw while decoding those banks. See [Program Upgrade 0.1.11](/typescript-sdk/program-upgrade-0-1-11). **If you are still on `marginfi-client-v2`, migrate to `@0dotxyz/p0-ts-sdk@^2.8.0` now.** --- # Overview What the Project 0 TypeScript SDK is, how it is organised, and where to start. `@0dotxyz/p0-ts-sdk` is the official TypeScript SDK for the marginfi program that powers Project 0 on Solana. It loads the protocol state (banks, oracle prices, lookup tables, e-mode settings) into a single client, wraps your marginfi account in a convenient API, and builds complete, ready-to-sign transactions for every protocol action — from a plain deposit to a leveraged loop that crosses venues (Project 0, Kamino, Drift and Jupiter Lend). **Program upgrade 0.1.11 — new oracle setups.** Integrators must be on `@0dotxyz/p0-ts-sdk@^2.8.0` before the first bank moves to a Scope or exchange-rate oracle; older SDKs throw while decoding those banks, which breaks client initialization. Upgrading is additive — no code changes. See [Program Upgrade 0.1.11](/typescript-sdk/program-upgrade-0-1-11). Coming from `@mrgnlabs/marginfi-client-v2`? It has been deprecated since March 2026 — follow the [migration guide](/typescript-sdk/migration). ## Start here [#start-here] Install the SDK, pick an environment, and run your first deposit and borrow. Wrapper methods, transaction builders, instruction builders and raw instructions — and when to use each. ## Core [#core] Create, derive, discover and load marginfi accounts; read positions; transfer and close. Find banks, understand asset tags, read APYs, caps, rate limits and oracle prices. The four core operations, their options, return shapes and how to send them. Health factor, free collateral, bank-aware max borrow/withdraw/deposit, and e-mode impacts. ## Advanced flows [#advanced-flows] Build leveraged positions in one flash loan with direct or bridged swap routing. Swap collateral, swap debt, repay with collateral, and roll Exponent PTs. Wrap your own instructions in a marginfi flash loan. Kamino, Drift and Jupiter Lend banks, plus minting LSTs from native stake. ## Reference [#reference] Result types, multi-transaction ordering, bundles, priority fees, oracle cranking and lookup tables. Typed `TransactionBuildingError` codes and the program errors you should recognise. Scope and exchange-rate oracles: why `>= 2.8.0` is required and what changes for you. What the 0.1.9 → 0.1.10 upgrade changed (completed 2026-08-25). Move from `marginfi-client-v2` to `p0-ts-sdk`. ## Package entry points [#package-entry-points] | Import | Contents | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `@0dotxyz/p0-ts-sdk` | Everything you normally need: `Project0Client`, `MarginfiAccountWrapper`, transaction and instruction builders, types and utilities. | | `@0dotxyz/p0-ts-sdk/vendor` | Heavy oracle and venue integrations (Pyth, Switchboard, Kamino, Drift, Jupiter Lend, Exponent, …). Split out to keep bundles small. | | `@0dotxyz/p0-ts-sdk/jupiter` | Jupiter swap helpers (`createJupiterClient`). | | `@0dotxyz/p0-ts-sdk/instructions` | Raw Anchor instruction builders — see [Architecture](/typescript-sdk/architecture). | The SDK is written in TypeScript and exports all of its types. Builders never sign or send: every `make*Tx` returns unsigned transactions and every `make*Ix` returns instructions, so the SDK fits any wallet or key-management setup. It works on mainnet (`production`) and the staging deployments; there is no devnet deployment. ## Running the examples [#running-the-examples] The SDK repository ships runnable examples in [`examples/`](https://github.com/0dotxyz/p0-ts-sdk/tree/main/examples): * **Core** — `01-deposit`, `02-borrow`, `03-withdraw`, `04-repay` * **Pricing & health** — `05-oracle-prices`, `06a-account-health-simulated`, `06b-account-health-calculated` * **Position swaps** — `10-swap-collateral`, `11-swap-debt` * **Native stake** — `12-mint-staked-lst`, `13-redeem-staked-lst`, `14-merge-stake-accounts` * **Exponent PT rollover** — `15-roll-pt`, `17-roll-pt-redeem-proof` * **Leverage loops** — `16a-loop`, `16b-loop-bridged`, `16c-loop-pinned-route` * **Discovery** — `18-mint-holders` * **Swaps** — `swap-engine` (multi-provider quoting used by loops and swaps) ```bash git clone https://github.com/0dotxyz/p0-ts-sdk.git cd p0-ts-sdk/examples cp .env.example .env # set RPC URL, account address, optional keypair npx tsx 01-deposit.ts ``` Each example runs in **simulation mode** by default, so you can explore without spending fees or risking funds. ## API reference and help [#api-reference-and-help] * **TypeDoc** — clone the repository and run `pnpm docs` (output in `docs/`). Every exported function and type also carries JSDoc in [`src/`](https://github.com/0dotxyz/p0-ts-sdk/tree/main/src). * **GitHub Issues** — [report bugs or request features](https://github.com/0dotxyz/p0-ts-sdk/issues). * **Discord** — join the [developer community](https://discord.com/invite/c2kNj7y9w8). * **AI agents** — the docs are available through the [MCP server](/guides/mcp-server) and as plain markdown (`/llms.txt`). --- # Collateral Swaps, Debt Swaps & Repay with Collateral Rebalance an open position atomically — swap one collateral for another, move debt between tokens, repay debt from collateral, or roll a matured Exponent PT — without touching account health mid-way. These builders restructure an existing position inside a single flash loan: the withdraw or borrow, the swap and the deposit or repay all land in one transaction, and the risk engine only checks health at the end. The user never has to hold the intermediate tokens or survive a temporarily unhealthy account. This page assumes you have a `client` and `wrappedAccount` from [Getting Started](/typescript-sdk/getting-started). | Builder | What it does | Shape inside the flash loan | | ------------------------------------------------------ | ------------------------------------------------- | ------------------------------------------------- | | `makeSwapCollateralTx` / `makeBridgedSwapCollateralTx` | Replace collateral A with collateral B | withdraw A → swap A→B → deposit B | | `makeSwapDebtTx` / `makeBridgedSwapDebtTx` | Replace debt A with debt B | borrow B → swap B→A → repay A | | `makeRepayWithCollatTx` | Pay down debt using collateral | withdraw A → swap A→B → repay B | | `makeRollPtTx` | Roll a matured Exponent PT into the next maturity | withdraw PT\_old → redeem → buy PT\_new → deposit | The `makeBridged*` variants try the direct single-route build first and, when the swap does not fit one transaction or cannot be quoted, decompose it through a bridge token (USDC, wSOL, USDT by default) as one atomic Jito bundle. `swapOpts` is the same `{ swapConfig }` object documented on [Looping](/typescript-sdk/looping#configure-the-swap). ## Reading the current position size [#reading-the-current-position-size] Every builder takes `totalPositionAmount` in UI units so it can detect a full withdraw/repay. Compute it from the balance's shares through the bank: ```typescript const sourceBalance = wrappedAccount.activeBalances.find((b) => b.bankPk.equals(sourceBank.address) ); if (!sourceBalance) throw new Error("No position in this bank"); // Collateral position const collateralUi = sourceBank .getAssetQuantity(sourceBalance.assetShares) .div(10 ** sourceBank.mintDecimals) .toNumber(); // Debt position const debtUi = sourceBank .getLiabilityQuantity(sourceBalance.liabilityShares) .div(10 ** sourceBank.mintDecimals) .toNumber(); ``` `balance.computeQuantityUi(bank)` returns `{ assets, liabilities }` already in UI units if you prefer; see [Accounts](/typescript-sdk/accounts#reading-positions). Always read the token program of each bank with `wrappedAccount.getMintDataFromBank(bank)` — the builders need it for both sides. ## Collateral swap [#collateral-swap] The destination bank must pass `isStandardDepositable`. Omit `withdrawAmount` to swap the whole position; pass it for a partial swap. ```typescript import { isStandardDepositable } from "@0dotxyz/p0-ts-sdk"; const destinationBank = client.banks.find( (bank) => bank.mint.equals(MSOL_MINT) && isStandardDepositable(bank) )!; const sourceMintData = await wrappedAccount.getMintDataFromBank(sourceBank); const destinationMintData = await wrappedAccount.getMintDataFromBank(destinationBank); const result = await wrappedAccount.makeSwapCollateralTx({ connection, withdrawOpts: { totalPositionAmount: collateralUi, withdrawAmount: collateralUi / 2, // optional, defaults to totalPositionAmount withdrawBank: sourceBank, tokenProgram: sourceMintData.tokenProgram, }, depositOpts: { depositBank: destinationBank, tokenProgram: destinationMintData.tokenProgram, }, swapOpts: { swapConfig }, assetShareValueMultiplierByBank: client.assetShareValueMultiplierByBank, }); // { transactions, actionTxIndex, quoteResponse, mustBeAtomicBundle } ``` ```typescript const result = await wrappedAccount.makeBridgedSwapCollateralTx({ connection, withdrawOpts: { totalPositionAmount: collateralUi, withdrawBank: sourceBank, tokenProgram: sourceMintData.tokenProgram, }, depositOpts: { depositBank: destinationBank, tokenProgram: destinationMintData.tokenProgram, }, swapOpts: { swapConfig }, bridgeOpts: { bridgeCandidateMints: [USDC_MINT, SOL_MINT] }, // optional }); // BridgedTxResult: { transactions, actionTxIndex, quoteResponse, mustBeAtomicBundle, bridgeMint? } ``` `assetShareValueMultiplierByBank` is injected for you. A bridged build deposits the bridge token in the first leg and swaps it on in the second, so any bridge candidate the account already *borrows* is skipped; if all candidates are blocked the call throws `BRIDGE_CONFLICT` (see [Handling errors](#handling-errors)). If source and destination share a mint (moving between two banks of the same token) no swap is run and `quoteResponse` is `undefined`. Otherwise `quoteResponse.outAmount` is the expected destination amount in native units. ## Debt swap [#debt-swap] The destination bank must pass `isStandardBorrowable`. There is no ExactOut quote for this flow, so the builder sizes the new borrow from the two `marketPrice` values (USD per token, UI units) you pass — the examples use the client's realtime oracle prices. Bridge legs price themselves from the oracle map automatically. ```typescript import { isStandardBorrowable } from "@0dotxyz/p0-ts-sdk"; const destinationBank = client.banks.find( (bank) => bank.mint.equals(SOL_MINT) && isStandardBorrowable(bank) )!; const priceOf = (bank: typeof sourceBank) => client.oraclePriceByBank.get(bank.address.toBase58())!.priceRealtime.price.toNumber(); const result = await wrappedAccount.makeSwapDebtTx({ connection, repayOpts: { totalPositionAmount: debtUi, repayAmount: debtUi, // optional, defaults to totalPositionAmount repayBank: sourceBank, tokenProgram: sourceMintData.tokenProgram, marketPrice: priceOf(sourceBank), }, borrowOpts: { borrowBank: destinationBank, tokenProgram: destinationMintData.tokenProgram, marketPrice: priceOf(destinationBank), }, swapOpts: { swapConfig }, assetShareValueMultiplierByBank: client.assetShareValueMultiplierByBank, }); // { transactions, actionTxIndex, quoteResponse, mustBeAtomicBundle } ``` ```typescript const result = await wrappedAccount.makeBridgedSwapDebtTx({ connection, repayOpts: { totalPositionAmount: debtUi, repayBank: sourceBank, tokenProgram: sourceMintData.tokenProgram, marketPrice: priceOf(sourceBank), }, borrowOpts: { borrowBank: destinationBank, tokenProgram: destinationMintData.tokenProgram, marketPrice: priceOf(destinationBank), }, swapOpts: { swapConfig }, bridgeOpts: { bridgeCandidateMints: [USDC_MINT, SOL_MINT] }, // optional }); ``` The first leg repays A by borrowing the bridge token; the second repays exactly that bridge debt while borrowing B. Bridge candidates the account already *supplies* are skipped. Reading the quote differs between the two paths: on a direct debt swap the new debt is the borrow, i.e. `quoteResponse.inAmount`; on a bridged result the merged quote maps it to `quoteResponse.outAmount`. Check `result.bridgeMint !== undefined` to tell them apart. ## Repay with collateral [#repay-with-collateral] Withdraw collateral, swap it into the debt token and repay, in one flash loan. `withdrawAmount` is the exact collateral to sell; `repayOpts.totalPositionAmount` caps the repay so a swap that over-delivers closes the debt instead of overpaying. ```typescript const result = await wrappedAccount.makeRepayWithCollatTx({ connection, withdrawOpts: { totalPositionAmount: collateralUi, withdrawAmount: 0.25, withdrawBank: collateralBank, tokenProgram: collateralMintData.tokenProgram, }, repayOpts: { repayBank: debtBank, tokenProgram: debtMintData.tokenProgram, totalPositionAmount: debtUi, }, swapOpts: { swapConfig }, assetShareValueMultiplierByBank: client.assetShareValueMultiplierByBank, }); console.log(`repaying ~${result.amountToRepay} ${debtBank.tokenSymbol}`); ``` Returns `{ transactions, swapQuote, amountToRepay, mustBeAtomicBundle }`. `amountToRepay` is derived from the swap's guaranteed min-out (`otherAmountThreshold`) and clamped to `totalPositionAmount`; when the expected output exceeds the debt the builder repays the full position instead. If collateral and debt share a mint no swap is run and `amountToRepay === withdrawAmount`. There is no bridged variant of this builder. ## Shared options [#shared-options] All four swap-backed builders accept: | Option | Description | | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `swapOpts` | `{ swapConfig }` (or a pinned `{ swapIxs }`) — see [Looping](/typescript-sdk/looping#configure-the-swap) | | `swapEngineRunner` | Override or wrap `runSwapEngine` to inspect/veto the chosen route — see [Controlling the route](/typescript-sdk/looping#controlling-the-route) | | `additionalIxs` | Extra instructions spliced into the flash loan | | `crossbarUrl` | Switchboard crossbar endpoint override for oracle cranks | | `overrideInferAccounts` | `{ group?, authority? }` | | `assetShareValueMultiplierByBank` | Required on the direct builders (`client.assetShareValueMultiplierByBank`); injected by the `makeBridged*` builders | Address lookup tables are always taken from `client.addressLookupTables`. Results are simulated and sent exactly like a loop — see [Simulate and send](/typescript-sdk/looping#simulate-and-send) and [Transactions](/typescript-sdk/transactions) for bundles. ## Handling errors [#handling-errors] The direct builders throw `SWAP_SIZE_EXCEEDED_POSITION_SWAP` (or `SWAP_SIZE_EXCEEDED_REPAY` for repay-with-collateral) when the route does not fit the transaction and `SWAP_QUOTE_FAILED` when it cannot be quoted; `isDecomposableSwapError(e)` matches both, and retrying with the `makeBridged*` builder is the usual answer. The bridged builders throw `BRIDGE_CONFLICT` when every bridge candidate is blocked by an opposite-side position: ```typescript import { isBridgeConflictError } from "@0dotxyz/p0-ts-sdk"; try { result = await wrappedAccount.makeBridgedSwapCollateralTx(params); } catch (e) { if (isBridgeConflictError(e)) { const blocked = e.details.conflictingBanks.map((b) => b.symbol ?? b.mint).join(", "); throw new Error(`Swap must route through ${blocked}, but the account already borrows them`); } throw e; } ``` The full code table is on [Error Handling](/typescript-sdk/errors). ## Exponent PT rollover [#exponent-pt-rollover] `makeRollPtTx` rolls a matured Exponent PT collateral position into its successor maturity so the full deposit ends up as the new PT: withdraw the old PT, redeem it to SY via Exponent `merge`, buy the new PT on the successor's CLMM pool, deposit. No external aggregator is involved, so there is no `swapOpts`; the Exponent accounts are resolved internally from `rollOpts`: ```typescript const result = await wrappedAccount.makeRollPtTx({ connection, withdrawOpts: { totalPositionAmount: maturedPtUi, withdrawAmount: maturedPtUi, // optional, defaults to the full position withdrawBank: maturedPtBank, tokenProgram: maturedMintData.tokenProgram, }, depositOpts: { depositBank: successorPtBank, tokenProgram: successorMintData.tokenProgram, }, rollOpts: { maturedMarket: MATURED_MARKET, // or maturedVault — one of the two is required successorMarket: SUCCESSOR_MARKET, // the successor CLMM (MarketThree) pool slippageBps: 100, // default 50 // lookupTable?: PublicKey — dedicated PT-roll LUT, see examples/create-pt-roll-lut.ts }, assetShareValueMultiplierByBank: client.assetShareValueMultiplierByBank, }); // { transactions, actionTxIndex, quoteResponse } ``` `RollPtOpts` also accepts `syTokenProgram` (defaults to the classic Token program). The SY→PT price is quoted by simulating a standalone `trade_pt` through `connection.simulateTransaction`; pass `simulateTx` (a `RollQuoteSimulator`) if your RPC proxy blocks that call. `quoteResponse.otherAmountThreshold` is the minimum new PT the deposit is sized to. The complete flow is in `examples/15-roll-pt.ts`. ## Related [#related] * [Looping](/typescript-sdk/looping) — `SwapProviderConfig`, swap-engine routing and pinned routes * [Flash Loans](/typescript-sdk/flash-loans) — the primitive these builders are built on * [Error Handling](/typescript-sdk/errors) * [Managing Your Account guide](/guides/managing-your-account) * Examples: [10-swap-collateral.ts](https://github.com/0dotxyz/p0-ts-sdk/tree/main/examples/10-swap-collateral.ts), [11-swap-debt.ts](https://github.com/0dotxyz/p0-ts-sdk/tree/main/examples/11-swap-debt.ts), [15-roll-pt.ts](https://github.com/0dotxyz/p0-ts-sdk/tree/main/examples/15-roll-pt.ts) --- # Program Upgrade 0.1.11 Upgrade to SDK 2.8.0 before September 4, 2026 — banks moving to the new oracle setups will not decode on older SDKs **Upgrade to `@0dotxyz/p0-ts-sdk@^2.8.0` before Friday, September 4, 2026.** From that date banks start moving to the new 0.1.11 oracle setups. SDK `2.7.x` and older throw while decoding any such bank, which fails `Project0Client.initialize` and every bank read — not just actions on that bank. The upgrade is additive: no code changes required. **TL;DR:** program 0.1.11 adds nine oracle setups that do not depend on Switchboard — Kamino **Scope** feeds, and **exchange-rate** pricing for mSOL, SPL/Sanctum LSTs and Exponent PTs. Existing instructions and account layouts are untouched. What breaks is **decoding**: older SDKs don't know the new enum values, so one migrated bank stops them loading banks at all. SDK `>= 2.8.0` decodes and prices every setup, includes the extra accounts in health checks, and is padded so future setups degrade gracefully instead of throwing. ## What changes [#what-changes] * **Nine new `OracleSetup` variants** (18–26): `Scope`, `PythMSOL`, `KaminoMSOL`, `JuplendMSOL`, `PythLST`, `KaminoLST`, `JuplendLST`, `PTPyth`, `PTFixed`. How each one prices is on [Oracles](/protocol-overview/oracles). * **`BankConfig.scope_entry_index`** (`u16`) replaces two padding bytes. Same size, no other layout change. * **Every existing instruction keeps its accounts and arguments.** Two admin instructions are added (`lending_pool_configure_bank_oracle_scope`, `lending_pool_set_oracle_price`), replacing `lending_pool_set_fixed_oracle_price`. * **New error codes** `6136`–`6139` and `6800`–`6803`; `6132` renamed to `UseSetOraclePrice`. See [Errors](/typescript-sdk/errors). ## The breaking change [#the-breaking-change] A bank's oracle setup is a one-byte enum and Anchor resolves it against the IDL. The IDL in SDK `<= 2.7.4` lists 18 variants, so a setup byte of `18` or higher makes the coder throw: ``` TypeError: Cannot read properties of null (reading 'property') ``` `Project0Client.initialize` decodes every bank in the group, so a single migrated bank breaks initialization for everyone on an older SDK. This affects **reads**, and it is triggered by the first bank migration — not by the program deploy. ## What you must do [#what-you-must-do] **Using `Project0Client` or the SDK builders** — upgrade. Nothing else. `initialize` passes the new pricing options through, `computeHealthAccountMetas` appends the extra accounts, and unknown future setups parse to `OracleSetup.Unknown` (zero price) instead of throwing. **Calling `fetchOracleData` directly** — pass the two new, optional option blocks, or banks on the new setups price at zero with a warning: ```ts const { bankOraclePriceMap } = await fetchOracleData(banks, { pythOpts: { mode: "on-chain", connection }, swbOpts: { mode: "on-chain", connection }, scopeOpts: { mode: "on-chain", connection }, oracleMultiplierOpts: { mode: "on-chain", connection }, }); ``` The exchange rate is baked into the price: an mSOL bank's `OraclePrice` is mSOL/USD, matching what the program caches on the bank. Venue receipt-token rates stay in `assetShareValueMultiplierByBank`, as before. See [Banks & Prices](/typescript-sdk/banks-and-prices#scope-and-exchange-rate-oracles-program-0111). **Hand-rolling `remainingAccounts`** — the new setups need their pricing account after the oracle: `oracleKeys[1]` for `PythMSOL` / `PythLST` / `PTPyth`, `oracleKeys[2]` for the Kamino and JupLend variants. `Scope` and `PTFixed` keep the `[bank, oracleKeys[0]]` shape. Prefer `computeHealthAccountMetas`. **Decoding accounts yourself** — use the SDK's bundled 0.1.11 IDL, passed through `new Program(...)`. Do not fetch the IDL from chain; the on-chain IDL account is not kept current. **Serving prices or multipliers from your own API** — return prices the way the program caches them (rate included, venue rate excluded). In API mode the SDK only rejects non-numeric multipliers, so your endpoint must return program-valid rates. ## Testing on staging [#testing-on-staging] `getConfig("staging-mainnet-clone")` already runs 0.1.11 with one bank per new family: | Bank | Setup | | ---------------------------------------------- | --------------------- | | `Dt8xUxXxUgVpg74ke5WB4XBogmv1E4TM7zRfwEnbm3SF` | wSOL · `Scope` | | `Bf2QXbRZPGtDHvd4AVoFDb4jNjMSDjsPGDaboSHkzE21` | mSOL · `PythMSOL` | | `ZEK4wkNUMbU2ALzKSfb888yZwugypJY466snSj6CuK6` | jitoSOL · `PythLST` | | `4jNGCy6SvEY2itfCrvTGQC65mKHcwZgoahiZjtvmBizK` | PT-hyUSD · `PTFixed` | | `7Np6qLQg691RUM64S4J63Py7dRNGMKho3Fh9UU5Q9YZw` | PT-hyloSOL · `PTPyth` | ## Troubleshooting [#troubleshooting] | Symptom | Cause | Fix | | --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | ----------------------------------------------- | | `TypeError: Cannot read properties of null (reading 'property')` on `initialize` or any bank decode | SDK `<= 2.7.4` meeting a 0.1.11 bank | Upgrade to `>= 2.8.0` | | Banks priced at `0` plus a `no scopeOpts` / `no oracleMultiplierOpts` warning | Direct `fetchOracleData` call without the new options | Pass both options | | `WrongNumberOfOracleAccounts` / `WrongOracleAccountKeys` on borrow or withdraw | Hand-built `remainingAccounts` missing the pricing account | Use `computeHealthAccountMetas` | | `ScopeStalePrice` (`6802`) or `StakePoolStale` (`6139`) | Feed entry or stake pool not refreshed | Wait; the SDK prices the bank at zero meanwhile | --- # Program Upgrade 0.1.10 What the marginfi 0.1.9 → 0.1.10 program upgrade means for SDK integrators **Completed.** Mainnet flipped to program 0.1.10 on 2026-08-25 15:00 UTC. SDK `2.6.x` and older break on flash loans, health simulation and account transfers. The next upgrade, 0.1.11, is documented on [Program Upgrade 0.1.11](/typescript-sdk/program-upgrade-0-1-11) and raises the minimum SDK to `2.8.0`. **TL;DR:** program 0.1.10 inserts required accounts into six instructions. That is a positional wire break in both directions, so a single SDK build cannot be sent to both programs unchanged. SDK `>= 2.7.0` builds 0.1.10-style and, for the three affected instructions it actually uses, removes the inserted account again while the target program still runs 0.1.9. The switch is a per-program-id timestamp baked into the SDK — no RPC call, no caching. Upgrade the SDK any time **before the flip** with zero code changes. ## Deployments [#deployments] | Program | Program ID | Group | Status | | ------- | --------------------------------------------- | ---------------------------------------------- | ----------------------------------------- | | 0.1.9 | `MFv2hWf31Z9kbCa1snEPYctwafyhdvnV7FZnsebVacA` | `4qp6Fx6tnZkY5Wropq9wUYgtFxXKwE6viZxFHg3rdAG8` | mainnet `production` until the flip | | 0.1.10 | `stag8sTKds2h4KzjUw3zKTsxbqvT4XKHdaR9X9E6Rct` | `FCPfpHA69EbS8f9KKSreTRkXbzFpunsKuYf5qNmnJjpo` | `staging` since 2026-08-03; mainnet after | Use `getConfig("staging")` (or `"staging-mainnet-clone"` for a production-like bank set) to test against 0.1.10 today. ## What changes in the program [#what-changes-in-the-program] * **Per-bank oracle circuit breaker** — a bank whose price moves more than a configured threshold in a window temporarily halts risk-increasing operations (borrow, withdraw, liquidate). New `OperationalState.CircuitBroken` bank state, which the SDK parses instead of throwing. * **Same-asset e-mode** — banks sharing a mint and oracle family (e.g. Kamino SOL, JupLend SOL, P0 SOL) can be placed in same-asset e-mode for higher leverage. New `SameAssetEmodeRegistry` account per group. * **Configurable fees** — per-liability-bank liquidation fees and a configurable account-transfer fee in `FeeState`. * **Account resizes** — `MarginfiGroup` grows 1,056 → 9,248 bytes and `FeeState` 256 → 512 bytes (reserved padding, layouts are prefix-identical). Expect a short outage right after the mainnet deploy while accounts are resized. * **Oracle fix** — Switchboard feeds no longer use `std_dev`; `oracle_max_confidence` configures a static confidence spread instead. Full details: [UPGRADE-0.1.10.md](https://github.com/0dotxyz/p0-ts-sdk/blob/main/UPGRADE-0.1.10.md) in the SDK repo. ## The breaking change: inserted accounts [#the-breaking-change-inserted-accounts] Solana instructions carry accounts as an ordered, unnamed array. Inserting a required account mid-list shifts every later index, so the six instructions below break in **both** directions (old layout → new program and new layout → old program): | Instruction | Inserted account | Position | SDK handles it | | ----------------------------------- | ----------------------------------------- | ----------------------------------------- | ------------------------------------- | | `lending_account_end_flashloan` | `group` | before `authority` (displaces the signer) | ✅ `makeEndFlashLoanIx` | | `lending_account_pulse_health` | `group` | after `marginfi_account` | ✅ `makePulseHealthIx` | | `transfer_to_new_account` | `fee_state` | before `system_program` | ✅ `makeAccountTransferToNewAccountIx` | | `transfer_to_new_account_pda` | `fee_state` | before `system_program` | — no SDK builder | | `lending_account_start_liquidation` | `group` | after `liquidation_record` | — no SDK builder | | `lending_account_end_liquidation` | `group` (+ optional trailing `fee_payer`) | after `liquidation_record` | — no SDK builder | Everything else survives: deposit / withdraw / borrow / repay / account create & close are unchanged, account decoding works across versions (layouts are prefix-identical), and admin instructions that only gained trailing `Option` args are tolerated by 0.1.9. ## How the SDK handles it [#how-the-sdk-handles-it] SDK `2.7.0`–`2.7.3` carried a per-program-id timestamp switch (`MARGINFI_V0_1_10_ACTIVATION` / `isMarginfiV0110Live`) so one build could talk to both programs around the flip: `makeEndFlashLoanIx`, `makePulseHealthIx` and `makeAccountTransferToNewAccountIx` built the 0.1.10 layout and spliced the inserted account out while the target program still ran 0.1.9. The switch was removed in **2.7.4** once the upgrade was final; the SDK now emits the 0.1.10 layout unconditionally, and those exports no longer exist. ## What you must do [#what-you-must-do] **Using the SDK's builders (Layers 2–4):** 1. Upgrade to `@0dotxyz/p0-ts-sdk@^2.7.0` before 2026-08-25 15:00 UTC. No code changes. 2. Optionally test against `getConfig("staging")`, which already runs 0.1.10. **Hand-rolling any of the six instructions** (raw `program.methods.*()` calls, the `/instructions` subpath, or your own Borsh encoding) — the SDK cannot intercept those: * Add the inserted account at the positions in the table above, and gate on `isMarginfiV0110Live(programId)` if you need to keep sending to 0.1.9 until the flip. * `end_liquidation` gains an optional trailing `fee_payer` writable signer; when omitted, `liquidation_receiver` pays the flat fee as before. * Don't send 0.1.9-encoded **admin** instructions to 0.1.10: shorter args make the new program read garbage for the appended options. **Decoding accounts yourself:** nothing breaks — but a `Bank` in `CircuitBroken` state only decodes with the 0.1.10 IDL bundled in SDK `>= 2.7.0`. ## Troubleshooting [#troubleshooting] | Symptom | Cause | Fix | | ------------------------------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------ | | `AnchorError caused by account: group. AccountNotEnoughKeys` on a loop/swap | 0.1.9-style `end_flashloan` sent to a 0.1.10 program | Upgrade SDK to `>= 2.7.0` | | Same error from `simulateBundle` health simulation | 0.1.9-style `pulse_health` | Upgrade SDK to `>= 2.7.0` | | Signer / `system_program` mismatch on flash loans or account transfer before the flip | 0.1.10 layout sent to 0.1.9 (hand-rolled, or wrong timestamp) | Use the SDK builders, or gate on `isMarginfiV0110Live` | --- # Transactions Result types, ordering and atomicity, simulation, priority fees, oracle cranking, integration refreshes, lookup tables and size limits. Every `make*Tx` builder — whether you call it on `MarginfiAccountWrapper` or as a free Layer 3 function — returns fully built transactions and leaves signing and sending to you. This page is the single reference for what those results contain, why there is sometimes more than one transaction, and the helpers the builders use under the hood in case you need them yourself. It assumes you have a `client` and `wrappedAccount` from [Getting Started](/typescript-sdk/getting-started); for the canonical sign-and-send loop see [Deposit, Withdraw, Borrow & Repay](/typescript-sdk/lending-and-borrowing#sending). ## Result types [#result-types] ### Transactions with metadata [#transactions-with-metadata] Builders return plain web3.js transactions with a few extra fields attached (`addTransactionMetadata` does `Object.assign`): ```typescript type ExtendedTransactionProperties = { type: TransactionType; // DEPOSIT, BORROW, CRANK, LOOP, FLASHLOAN, ... signers?: Signer[]; // extra keypairs the SDK generated (e.g. temp wSOL account) addressLookupTables?: AddressLookupTableAccount[]; unitsConsumed?: number; }; type ExtendedTransaction = Transaction & ExtendedTransactionProperties; // legacy type ExtendedV0Transaction = VersionedTransaction & ExtendedTransactionProperties; type SolanaTransaction = ExtendedTransaction | ExtendedV0Transaction; ``` `makeDepositTx` and `makeRepayTx` return an `ExtendedTransaction` (legacy, `feePayer` set, **no blockhash** — set `recentBlockhash` before signing). `makeCreateMarginfiAccountTx` is legacy too; every other builder returns `ExtendedV0Transaction`s that already carry a blockhash and are compiled against lookup tables. Always pass `tx.signers` along when you sign. `TransactionType` is an enum (`DEPOSIT`, `WITHDRAW`, `BORROW`, `REPAY`, `CRANK`, `FLASHLOAN`, `LOOP`, `REPAY_COLLAT`, `DEPOSIT_STAKE`, ...) and `TransactionConfigMap[type].label(info?)` gives a human-readable label if you want to show progress in a UI. ### Builder results [#builder-results] `TransactionBuilderResult` (above) is what `makeBorrowTx`, `makeWithdrawTx`, `makeKaminoWithdrawTx`, `makeDriftWithdrawTx` and `makeJuplendWithdrawTx` return. Flash-loan based flows add fields: | Result type | Returned by | Extra fields | | ------------------------------ | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `TransactionBuilderResult` | `makeBorrowTx`, `makeWithdrawTx`, integration withdraws | — | | `FlashloanActionResult` | type only; declared for flash-loan flows | `txOverflown: boolean` — in practice current builders throw `TransactionBuildingError` (`SWAP_SIZE_EXCEEDED_*`) instead of returning an oversized transaction, so handle the error path ([Error Handling](/typescript-sdk/errors)) | | Loop / swap results | `makeLoopTx`, `makeSwapCollateralTx`, `makeSwapDebtTx`, `makeRollPtTx` | `quoteResponse?: SwapQuoteResult`, `mustBeAtomicBundle: boolean` | | `makeRepayWithCollatTx` result | `makeRepayWithCollatTx` | `swapQuote`, `amountToRepay`, `mustBeAtomicBundle` (no `actionTxIndex`) | | `BridgedTxResult` | `makeBridgedLoopTx`, `makeBridgedSwapCollateralTx`, `makeBridgedSwapDebtTx` | `quoteResponse`, `bridgeMint?: PublicKey` (set only when the double-hop path was used), `mustBeAtomicBundle` | | `TransferPositionsResult` | `makeTransferPositionsTx` | `destinationAccount: MarginfiAccountType`, `mustBeAtomicBundle` | | `BulkLendTxsResult` | `makeBulkWithdrawTx`, `makeBulkRepayTx` | `mustBeAtomicBundle` | ## Ordering and atomicity [#ordering-and-atomicity] Send `result.transactions` **in order** and confirm each one before the next. The transaction at `actionTxIndex` is the one whose signature you should surface to the user; a crank transaction that lands without the action is harmless (it only refreshes prices). When a result carries `mustBeAtomicBundle`, use it to decide how to send: * `false` — sequential sends are safe. Cranked Switchboard prices stay valid for at least about a minute, so a short gap between crank and action is fine. * `true` — the transactions must land together as **one atomic Jito bundle**. This is set when the action transaction contains integration refreshes (Kamino/Drift/Jupiter Lend state goes stale within a slot) or when a bridged double-hop swap has to land as one operation. Sending these sequentially can leave you with a half-executed position. ```typescript import { VersionedTransaction } from "@solana/web3.js"; const result = await wrappedAccount.makeBorrowTx(bankAddress, "100"); for (const tx of result.transactions) { if (!(tx instanceof VersionedTransaction)) { tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash; } const sig = await wallet.sendTransaction(tx, connection, { signers: tx.signers }); await connection.confirmTransaction(sig, "confirmed"); } ``` The SDK does not ship a bundle sender; sign the transactions and POST them to a Jito block engine. A bundle holds at most five transactions and needs a tip. `makeBundleTipIx(feePayer, lamports = 100_000)` builds a `SystemProgram.transfer` to a random Jito tip account — put it in a small extra transaction appended to the bundle (the builder transactions are already compiled), or prepend it to your own instructions if you build at Layer 2. ```typescript import { TransactionMessage, VersionedTransaction } from "@solana/web3.js"; import { makeBundleTipIx } from "@0dotxyz/p0-ts-sdk"; const result = await wrappedAccount.makeLoopTx({ ...loopParams }); if (!result.mustBeAtomicBundle) { // sequential is fine — see the other tab } // Sign the builder transactions (wallet adapter: wallet.signAllTransactions) const signed = result.transactions.map((tx) => { (tx as VersionedTransaction).sign([signer, ...(tx.signers ?? [])]); return tx as VersionedTransaction; }); // Tip transaction const { blockhash } = await connection.getLatestBlockhash(); const tipTx = new VersionedTransaction( new TransactionMessage({ payerKey: signer.publicKey, recentBlockhash: blockhash, instructions: [makeBundleTipIx(signer.publicKey, 100_000)], }).compileToLegacyMessage() ); tipTx.sign([signer]); const encoded = [...signed, tipTx].map((tx) => Buffer.from(tx.serialize()).toString("base64")); const res = await fetch("https://mainnet.block-engine.jito.wtf/api/v1/bundles", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "sendBundle", params: [encoded, { encoding: "base64" }], }), }); const { result: bundleId } = await res.json(); ``` ## Simulation [#simulation] `connection.simulateTransaction(tx)` only sees one transaction at a time, so a borrow that depends on a preceding oracle crank will fail simulation in isolation. Use `simulateBundle` to simulate the whole set against an RPC that supports the `simulateBundle` method (Jito block engines and Triton-backed endpoints do; plain public RPCs usually do not): ```typescript import { simulateBundle } from "@0dotxyz/p0-ts-sdk"; const results = await simulateBundle( connection.rpcEndpoint, result.transactions, [wrappedAccount.address] // optional: accounts to return post-execution (last tx only) ); results.forEach((r, i) => { if (r.err) console.error(`tx ${i} failed`, r.err, r.logs); else console.log(`tx ${i} ok, CU: ${r.unitsConsumed}`); }); ``` `simulateBundle(rpcEndpoint, transactions, includeAccounts?)` sends a `simulateBundle` JSON-RPC call with `skipSigVerify` and `replaceRecentBlockhash` enabled, so you can simulate unsigned transactions. It returns one `{ err?, logs, unitsConsumed?, postExecutionAccounts? }` entry per transaction. If the bundle as a whole fails, or the RPC rejects the call, it throws a `BundleSimulationError` (`name === "BundleSimulationError"`, with a `logs` array when available); the class itself is not exported, so match on `error.name`. ## Priority fees [#priority-fees] Simple builders (`makeDepositTx`, `makeBorrowTx`, `makeWithdrawTx`, `makeRepayTx`) do **not** add compute-budget instructions. The flash-loan flows (loop, swaps, repay-with-collateral, roll PT, transfer positions) already prepend `setComputeUnitLimit(1_200_000)` and a 1 micro-lamport `setComputeUnitPrice`; you should not add a second pair. The helpers live in `src/services/transaction/helpers/priority.ts`: | Helper | Use | | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | | `makePriorityFeeMicroIx(priorityFeeMicro?)` | **Preferred.** `setComputeUnitPrice` in micro-lamports per CU (default 1). | | `makePriorityFeeIx(priorityFeeUi?, computeUnitsLimit?)` | Deprecated. Takes a fee in SOL and converts it to micro-lamports; fees above 0.1 SOL are discarded. | | `makeTxPriorityIx(feePayer, feeUi, "BUNDLE" \| "RPC" \| "DYNAMIC")` | Deprecated. Returns `{ bundleTipIx?, priorityFeeIx }`. | | `makeBundleTipIx(feePayer, bundleTip = 100_000)` | Jito tip transfer (lamports) for bundles. | Because builder transactions are already compiled, the clean place to add a priority fee is when you build the transaction yourself from Layer 2 instructions: ```typescript import { makePriorityFeeMicroIx } from "@0dotxyz/p0-ts-sdk"; import { ComputeBudgetProgram } from "@solana/web3.js"; const ixs = await wrappedAccount.makeDepositIx(bankAddress, "250"); const tx = new Transaction().add( ComputeBudgetProgram.setComputeUnitLimit({ units: 200_000 }), makePriorityFeeMicroIx(50_000), // 50k micro-lamports per CU ...ixs.instructions ); ``` For a compiled `ExtendedV0Transaction` you can `decompileV0Transaction(tx, tx.addressLookupTables ?? [])`, prepend the fee instruction to `message.instructions`, and recompile with `compileToV0Message(tx.addressLookupTables)`. The constants `PRIORITY_TX_SIZE` (44 bytes) and `BUNDLE_TX_SIZE` (81 bytes) are the size budgets to reserve for a priority-fee or tip instruction appended at send time. ## Oracle cranking [#oracle-cranking] Pyth feeds are pushed externally and need no cranking. Most Project 0 banks use **Switchboard on-demand pull feeds**, which only update when someone submits a feed-update instruction. Actions that can reduce account health — **borrow, withdraw, liquidate** — run the on-chain health check against the feed's last price, so a stale feed makes them fail with `RiskEngineInitRejected`. The builders handle this with `makeSmartCrankSwbFeedIx`. It projects the account's balances after your instructions, then picks the minimum set of feeds to update: 1. every **liability** bank's oracle is always cranked; 2. **collateral** oracles are cranked only until the projected health covers the liabilities — not every deposit; 3. when the group rate limiter is on, every withdrawn bank's oracle is cranked because the program requires a fresh price for any withdraw. If a required feed cannot be updated (no Crossbar response, feed misconfigured) the builder throws `TransactionBuildingError` with code `ORACLE_CRANK_FAILED` listing the uncrankable banks. ```typescript import { makeSmartCrankSwbFeedIx } from "@0dotxyz/p0-ts-sdk"; const { instructions: crankIxs, luts: feedLuts } = await makeSmartCrankSwbFeedIx({ marginfiAccount: wrappedAccount.getUnderlyingAccount(), bankMap: client.bankMap, oraclePrices: client.oraclePriceByBank, assetShareValueMultiplierByBank: client.assetShareValueMultiplierByBank, instructions: borrowIxs.instructions, // the action you are about to send program: client.program, connection, crossbarUrl: "https://crossbar.0.xyz", // optional groupRateLimiterEnabled: isGroupRateLimiterEnabled(client.group.rateLimiter), }); ``` The crank instructions go into their own transaction (`type: TransactionType.CRANK`, compiled with `feedLuts`) that precedes the action transaction; when no feed is stale `crankIxs` is empty and the result has a single transaction. `makeSmartCrankSwbFeedIxForAccounts({ marginfiAccounts, ... })` is the multi-account variant used by transfer-positions flows. `crossbarUrl` selects the Switchboard Crossbar gateway that serves the signed price updates; the Layer 3 builders accept it as an optional parameter and fall back to `DEFAULT_CROSSBAR_URL` (`https://crossbar.0.xyz`) and then `DEFAULT_FALLBACK_CROSSBAR_URL` (`https://crossbar.switchboard.xyz`). Lower-level pieces — `makeUpdateSwbFeedIx({ swbPullOracles, feePayer, connection, crossbarUrl? })` and `computeSmartCrank(...)` — are exported too. See [Oracles](/protocol-overview/oracles) for the protocol side. ## Integration refreshes [#integration-refreshes] Banks backed by an external venue need that venue's state to be current in the **same transaction** as your action, which is why these instructions are prepended to the action transaction rather than sent separately (and why such results set `mustBeAtomicBundle`). The builders call: ```typescript import { makeRefreshIntegrationBanksIxs } from "@0dotxyz/p0-ts-sdk"; const refreshIxs = makeRefreshIntegrationBanksIxs( wrappedAccount.getUnderlyingAccount(), client.bankMap, [bank.address], // banks to exclude from Drift/JupLend updates (their CPI self-updates) and to include in the Kamino refresh client.bankIntegrationMap ); // refreshIxs.instructions, ordered kamino -> drift -> juplend ``` It composes three helpers you can also call individually, all with the signature `(marginfiAccount, bankMap, banks: PublicKey[], bankMetadataMap)` and returning an `InstructionsWrapper`: | Helper | What it emits | Note | | --------------------------- | ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | | `makeRefreshKaminoBanksIxs` | Kamino `refresh_reserve` + `refresh_obligation` for every Kamino bank the account holds **plus** the banks you pass | Kamino has no self-refreshing CPI, so the action bank must be included. | | `makeUpdateDriftMarketIxs` | Drift spot-market update for held Drift banks, **minus** the banks you pass | Drift deposit/withdraw update their own market via CPI. | | `makeUpdateJupLendRateIxs` | Jupiter Lend `update_rate` for held JupLend banks, minus the banks you pass | `juplend_deposit` / `juplend_withdraw` call `updateRate` internally. | The per-bank states these read (`kaminoStates`, `driftStates`, `jupLendStates`) come from `client.bankIntegrationMap`; see [Integration Banks & Native Stake](/typescript-sdk/integrations). ## Address lookup tables [#address-lookup-tables] Without LUTs every account costs 32 bytes and a transaction fits roughly 35 accounts; with LUTs accounts are 1-byte indexes, which is what makes health checks over many banks fit. `client.addressLookupTables` is the combined set the client loaded at `initialize()`: the general group tables from `ADDRESS_LOOKUP_TABLE_FOR_GROUP[groupPk]` followed by the native-stake tables from `ADDRESS_LOOKUP_TABLE_FOR_GROUP_NATIVE_STAKE[groupPk]` (both exported from `src/constants/luts.consts.ts`). Switchboard feed LUTs are fetched per crank and returned as `feedLuts`; swap builders add the aggregator's tables from the quote. The builders pass the whole array and let two helpers pick the subset to compile with: * `selectLutsForBanks(luts, banks)` — returns only the native-stake tables when every bank is `STAKED` or `SOL`, otherwise the general tables. * `selectLutsForAccountAction(luts, targetBank, balances, bankMap, extraBankAddresses?)` — same, but collects the target bank, the account's active-position banks and any extra health-check banks first. To build your own v0 transaction from instructions: ```typescript import { makeVersionedTransaction, splitInstructionsToFitTransactions, selectLutsForAccountAction, getTxSize, } from "@0dotxyz/p0-ts-sdk"; const luts = selectLutsForAccountAction( client.addressLookupTables, bank, wrappedAccount.balances, client.bankMap ); const { blockhash } = await connection.getLatestBlockhash(); // One transaction from a legacy Transaction's instructions const v0 = await makeVersionedTransaction(blockhash, legacyTx, wallet.publicKey, luts); console.log(getTxSize(v0)); // bytes // Or split a long instruction list across as many transactions as needed const txs = splitInstructionsToFitTransactions( [computeBudgetIx], // mandatory ixs repeated in every transaction actionIxs, { blockhash, payerKey: wallet.publicKey, luts, sizeMargin: 44, maxAccountLocks: 64 } ); ``` `splitInstructionsToFitTransactions` throws `"Single instruction too large to fit in a transaction"` if one instruction alone exceeds the limit. `isV0Tx(tx)`, `isFlashloan(tx)`, `decompileV0Transaction(tx, luts)` and `addTransactionMetadata(tx, props)` are exported alongside. ## Transaction size limits [#transaction-size-limits] From `src/constants/transaction.consts.ts`: | Constant | Value | Meaning | | ------------------- | ----- | ------------------------------------------------------------------------- | | `MAX_TX_SIZE` | 1232 | Maximum serialized transaction size in bytes. | | `MAX_ACCOUNT_LOCKS` | 64 | Maximum distinct accounts (static + LUT-resolved) a transaction may lock. | | `PRIORITY_TX_SIZE` | 44 | Bytes to reserve for a priority-fee instruction added at send time. | | `BUNDLE_TX_SIZE` | 81 | Bytes to reserve for a Jito tip instruction added at send time. | `getTxSize(tx)` returns the byte size of a legacy or versioned transaction (and logs a warning with a breakdown when a v0 transaction exceeds 1232); `getTotalAccountKeys(tx)` and `getWritableAccountKeys(tx)` count locks. Flash-loan flows size their swap leg against these limits before building; when a swap route cannot fit they throw `TransactionBuildingError` (`SWAP_SIZE_EXCEEDED_LOOP` / `_REPAY` / `_POSITION_SWAP`) rather than returning a transaction with `txOverflown` set — try a bridged variant or a route with fewer hops (see [Looping](/typescript-sdk/looping) and [Error Handling](/typescript-sdk/errors)). ## Related [#related] * [Architecture & API Layers](/typescript-sdk/architecture) — which layer adds which of these mechanics * [Deposit, Withdraw, Borrow & Repay](/typescript-sdk/lending-and-borrowing) — the canonical sign-and-send loop * [Error Handling](/typescript-sdk/errors) — `TransactionBuildingError` codes raised while building * [Oracles](/protocol-overview/oracles) — why Switchboard feeds need cranking * Examples: [`02-borrow.ts`](https://github.com/0dotxyz/p0-ts-sdk/tree/main/examples/02-borrow.ts) (`simulateBundle`), [`16a-loop.ts`](https://github.com/0dotxyz/p0-ts-sdk/tree/main/examples/16a-loop.ts) (`mustBeAtomicBundle`)