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:

ParameterDescription
maxLeverageMaximum leverage allowed (e.g., 5x, 10x)
interestRateAPR charged on borrowed capital
availableForOpenLiquidity currently available for new positions
utilizationPercentage of pool capital currently lent out
openLTVMaximum LTV at which new positions can be opened
liquidationLTVLTV threshold that triggers liquidation
sideLONG 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:

ParameterDescription
searchToken name, ticker, or mint address
includeTokensInclude token metadata in response
limitMax results (default: 20)
offsetPagination offset
orderBySort 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:

  1. Queries all active offers for the requested token pair and side
  2. Filters by available liquidity — the pool must cover collateralAmount x (leverage - 1)
  3. Filters by leverage limit — the pool's maxLeverage must be >= your requested leverage
  4. Sorts by best terms: highest leverage limit, lowest utilization, most favorable rates
  5. 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:

EventEffect on Pool
Position openedavailableForOpen decreases, utilization increases
Position closedavailableForOpen increases, utilization decreases
LiquidationSame as close — capital returns to pool
Lender depositsavailableForOpen increases
Lender withdrawsavailableForOpen 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

IssueCauseFix
NO_OFFER_FOR_TOKENNo pool exists for this token/sideCheck GET /offers?search=TOKEN to verify availability
INSUFFICIENT_LIQUIDITYPool doesn't have enough for your tradeReduce trade size or wait for liquidity to free up
LEVERAGE_EXCEEDEDRequested leverage > pool's maxLeverageQuery the offer to check its maxLeverage and cap your request
Stale quotesPool utilization changed between quote and openAlways call open-by-token immediately after getting a quote. Don't cache quotes.