Current chapter: Chapter 6

Developers

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 fees
  • action: borrow or repay
  • collateralMint: native for SOL, or the exact active-cluster SPL collateral mint
  • collateralAmount: canonical raw base-unit string. Positive for borrow, exactly 0 for repay
  • debtAmount: 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
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

200 application/json
{
  "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 ceiling
  • lastValidBlockHeight: blockhash lifetime used when confirming the transaction
  • prioritizationFeeLamports: priority fee included by the builder, currently 0
  • quoteExpiresAt: 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

typescript
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.