Developers
Chapters
Integration Guide
POST a borrow intent, get back an unsigned transaction. No API key, no signup.
Borrow transaction API
POST /api/borrow builds and simulates an unsigned Solana v0 transaction for a borrow or repay action. The response follows the same serialized-transaction model used by swap APIs: your app decodes the base64 transaction, asks the requested wallet to sign it, and submits the exact transaction to Solana.
The endpoint is public, requires no API key, permits cross-origin POST requests, and never signs or submits on the caller's behalf. The server fetches the current oracle quote, assembles the protocol instructions, and simulates the completed transaction before returning it.
Request
userPublicKey: canonical Solana address that will sign and pay transaction feesaction:borroworrepaycollateralMint:nativefor SOL, or the exact active-cluster SPL collateral mintcollateralAmount: canonical raw base-unit string. Positive for borrow, exactly0for repaydebtAmount: positive canonical raw CCPU amount, using six decimals
All five fields are required. Extra fields, display-unit decimals, leading zeroes, inactive collateral mints, and values outside signed 64-bit range are rejected.
curl https://stable-mainnet.vercel.app/api/borrow \
-H 'Content-Type: application/json' \
-d '{
"userPublicKey": "YOUR_WALLET_ADDRESS",
"action": "borrow",
"collateralMint": "native",
"collateralAmount": "100000000",
"debtAmount": "5000000"
}'Response
{
"userPublicKey": "YOUR_WALLET_ADDRESS",
"action": "borrow",
"collateralMint": "native",
"collateralAmount": "100000000",
"debtAmount": "5000000",
"borrowTransaction": "AQAAAAAAAA...",
"lastValidBlockHeight": 123456789,
"prioritizationFeeLamports": 0,
"quoteExpiresAt": 1786651200000
}borrowTransaction: canonical base64-encoded unsigned v0 transaction, limited to Solana's transaction size ceilinglastValidBlockHeight: blockhash lifetime used when confirming the transactionprioritizationFeeLamports: priority fee included by the builder, currently0quoteExpiresAt: signed oracle quote expiry as Unix time in milliseconds
The request fields are echoed so consumers can bind the response to the original intent before opening a wallet prompt. The transaction requires exactly one signature: userPublicKey. Reject a response whose echoed fields or signer set do not match the request.
Sign and submit
import {
Connection,
VersionedTransaction,
} from "@solana/web3.js";
const intent = {
userPublicKey: wallet.publicKey.toBase58(),
action: "borrow",
collateralMint: "native",
collateralAmount: "100000000", // 0.1 SOL
debtAmount: "5000000", // 5 CCPU
};
const response = await fetch(
"https://stable-mainnet.vercel.app/api/borrow",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(intent),
},
);
if (!response.ok) {
const { error } = await response.json();
throw new Error(`borrow build failed: ${error}`);
}
const build = await response.json();
if (
build.userPublicKey !== intent.userPublicKey ||
build.action !== intent.action ||
build.collateralMint !== intent.collateralMint ||
build.collateralAmount !== intent.collateralAmount ||
build.debtAmount !== intent.debtAmount ||
Date.now() >= build.quoteExpiresAt
) {
throw new Error("response does not match the live intent");
}
const wireBytes = Uint8Array.from(
atob(build.borrowTransaction),
(character) => character.charCodeAt(0),
);
const transaction = VersionedTransaction.deserialize(wireBytes);
if (
transaction.message.header.numRequiredSignatures !== 1 ||
transaction.message.staticAccountKeys[0]?.toBase58() !== intent.userPublicKey
) {
throw new Error("unexpected transaction signer set");
}
const signed = await wallet.signTransaction(transaction);
const connection = new Connection(MAINNET_RPC_URL, "confirmed");
const signature = await connection.sendRawTransaction(signed.serialize());
await connection.confirmTransaction(
{
signature,
blockhash: transaction.message.recentBlockhash,
lastValidBlockHeight: build.lastValidBlockHeight,
},
"confirmed",
);Do not add, remove, or reorder instructions after the API returns the transaction. If the oracle quote or blockhash expires, discard the old bytes and call the endpoint again. Never retry by resubmitting an expired transaction.
Errors
Errors use a stable { "error": "code" } envelope. Validation returns invalid_request orbody_too_large. Capacity failures returnrate_limited or service_unavailable. Build failures return route_unavailable,oracle_unavailable, rpc_unavailable,transaction_too_large, or simulation_failed.