Project 0 Skill
name: project0
by borcherd · published 2026-03-22
$ claw add gh:borcherd/borcherd-project-0---
name: project0
version: 2.2.6
description: >
Permissionless DeFi yield and credit on Solana via the Project 0 (P0) protocol.
Deposit funds to earn yield across Solana's highest-yielding venues.
Borrow stablecoins against deposited collateral instead of selling crypto.
Execute advanced yield strategies via rate arbitrage and looping.
All operations are on-chain and permissionless -- no accounts, no approval process.
Note: This skill requires a wallet keypair to sign transactions. Use a dedicated
wallet with limited funds -- never expose your main private key. The agent will
always ask for confirmation before signing. Read-only operations need no keypair.
homepage: https://0.xyz
metadata:
openclaw:
requires:
env:
- RPC_URL
- WALLET_KEYPAIR
- WALLET_ADDRESS
- P0_ACCOUNT
- JUPITER_API_KEY
primaryEnv: RPC_URL
---
# Project 0 Skill
What is Project 0?
P0 is a permissionless DeFi prime broker on Solana. It provides unified margin
accounts that span multiple lending venues (P0 native, Kamino, Drift), giving
agents access to the best yields and deepest liquidity across the ecosystem.
All operations are on-chain, non-custodial, and require no signup.
**Documentation:**
Use Case 1: Yield + Credit
Deposit funds to earn the best yield across DeFi. When the user needs
liquidity (e.g. to make a purchase), borrow stablecoins against deposited
collateral instead of selling crypto. The user keeps earning yield while
accessing cash.
**Flow:** Deposit -> Earn yield -> Borrow stablecoins when needed -> Repay.
**Example prompts:**
Use Case 2: Advanced Yield Strategies
Find the absolute best yields on Solana, which often come from rate arbitrage
and looping strategies. These involve depositing one asset, borrowing another,
and managing the leveraged position. Higher yields, but requires active health
monitoring.
**Flow:** Identify strategy -> Deposit collateral -> Borrow -> Monitor health.
**Example prompts:**
---
Agent Workflow
When a user asks to earn yield, deposit, borrow, or manage positions on P0,
follow these steps in order. Do NOT skip ahead to writing code.
Step 1: Check wallet balances
Resolve the wallet address: check `.env` for `WALLET_ADDRESS`, or derive
it from the keypair if `WALLET_KEYPAIR` is set, or ask the user.
Fetch the wallet's token holdings from the wallet API (see Wallet endpoint
below). No credentials needed. Use the `address` field from each token to
match against P0 banks by `mint`.
Step 2: Fetch P0 data
Fetch banks and strategies from the P0 APIs (see Read-Only APIs below).
Step 3: Recommend optimal action
Determine which use case fits the user's request:
deposit opportunity and mention borrowing as an option (Use Case 1).
rate arbs, or looping, recommend the best strategy and explain the
deposit + borrow setup required (Use Case 2).
Match wallet holdings to available bank yields. For each token the wallet
holds, find the best deposit APY from the banks data. Compare across all
holdings to find the highest overall yield.
If the wallet holds tokens that have no corresponding P0 bank (e.g.
memecoins), suggest swapping them into a supported token.
If swapping to a different token would yield significantly more, recommend
the swap and explain the tradeoff.
Consider depositing multiple assets if the wallet holds several supported
tokens -- not just the single highest-yield one.
**Present the plan to the user with specific numbers before executing.**
Example: _"Your wallet holds 0.09 bbSOL (~$18) and 0.10 SOL (~$15). bbSOL
earns 15.2% APY on P0 vs SOL at 4.9%. I recommend depositing your bbSOL
for the higher yield. Shall I proceed?"_
Step 4: Collect credentials
Before executing on-chain operations, the agent needs an RPC URL and a
wallet keypair.
**RPC URL:** Check `.env` for `RPC_URL`, `SOLANA_RPC_URL`, or `HELIUS_RPC_URL`.
If not found, ask the user: _"I need a paid Solana RPC URL to execute
transactions. (Helius has a free tier at https://www.helius.dev)"_
**Wallet address:** Check `.env` for `WALLET_ADDRESS`. If set, use it for
read-only operations (wallet balances, account discovery) without needing
the keypair. The keypair is only required when signing transactions.
**Wallet keypair:** If the user provided a keypair path in their message,
use it directly. Otherwise check `.env` for `WALLET_KEYPAIR`. If neither,
ask: _"I need a Solana keypair to sign transactions. Set `WALLET_KEYPAIR`
in your `.env` to the file path, or tell me where your keypair JSON file
is."_
**P0 account (optional):** Check `.env` for `P0_ACCOUNT`. If set, use it
directly when loading the account (skip discovery/prompt). If not set,
follow the account discovery logic in "Create or load account" below.
Do not fabricate URLs, file paths, or account addresses. Wait for the user
to provide real values.
Step 5: Collect swap credentials (only if needed)
If the plan from step 3 involves swapping tokens, the agent needs a Jupiter
API key.
Check for an existing key in the environment:
or `.env` file
If no key is found, ask the user:
_"I need a Jupiter API key for the token swap. Get a free one at
https://portal.jup.ag (60 req/min)"_
If no swap is needed, skip this step entirely.
Step 6: Execute
Execute the plan from step 3. Swap first if needed (see Swapping Tokens
section), then deposit, borrow, etc. Report results with Solscan links:
`https://solscan.io/tx/${signature}`
---
Read-Only: Fetching Data from P0
Use the public HTTP APIs for read-only tasks. No SDK, no wallet, no RPC needed.
Banks endpoint
`GET https://ai.0.xyz/api/banks`
Returns every lending pool (bank) with rates, metadata, and pricing. Only
collateral-tier banks are included (isolated banks are filtered out). The
response is a lightweight projection (9 fields per bank) with deposit APY
pre-computed.
const res = await fetch("https://ai.0.xyz/api/banks");
const banks = await res.json();**Fields per bank:**
| Field | Description |
| --------------- | ----------------------------------------------------------------------------- |
| `bank_address` | On-chain bank address (use with SDK `client.getBank()`) |
| `symbol` | Token symbol (SOL, USDC, JitoSOL, ...) |
| `mint` | Token mint address |
| `mint_decimals` | Token decimal places (9 for SOL, 6 for USDC) |
| `venue` | Lending venue (P0, Kamino, Drift) |
| `deposit_apy` | Effective deposit APY as percentage (pre-computed, includes underlying yield) |
| `borrow_apy` | Borrow APY as percentage |
| `usd_price` | Oracle price in USD |
| `token_program` | Token program (TOKEN_PROGRAM_ID or TOKEN_2022) |
The `deposit_apy` field already includes underlying token yield (e.g. LST staking
rates). No manual computation needed -- just sort by `deposit_apy` descending.
**Borrowing is only available on P0 venue banks.** Kamino and Drift banks are
deposit-only -- you can earn yield on them but cannot borrow from them. When
looking for borrow opportunities, filter to `venue === "P0"`.
**Finding best deposit yields:**
Sort banks by `deposit_apy` descending.
**Finding cheapest stablecoin borrows:**
Filter where `venue` is `"P0"` and `symbol` is in `[USDC, USDT, USDG, USDS, HYUSD]`,
sort by `borrow_apy` ascending.
**Matching wallet holdings to banks:** Use the `mint` field to match tokens in
the wallet to available banks. A wallet token account's mint address corresponds
to a bank's `mint` field.
Strategies endpoint
`GET https://ai.0.xyz/api/strategies`
Returns precomputed strategies showing the best deposit/borrow combinations
with projected APYs. Useful for finding which token to deposit and which to
borrow for the best spread.
const res = await fetch("https://ai.0.xyz/api/strategies");
const strategies = await res.json();**Fields per strategy:**
| Field | Description |
| ---------------------- | ----------------------------------------------------- |
| `heading` | Human-readable name (e.g. "bbSOL/SOL Rate Arbitrage") |
| `primaryBankAddress` | Bank to deposit into |
| `secondaryBankAddress` | Bank to borrow from |
| `spread` | Rate spread between deposit and borrow |
| `leverage` | Leverage multiplier |
| `apy` | Projected APY (decimal, e.g. 0.085 = 8.5%) |
The endpoint returns the top strategies sorted by APY descending. The `heading`
field describes the strategy type (e.g. "Rate Arbitrage", "Loop", etc.).
**Using strategies to plan deposits and borrows:**
Strategies show which deposit/borrow pairs have the best spread. The agent can
execute these as separate deposit + borrow operations:
// Strategies are already sorted by APY descending
const best = strategies[0];
// best.primaryBankAddress = bank to deposit into
// best.secondaryBankAddress = bank to borrow from
// Execute as: deposit into primaryBank, then borrow from secondaryBank**Connecting strategies to banks:** Use `primaryBankAddress` and
`secondaryBankAddress` to look up bank details from the banks endpoint.
Wallet endpoint
`GET https://ai.0.xyz/api/wallet/{address}`
Returns all token holdings for a Solana wallet with balances and USD values.
No API key or RPC needed. Tokens are sorted by USD value descending.
const res = await fetch(`https://ai.0.xyz/api/wallet/${walletAddress}`);
const data = await res.json();
// data.wallet, data.total_usd_value, data.tokens[]Returns `{ wallet, total_usd_value, tokens[] }` where each token has:
| Field | Description |
| ----------- | ---------------------------------------- |
| `address` | Token mint address (matches bank `mint`) |
| `symbol` | Token symbol (SOL, USDC, bbSOL, ...) |
| `name` | Token name |
| `decimals` | Token decimal places |
| `balance` | Human-readable balance (UI amount) |
| `usd_price` | Price per token in USD |
| `usd_value` | Total USD value of this holding |
---
On-Chain: Interacting with the Protocol
Use the TypeScript SDK for actions that require signing: create account, deposit,
withdraw, borrow, repay. Requires a Solana keypair and user authorization.
Prerequisites
(see Agent Workflow step 4)
read-only operations (see Agent Workflow step 4)
provided by user in their message (see Agent Workflow step 4)
account selection (see Agent Workflow step 4)
for swaps (see Agent Workflow step 5)
Wallet setup
The agent needs a Solana keypair to sign transactions. Resolve the path
using this priority:
1. If the user provided a path in their message, use it directly
2. Check `.env` for `WALLET_KEYPAIR`
3. If neither, ask: _"I need a Solana keypair to sign transactions. Set
`WALLET_KEYPAIR` in your `.env` to the file path, or tell me where
your keypair JSON file is."_
import { Keypair } from "@solana/web3.js";
import fs from "fs";
// keypairPath from user message, or WALLET_KEYPAIR from .env
const wallet = Keypair.fromSecretKey(
Uint8Array.from(JSON.parse(fs.readFileSync(keypairPath, "utf-8"))),
);To generate a new keypair:
solana-keygen new --outfile ~/.config/solana/id.json
# Then add to .env: WALLET_KEYPAIR=~/.config/solana/id.jsonInstall
npm install @0dotxyz/p0-ts-sdk`@solana/web3.js` (1.98.4) is bundled as a direct dependency -- no need to
install it separately.
Full SDK documentation: https://docs.0.xyz/typescript-sdk/overview
Initialize client
import { Connection } from "@solana/web3.js";
import { Project0Client, getConfig } from "@0dotxyz/p0-ts-sdk";
// RPC_URL from .env or provided by user
const connection = new Connection(RPC_URL, "confirmed");
const config = getConfig("production");
const client = await Project0Client.initialize(connection, config);Reuse the client instance. Do not reinitialize per operation.
Create or load account
A P0 account (MarginfiAccount) is an on-chain PDA that holds positions.
One wallet can own multiple accounts via different `accountIndex` values.
Each account has isolated positions and risk — collateral in one account
is NOT accessible from another.
import { PublicKey } from "@solana/web3.js";
let wrappedAccount: MarginfiAccountWrapper;
// If P0_ACCOUNT is set in .env, use it directly
if (process.env.P0_ACCOUNT) {
wrappedAccount = await client.fetchAccount(
new PublicKey(process.env.P0_ACCOUNT),
);
} else {
const accountAddresses = await client.getAccountAddresses(wallet.publicKey);
if (accountAddresses.length === 0) {
// No accounts -- create one
const createTx = await client.createMarginfiAccountTx(
wallet.publicKey,
0, // accountIndex
);
createTx.recentBlockhash = (
await connection.getLatestBlockhash()
).blockhash;
createTx.sign(wallet);
const createSig = await connection.sendRawTransaction(createTx.serialize());
await connection.confirmTransaction(createSig, "confirmed");
const created = await client.getAccountAddresses(wallet.publicKey);
wrappedAccount = await client.fetchAccount(created[0]!);
} else if (accountAddresses.length === 1) {
// Single account -- use it
wrappedAccount = await client.fetchAccount(accountAddresses[0]!);
} else {
// Multiple accounts -- ask the user which to use
// Present: "You have N P0 accounts:
// 1) 3p16...s4aq
// 2) 7xKm...2fNp
// Which account should I use?"
wrappedAccount = await client.fetchAccount(chosenAddress);
}
} // end P0_ACCOUNT else**IMPORTANT — persist the account address.** If you deposited in a previous
step, store `wrappedAccount.address` and reuse it for subsequent operations
(borrow, withdraw, repay). Do NOT re-discover accounts and blindly pick
`accountAddresses[0]` — it may be a different account than the one you
deposited into.
---
Core Operations
Deposit
Returns a legacy transaction. Amounts are in UI units (human-readable numbers).
**SOL gas reserve:** When depositing SOL, always keep at least **0.01 SOL** in
the wallet for transaction fees. Depositing the entire SOL balance will cause
subsequent transactions to fail.
const depositTx = await wrappedAccount.makeDepositTx(bankAddress, 100);
depositTx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
depositTx.sign(wallet);
const sig = await connection.sendRawTransaction(depositTx.serialize());
await connection.confirmTransaction(sig, "confirmed");Withdraw
Returns a versioned transaction bundle. Send each tx sequentially.
const withdrawResult = await wrappedAccount.makeWithdrawTx(
bankAddress,
50,
false, // set true to withdraw all
);
for (const tx of withdrawResult.transactions) {
tx.sign([wallet]);
const sig = await connection.sendRawTransaction(tx.serialize());
await connection.confirmTransaction(sig, "confirmed");
}Borrow
Returns a versioned transaction bundle (may include oracle crank txs).
**Pre-borrow checks are mandatory.** Skipping these will result in failed
transactions (program error 6009) or wasted fees.
// 1. Verify this account has collateral
if (wrappedAccount.activeBalances.length === 0) {
throw new Error("Account has no deposits -- deposit collateral first");
}
// 2. Check max borrow capacity for this bank
const maxBorrow = wrappedAccount.computeMaxBorrowForBank(bankAddress);
if (maxBorrow.isZero()) {
throw new Error(
"No borrow capacity -- check this is the correct account and it has " +
"sufficient collateral. If the wallet has multiple accounts, ask the " +
"user which one to use.",
);
}
// 3. Clamp to max (leave 5% buffer for price movement)
const amount = Math.min(desiredAmount, maxBorrow.toNumber() * 0.95);
// 4. Build and send (verify each tx -- see Transaction Verification below)
const borrowResult = await wrappedAccount.makeBorrowTx(bankAddress, amount);
for (const tx of borrowResult.transactions) {
tx.sign([wallet]);
const sig = await connection.sendRawTransaction(tx.serialize());
const confirmation = await connection.confirmTransaction(sig, "confirmed");
if (confirmation.value.err) {
throw new Error(
`Borrow tx failed: ${JSON.stringify(confirmation.value.err)}`,
);
}
}Repay
Returns a legacy transaction.
// Repay specific amount
const repayTx = await wrappedAccount.makeRepayTx(bankAddress, 50, false);
// Repay all debt
const repayAllTx = await wrappedAccount.makeRepayTx(bankAddress, 0, true);
repayTx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
repayTx.sign(wallet);
const sig = await connection.sendRawTransaction(repayTx.serialize());
await connection.confirmTransaction(sig, "confirmed");Transaction verification
After sending any transaction, always verify it succeeded on-chain.
`confirmTransaction` only confirms the tx was included in a block — it does
NOT check whether the program executed successfully. A transaction can be
"confirmed" but still fail at the program level (e.g., error 6009:
"RiskEngine rejected").
const sig = await connection.sendRawTransaction(tx.serialize());
const confirmation = await connection.confirmTransaction(sig, "confirmed");
// CRITICAL -- check for program-level errors
if (confirmation.value.err) {
throw new Error(
`Transaction failed: ${JSON.stringify(confirmation.value.err)}` +
`\nhttps://solscan.io/tx/${sig}`,
);
}
// Only report success AFTER this check passes
console.log(`Success: https://solscan.io/tx/${sig}`);**Never report a transaction as successful without checking
`confirmation.value.err`.** If `err` is not null, the transaction landed
on-chain but the program rejected it — the user's funds were not moved.
**Retry policy:** If a transaction fails, do NOT retry more than once.
Report the failure with the Solscan link and the error message, and let the
user decide how to proceed.
---
Swapping Tokens via Jupiter
If the wallet holds a different token than what a strategy requires (e.g.
holds SOL but the best yield is for bbSOL), swap first using the Jupiter API.
**Jupiter requires an API key.** Check `.env` for `JUPITER_API_KEY` or
`JUP_API_KEY`. If not found, ask the user: _"I need a Jupiter API key for
the token swap. Get a free one at https://portal.jup.ag (60 req/min)"_
Get a quote and execute swap
import { VersionedTransaction } from "@solana/web3.js";
// JUP_API_KEY from .env or provided by user
const inputMint = "So11111111111111111111111111111111111111112"; // SOL
const outputMint = "Bybit2vBJGhPF52GBdNaQfUJ6ZpThSgHBobjWZpLPb4B"; // bbSOL
const amount = 100000000; // raw integer units (0.1 SOL = 100000000 lamports)
// 1. Get quote
const quoteResponse = await (
await fetch(
`https://api.jup.ag/swap/v1/quote?inputMint=${inputMint}` +
`&outputMint=${outputMint}` +
`&amount=${amount}` +
`&slippageBps=50` +
`&restrictIntermediateTokens=true`,
{ headers: { "x-api-key": JUP_API_KEY } },
)
).json();
// 2. Build swap transaction
const swapResponse = await (
await fetch("https://api.jup.ag/swap/v1/swap", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": JUP_API_KEY,
},
body: JSON.stringify({
quoteResponse,
userPublicKey: wallet.publicKey.toBase58(),
dynamicComputeUnitLimit: true,
dynamicSlippage: true,
prioritizationFeeLamports: {
priorityLevelWithMaxLamports: {
maxLamports: 1000000,
priorityLevel: "veryHigh",
},
},
}),
})
).json();
// 3. Sign and send
const swapTx = VersionedTransaction.deserialize(
Buffer.from(swapResponse.swapTransaction, "base64"),
);
swapTx.sign([wallet]);
const sig = await connection.sendRawTransaction(swapTx.serialize());
await connection.confirmTransaction(sig, "confirmed");
console.log(`Swap: https://solscan.io/tx/${sig}`);`amount` is in raw integer units (lamports for SOL, smallest unit for SPL).
Use `mint_decimals` from the banks API to convert.
---
Health Monitoring & Portfolio
Checking portfolio positions
import { MarginRequirementType } from "@0dotxyz/p0-ts-sdk";
// List all active positions
for (const balance of wrappedAccount.activeBalances) {
const bank = client.getBank(balance.bankPk);
if (!bank) continue;
const bankAddr = balance.bankPk.toBase58();
const multiplier = client.assetShareValueMultiplierByBank.get(bankAddr);
const qty = balance.computeQuantityUi(bank, multiplier);
// Optionally fetch bank metadata from banks API for symbol/venue labels
if (qty.assets.gt(0))
console.log(`Deposit: ${qty.assets.toFixed(4)} ${bankAddr}`);
if (qty.liabilities.gt(0))
console.log(`Borrow: ${qty.liabilities.toFixed(4)} ${bankAddr}`);
}
// Account-level metrics
const health = wrappedAccount.computeHealthComponentsFromCache(
MarginRequirementType.Maintenance,
);
const healthFactor = health.assets.dividedBy(health.liabilities).toNumber();
console.log(
`Account value: $${wrappedAccount.computeAccountValue().toFixed(2)}`,
);
console.log(`Health factor: ${healthFactor.toFixed(2)}`);
console.log(
`Free collateral: $${wrappedAccount.computeFreeCollateralFromCache().toFixed(2)}`,
);
console.log(`Net APY: ${(wrappedAccount.computeNetApy() * 100).toFixed(2)}%`);Health factor reference
| Health Factor | Status |
| ------------- | ------------------------------- |
| > 2.0 | Healthy |
| 1.1 - 2.0 | Monitor closely |
| 1.0 - 1.1 | Danger -- repay or deposit more |
| < 1.0 | Liquidatable |
---
Error Reference
| Error | Cause | Resolution |
| ------------------------------ | ---------------------------------------------- | -------------------------------------------------------- |
| `Bank not found` | No bank for the given address | Verify bank address from banks API |
| `Insufficient free collateral` | Not enough collateral to borrow | Deposit more or borrow less |
| `Simulation failed` | Transaction would fail on-chain | Check logs -- often stale oracle or insufficient balance |
| `Transaction expired` | Blockhash expired before confirmation | Retry with fresh blockhash |
| `Account not found` | P0 account address does not exist | Verify address or create a new account |
| `Program error 6009` | RiskEngine rejected (bad health/stale oracles) | Verify correct account has collateral; check health |
| `429 Too Many Requests` | RPC rate limited | Use a paid RPC provider |
More tools from the same signal band
Order food/drinks (点餐) on an Android device paired as an OpenClaw node. Uses in-app menu and cart; add goods, view cart, submit order (demo, no real payment).
Sign plugins, rotate agent credentials without losing identity, and publicly attest to plugin behavior with verifiable claims and authenticated transfers.
The philosophical layer for AI agents. Maps behavior to Spinoza's 48 affects, calculates persistence scores, and generates geometric self-reports. Give your...