Trading API

Overview

The token-pair endpoints accept a baseTokenMint instead of an explicit offerPublicKey. The server automatically selects the best matching liquidity pool (offer) — you never need to query offers directly.

For new integrations, always use these endpoints. The offerPublicKey endpoints (POST /positions/open) exist for advanced use cases but require you to discover and manage offer addresses manually.

Get a Quote

POST /api/v1/positions/quote-by-token

Get a price preview without building a transaction. Use this to display expected entry price, slippage, and fees before asking the user to confirm.

Request:

{
  "baseTokenMint": "So11111111111111111111111111111111111111112",
  "userPublicKey": "GsbwXfJraMomNxBcjK7xK2xQx5MQgQx4Ld8QkLeNmA3v",
  "collateralAmount": "1000000000",
  "leverage": 3,
  "side": "LONG",
  "quoteTokenMint": "So11111111111111111111111111111111111111112",
  "slippageBps": 50
}
FieldTypeRequiredDescription
baseTokenMintstringYesToken mint you want to go long/short on (base58)
userPublicKeystringYesUser's wallet address (base58)
collateralAmountstringYesCollateral in the quote token's smallest units (lamports for SOL)
leveragenumberYesMultiplier between 1.1 and 100
sidestringYes"LONG" or "SHORT"
quoteTokenMintstringNoQuote token (default: WSOL for LONG, USDC for SHORT)
slippageBpsnumberNoSlippage tolerance in bps (default: 50, max: 10000)

Response: Returns a SwapQuote object:

{
  "inAmount": "1000000000",
  "outAmount": "2950000000",
  "priceImpactPct": "0.12",
  "otherAmountThreshold": "2920500000",
  "inputMint": "So11111111111111111111111111111111111111112",
  "outputMint": "So11111111111111111111111111111111111111112",
  "slippageBps": 50
}

Open a Position

POST /api/v1/positions/open-by-token

Build an unsigned Solana transaction to open a leveraged position. Takes the same request shape as quote-by-token.

Request: Identical to quote-by-token above.

Response:

{
  "transaction": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAkP...",
  "positionAddress": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
  "lastValidBlockHeight": 285432100,
  "quote": { "outAmount": "9950000000", "priceImpactPct": 0.12, "slippageBps": 50 }
}

Deserialise with VersionedTransaction.deserialize(bs58.decode(response.transaction)), sign with the user's wallet, and submit.

How Offer Matching Works

When you call open-by-token, the server:

  1. Queries active offers for the requested token pair and side
  2. Filters by available liquidity (must cover collateralAmount × (leverage - 1))
  3. Sorts by best terms (highest leverage limit, lowest utilization)
  4. Selects the top offer and builds the transaction against it

If no matching offer exists, the server returns 404 NO_OFFER_FOR_TOKEN.

Supported Sides

SideCollateral TokenWhat Happens
LONGWSOL (default) or USDCProtocol borrows SOL/USDC, swaps into the target token. Profit when price rises.
SHORTUSDCProtocol borrows the volatile token, swaps to USDC. Profit when price falls.

Short position support is currently in beta. Contact https://lavarage.xyz/partners for access.

Monitor Positions

GET /api/v1/positions — List positions filtered by owner wallet or status.

curl "https://api.lavarage.xyz/api/v1/positions?owner=GsbwXfJraMomNxBcjK7xK2xQx5MQgQx4Ld8QkLeNmA3v&status=OPEN" \
  -H "x-api-key: YOUR_KEY"

Query parameters:

  • owner — filter by wallet address
  • statusOPEN, CLOSED, or ALL (default: ALL)
  • sideLONG, SHORT, or BORROW
  • nodeWallet — filter by vault/node wallet address
  • limit — max results (default: 50, max: 250)
  • offset — pagination offset

Close a Position

Two-step process: get a quote first, then build the transaction.

Step 1 — Get close quote:

curl -X POST https://api.lavarage.xyz/api/v1/positions/close-quote \
  -H "x-api-key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "positionAddress": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
    "userPublicKey": "GsbwXfJraMomNxBcjK7xK2xQx5MQgQx4Ld8QkLeNmA3v"
  }'

Returns a CloseQuote with repayAmount, fee, and swap details.

Step 2 — Build close transaction:

curl -X POST https://api.lavarage.xyz/api/v1/positions/close \
  -H "x-api-key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "positionAddress": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
    "userPublicKey": "GsbwXfJraMomNxBcjK7xK2xQx5MQgQx4Ld8QkLeNmA3v",
    "slippageBps": 50
  }'

Returns { transaction, lastValidBlockHeight, quote }.


MEV Protection

Solana transactions that include Jupiter swaps are vulnerable to sandwich attacks — a bot detects your pending swap, front-runs it to move the price, and back-runs to profit from the difference. This costs traders real money on every trade.

Lavarage integrates with Astralane to submit transactions through a MEV-protected gateway that prevents sandwich attacks.

How It Works

  1. Build the transaction with a tip — pass astralaneTipLamports when calling /positions/open or /positions/close
  2. Sign the transaction client-side with the user's wallet
  3. Submit via Astralane — send to POST /api/v1/bundle/submit instead of regular Solana RPC

The server fans out your transaction to multiple geo-distributed Astralane endpoints (NY, FR, SG, JP, AMS, LIM, LA) and returns the first success.

Step-by-Step: Open Position with MEV Protection

1. Get the recommended tip amount

GET /api/v1/bundle/tip

Response:

{ "tipLamports": 1200000 }

2. Build the transaction with the tip

POST /api/v1/positions/open
{
  "offerPublicKey": "7xS2gz2bTp3fwCC7knJvUWTEU9Tycczu6VhJYKgi1wdz",
  "userPublicKey": "YOUR_WALLET_ADDRESS",
  "collateralAmount": "1000000000",
  "leverage": 3,
  "side": "LONG",
  "slippageBps": 50,
  "astralaneTipLamports": 10000
}

The returned transaction automatically includes a transfer to the Astralane tip wallet.

3. Sign the transaction

Sign the returned transaction (base58-encoded VersionedTransaction) with the user's wallet. Decode from base58, sign, and re-encode to base64.

4. Submit via MEV-protected gateway

POST /api/v1/bundle/submit
{
  "transaction": "BASE64_ENCODED_SIGNED_TX",
  "mevProtect": true
}

Response:

{ "result": "5xYz...txSignature" }

Closing Positions with MEV Protection

Same flow — add astralaneTipLamports to the close request:

POST /api/v1/positions/close
{
  "positionAddress": "...",
  "userPublicKey": "...",
  "slippageBps": 50,
  "astralaneTipLamports": 10000
}

Sign and submit via /bundle/submit with mevProtect: true.

Partial Sell (Jito Bundle)

Partial sells use a Jito bundle (multiple transactions executed atomically) instead of a single MEV-protected transaction:

  1. POST /positions/partial-sell — returns splitTransaction + closeTransaction
  2. Build a tip transaction (SOL transfer to a Jito tip account)
  3. Sign all 3 transactions
  4. Submit as a bundle:
POST /api/v1/bundle
{
  "transactions": ["signedTipTx", "signedSplitTx", "signedCloseTx"]
}

Tip Amounts

EndpointDescription
GET /api/v1/bundle/tipReturns the current Jito tip floor (99th percentile x 1.2, cached 10s, minimum 1M lamports)

The tip is paid by the user as part of the transaction. Higher tips improve landing probability during network congestion. The minimum astralaneTipLamports is 10,000 lamports (0.00001 SOL).

When to Use MEV Protection

ScenarioRecommendation
Large trades (> $1K)Always use MEV protection
Small trades (< $100)Optional — tip cost may exceed MEV savings
High-volatility tokensRecommended — more attractive to sandwich bots
Stablecoin pairsLower risk — less profitable for bots

Without MEV Protection

If you don't pass astralaneTipLamports, the returned transaction has no tip instruction. Sign and submit directly to a Solana RPC endpoint as usual. This is simpler but leaves the transaction exposed to MEV.


Transaction Confirmation

Every transaction-building endpoint returns lastValidBlockHeight alongside the transaction. Use it to detect transaction expiry without indefinite polling.

import { Connection, VersionedTransaction } from '@solana/web3.js'
import bs58 from 'bs58'

const connection = new Connection('https://api.mainnet-beta.solana.com', 'confirmed')

const { transaction, lastValidBlockHeight } = await buildTx(...)
const tx = VersionedTransaction.deserialize(bs58.decode(transaction))
const signed = await wallet.signTransaction(tx)

const sig = await connection.sendRawTransaction(signed.serialize(), {
  skipPreflight: false,
  preflightCommitment: 'confirmed',
})

// Wait with expiry detection
const result = await connection.confirmTransaction(
  { signature: sig, lastValidBlockHeight, blockhash: tx.message.recentBlockhash },
  'confirmed',
)

if (result.value.err) {
  throw new Error(`Transaction failed: ${JSON.stringify(result.value.err)}`)
}
console.log('Confirmed:', sig)

Handling Expiry

A Solana transaction expires after ~90 seconds if not included in a block. If confirmTransaction returns with TransactionExpiredBlockheightExceededError, the transaction was never executed and it is safe to retry.

import { TransactionExpiredBlockheightExceededError } from '@solana/web3.js'

try {
  await connection.confirmTransaction({ signature, lastValidBlockHeight, blockhash }, 'confirmed')
} catch (err) {
  if (err instanceof TransactionExpiredBlockheightExceededError) {
    // Safe to retry — transaction was never executed
    console.warn('Transaction expired, rebuilding...')
    // Call the API again to get a fresh transaction
  } else {
    throw err
  }
}

Retry Strategy

  1. Build transaction via API → get transaction + lastValidBlockHeight
  2. Sign and submit
  3. Confirm with expiry detection
  4. If expired: rebuild from API (new blockhash), sign, submit
  5. If confirmed but on-chain error: inspect error, fix parameters, do not retry blindly

Do not resubmit the same signed transaction after expiry — always rebuild via the API to get a fresh blockhash.


Code Examples

TypeScript: Open a 3x Long SOL Position

Full working example using the native fetch API. No SDK required.

import { VersionedTransaction, Connection } from '@solana/web3.js'
import bs58 from 'bs58'

const API_BASE = 'https://api.lavarage.xyz'
const API_KEY = process.env.LAVARAGE_API_KEY!

interface OpenByTokenRequest {
  baseTokenMint: string
  userPublicKey: string
  collateralAmount: string
  leverage: number
  side: 'LONG' | 'SHORT'
  slippageBps?: number
  quoteTokenMint?: string
}

async function openLeveragedPosition(
  wallet: { publicKey: string; signTransaction: (tx: VersionedTransaction) => Promise<VersionedTransaction> },
  params: OpenByTokenRequest,
): Promise<string> {
  // Step 1: Build the unsigned transaction
  const res = await fetch(`${API_BASE}/api/v1/positions/open-by-token`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': API_KEY,
    },
    body: JSON.stringify(params),
  })

  if (!res.ok) {
    const err = await res.json()
    throw new Error(`API error ${err.code}: ${err.message}`)
  }

  const { transaction } = await res.json()

  // Step 2: Deserialise, sign, submit
  const txBytes = bs58.decode(transaction)
  const tx = VersionedTransaction.deserialize(txBytes)
  const signedTx = await wallet.signTransaction(tx)

  const connection = new Connection('https://api.mainnet-beta.solana.com', 'confirmed')
  const signature = await connection.sendRawTransaction(signedTx.serialize(), {
    skipPreflight: false,
    preflightCommitment: 'confirmed',
  })

  await connection.confirmTransaction(signature, 'confirmed')
  return signature
}

// Usage
const signature = await openLeveragedPosition(wallet, {
  baseTokenMint: 'So11111111111111111111111111111111111111112', // SOL
  userPublicKey: wallet.publicKey,
  collateralAmount: '1000000000', // 1 SOL in lamports
  leverage: 3,
  side: 'LONG',
  slippageBps: 50, // 0.5%
})

console.log('Position opened:', signature)

TypeScript: Full Position Lifecycle

// 1. Open
const openSig = await openLeveragedPosition(wallet, { ... })

// 2. Poll positions
const posRes = await fetch(
  `${API_BASE}/api/v1/positions?owner=${wallet.publicKey}&status=OPEN`,
  { headers: { 'x-api-key': API_KEY } }
)
const positions = await posRes.json()
const position = positions[0]

// 3. Get close quote
const quoteRes = await fetch(`${API_BASE}/api/v1/positions/close-quote`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'x-api-key': API_KEY },
  body: JSON.stringify({
    positionAddress: position.address,
    userPublicKey: wallet.publicKey,
  }),
})
const quote = await quoteRes.json()
console.log('Close quote — repay:', quote.repayAmount, 'fee:', quote.fee)

// 4. Build close transaction
const closeRes = await fetch(`${API_BASE}/api/v1/positions/close`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'x-api-key': API_KEY },
  body: JSON.stringify({
    positionAddress: position.address,
    userPublicKey: wallet.publicKey,
    slippageBps: 50,
  }),
})
const { transaction } = await closeRes.json()

// 5. Sign and submit
const closeTx = VersionedTransaction.deserialize(bs58.decode(transaction))
const signedClose = await wallet.signTransaction(closeTx)
const closeSig = await connection.sendRawTransaction(signedClose.serialize())
await connection.confirmTransaction(closeSig, 'confirmed')
console.log('Position closed:', closeSig)

Python: Basic Position Lifecycle

import requests
import base58
from solders.transaction import VersionedTransaction
from solders.keypair import Keypair

API_BASE = "https://api.lavarage.xyz"
API_KEY = "your-api-key"
HEADERS = {"Content-Type": "application/json", "x-api-key": API_KEY}

# Open a 3x long SOL position
open_res = requests.post(
    f"{API_BASE}/api/v1/positions/open-by-token",
    headers=HEADERS,
    json={
        "baseTokenMint": "So11111111111111111111111111111111111111112",
        "userPublicKey": str(keypair.pubkey()),
        "collateralAmount": "1000000000",
        "leverage": 3,
        "side": "LONG",
        "slippageBps": 50,
    }
)
open_res.raise_for_status()
transaction = open_res.json()["transaction"]

# Deserialise, sign, submit
tx_bytes = base58.b58decode(transaction)
tx = VersionedTransaction.from_bytes(tx_bytes)
tx.sign([keypair])  # sign with the user's keypair
# submit via your preferred RPC client

# List open positions
positions_res = requests.get(
    f"{API_BASE}/api/v1/positions",
    headers=HEADERS,
    params={"owner": str(keypair.pubkey()), "status": "OPEN"}
)
positions = positions_res.json()
print(f"Open positions: {len(positions)}")

Generate a Client from the OpenAPI Spec

# Download the spec
curl https://api.lavarage.xyz/docs-json -o openapi.json

# Generate a TypeScript fetch client
npx @openapitools/openapi-generator-cli generate \
  -i openapi.json \
  -g typescript-fetch \
  -o ./lavarage-client \
  --additional-properties=typescriptThreePlus=true

# Generate a Python client
npx @openapitools/openapi-generator-cli generate \
  -i openapi.json \
  -g python \
  -o ./lavarage-python-client

The generated client includes full TypeScript types for all request/response shapes.