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
}| Field | Type | Required | Description |
|---|---|---|---|
baseTokenMint | string | Yes | Token mint you want to go long/short on (base58) |
userPublicKey | string | Yes | User's wallet address (base58) |
collateralAmount | string | Yes | Collateral in the quote token's smallest units (lamports for SOL) |
leverage | number | Yes | Multiplier between 1.1 and 100 |
side | string | Yes | "LONG" or "SHORT" |
quoteTokenMint | string | No | Quote token (default: WSOL for LONG, USDC for SHORT) |
slippageBps | number | No | Slippage 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:
- Queries active offers for the requested token pair and side
- Filters by available liquidity (must cover
collateralAmount × (leverage - 1)) - Sorts by best terms (highest leverage limit, lowest utilization)
- 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
| Side | Collateral Token | What Happens |
|---|---|---|
LONG | WSOL (default) or USDC | Protocol borrows SOL/USDC, swaps into the target token. Profit when price rises. |
SHORT | USDC | Protocol 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 addressstatus—OPEN,CLOSED, orALL(default:ALL)side—LONG,SHORT, orBORROWnodeWallet— filter by vault/node wallet addresslimit— 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
- Build the transaction with a tip — pass
astralaneTipLamportswhen calling/positions/openor/positions/close - Sign the transaction client-side with the user's wallet
- Submit via Astralane — send to
POST /api/v1/bundle/submitinstead 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/tipResponse:
{ "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:
POST /positions/partial-sell— returnssplitTransaction+closeTransaction- Build a tip transaction (SOL transfer to a Jito tip account)
- Sign all 3 transactions
- Submit as a bundle:
POST /api/v1/bundle
{
"transactions": ["signedTipTx", "signedSplitTx", "signedCloseTx"]
}Tip Amounts
| Endpoint | Description |
|---|---|
GET /api/v1/bundle/tip | Returns 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
| Scenario | Recommendation |
|---|---|
| Large trades (> $1K) | Always use MEV protection |
| Small trades (< $100) | Optional — tip cost may exceed MEV savings |
| High-volatility tokens | Recommended — more attractive to sandwich bots |
| Stablecoin pairs | Lower 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
- Build transaction via API → get
transaction+lastValidBlockHeight - Sign and submit
- Confirm with expiry detection
- If expired: rebuild from API (new blockhash), sign, submit
- 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-clientThe generated client includes full TypeScript types for all request/response shapes.
Updated 4 days ago