Pools & Smart Order Routing
Every leveraged trade on Lavarage borrows from a lending pool. Understanding how pools work and how the routing engine selects them is essential for building a good integration.
How Pools Work
On Lavarage, a pool (also called an offer) is a lending vault operated by a lender. Each pool has its own parameters:
| Parameter | Description |
|---|---|
maxLeverage | Maximum leverage allowed (e.g., 5x, 10x) |
interestRate | APR charged on borrowed capital |
availableForOpen | Liquidity currently available for new positions |
utilization | Percentage of pool capital currently lent out |
openLTV | Maximum LTV at which new positions can be opened |
liquidationLTV | LTV threshold that triggers liquidation |
side | LONG or SHORT — which direction this pool supports |
A single token pair may have multiple pools with different terms. SOL/USDC might have three pools: one offering 3x max at low interest, another offering 10x at higher interest, and a third with deep liquidity but moderate terms.
Discovering Pools
GET /api/v1/offers
List all active pools, optionally filtered by token:
# All SOL pools
curl "https://api.lavarage.xyz/api/v1/offers?search=SOL&includeTokens=true"
# Filter by specific token mint
curl "https://api.lavarage.xyz/api/v1/offers?search=So11111111111111111111111111111111111111112&includeTokens=true"Query parameters:
| Parameter | Description |
|---|---|
search | Token name, ticker, or mint address |
includeTokens | Include token metadata in response |
limit | Max results (default: 20) |
offset | Pagination offset |
orderBy | Sort by price, leverage, interest, or fee |
Response includes the full offer object with publicKey, maxLeverage, interestRate, availableForOpen, and token metadata.
GET /api/v1/offers/utilization
Get utilization stats across all pools:
curl "https://api.lavarage.xyz/api/v1/offers/utilization"Useful for monitoring system-wide liquidity and building dashboards.
Two Ways to Open Positions
1. Let the Server Route (Recommended)
Use POST /api/v1/positions/open-by-token with just the token mint. The server handles pool selection automatically.
{
"baseTokenMint": "So11111111111111111111111111111111111111112",
"userPublicKey": "GsbwXfJraMomNxBcjK7xK2xQx5MQgQx4Ld8QkLeNmA3v",
"collateralAmount": "1000000000",
"leverage": 3,
"side": "LONG",
"slippageBps": 50
}The routing engine:
- Queries all active offers for the requested token pair and side
- Filters by available liquidity — the pool must cover
collateralAmount x (leverage - 1) - Filters by leverage limit — the pool's
maxLeveragemust be >= your requested leverage - Sorts by best terms: highest leverage limit, lowest utilization, most favorable rates
- Selects the top offer and builds the transaction
If no matching offer exists, the server returns 404 NO_OFFER_FOR_TOKEN.
This is the recommended approach for all integrations. It handles pool selection, liquidity checks, and fallback logic for you.
2. Specify the Pool (Advanced)
Use POST /api/v1/positions/open with a specific offerPublicKey:
{
"offerPublicKey": "8xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"userPublicKey": "GsbwXfJraMomNxBcjK7xK2xQx5MQgQx4Ld8QkLeNmA3v",
"collateralAmount": "1000000000",
"leverage": 3,
"side": "LONG",
"slippageBps": 50
}When to use this:
- You've built your own pool selection logic
- You want to route to a specific lender (e.g., your own vault)
- You need deterministic pool selection for testing
You are responsible for: checking the pool has sufficient liquidity, the leverage is within limits, and the pool supports your requested side. The server will reject the transaction if any constraint is violated.
Building Your Own Router
If you want finer control over pool selection, here's the pattern:
async function findBestPool(tokenMint: string, side: string, leverage: number, borrowNeeded: number) {
const offers = await fetch(
`${API_BASE}/api/v1/offers?search=${tokenMint}&includeTokens=true`
).then(r => r.json());
return offers
.filter((o: any) => o.side === side)
.filter((o: any) => parseFloat(o.maxLeverage) >= leverage)
.filter((o: any) => parseFloat(o.availableForOpen) >= borrowNeeded)
.sort((a: any, b: any) => {
// Prefer lower utilization (more liquidity headroom)
const utilDiff = parseFloat(a.utilization) - parseFloat(b.utilization);
if (Math.abs(utilDiff) > 5) return utilDiff;
// Then prefer lower interest rate
return parseFloat(a.interestRate) - parseFloat(b.interestRate);
})[0];
}Then pass the selected pool's publicKey to the open endpoint.
Pool Lifecycle Events
Pools are dynamic. Their state changes as traders open and close positions:
| Event | Effect on Pool |
|---|---|
| Position opened | availableForOpen decreases, utilization increases |
| Position closed | availableForOpen increases, utilization decreases |
| Liquidation | Same as close — capital returns to pool |
| Lender deposits | availableForOpen increases |
| Lender withdraws | availableForOpen decreases |
For real-time pool updates, use the SSE stream (GET /api/v1/events/stream) which pushes offer state changes as they happen. See Real-Time Data for details.
Common Pitfalls
| Issue | Cause | Fix |
|---|---|---|
NO_OFFER_FOR_TOKEN | No pool exists for this token/side | Check GET /offers?search=TOKEN to verify availability |
INSUFFICIENT_LIQUIDITY | Pool doesn't have enough for your trade | Reduce trade size or wait for liquidity to free up |
LEVERAGE_EXCEEDED | Requested leverage > pool's maxLeverage | Query the offer to check its maxLeverage and cap your request |
| Stale quotes | Pool utilization changed between quote and open | Always call open-by-token immediately after getting a quote. Don't cache quotes. |
Updated 4 days ago