Real-Time Data

Two ways to get real-time updates: Server-Sent Events (SSE) for streaming data, and Webhooks for push notifications on state changes.

Server-Sent Events (SSE)

The SSE endpoint streams real-time updates without polling.

Endpoint: GET /api/v1/sse

Authentication modes:

  • Public mode (default): no auth, same behavior as before
  • Backend scoped mode: add apiKey=<raw_api_key> query param to scope position events to that API key

Optional query parameter: owner=<wallet_address> to further narrow position events to one wallet.

Connecting

Public mode (frontend/browser):

const es = new EventSource('https://api.lavarage.xyz/api/v1/sse?owner=YOUR_WALLET')

es.onopen = () => console.log('Connected')
es.onerror = () => {
  es.close()
  setTimeout(reconnect, 5_000)
}

Backend scoped mode (server-to-server):

Use this only from trusted backend services. Do not embed apiKey in browser/client code.

import EventSource from 'eventsource'

const apiKey = process.env.LAVARAGE_API_KEY!
const owner = 'YOUR_WALLET'
const url = 'https://api.lavarage.xyz/api/v1/sse?apiKey=' + encodeURIComponent(apiKey) + '&owner=' + owner

const es = new EventSource(url)

es.onopen = () => console.log('Connected')
es.onerror = () => {
  es.close()
  setTimeout(reconnect, 5_000)
}

Event Types

prices

Emitted every ~10 seconds with updated token prices.

Payload shape: Record<mintAddress, usdPriceString>

es.addEventListener('prices', (e) => {
  const prices: Record<string, string> = JSON.parse(e.data)
  // { "So111...": "152.40", "EPjFW...": "1.0001" }
  updatePriceStore(prices)
})

positions

Emitted when a monitored position changes status (e.g. SUBMITTED → ONCHAIN, or position closed/liquidated).

Payload shape: array of lightweight updates:

[
  {
    "address": "7xKX...",
    "owner": "Gsbw...",
    "status": "ONCHAIN",
    "event": "position_opened"
  }
]

Notes:

  • This is a change signal, not a full position object
  • Sometimes the server emits [] as a generic refresh signal (e.g. backfill pass)
  • Best practice: on any positions event, re-fetch GET /api/v1/positions
es.addEventListener('positions', (e) => {
  const updates = JSON.parse(e.data) // array, may be []
  invalidatePositionsCache()
})

offers

Emitted when offer/pool data changes (new offers, liquidity changes).

Payload shape is currently a small signal object (example: { "updated": 2839 }).

Important:

  • It does not include full offer rows
  • It does not include a specific changed-offer ID list
  • Client should treat it as an invalidation signal and re-fetch relevant offer endpoints
es.addEventListener('offers', (e) => {
  const signal = JSON.parse(e.data) // e.g. { updated: number }
  invalidateOffersCache()
})

heartbeat

Emitted every 30 seconds to keep the connection alive through proxies. No data payload — safe to ignore.

Reconnection

Implement a reconnect loop in your backend service:

function connect(walletAddress?: string) {
  const apiKey = process.env.LAVARAGE_API_KEY!
  const url = walletAddress
    ? `https://api.lavarage.xyz/api/v1/sse?apiKey=${encodeURIComponent(apiKey)}&owner=${walletAddress}`
    : `https://api.lavarage.xyz/api/v1/sse?apiKey=${encodeURIComponent(apiKey)}`

  const es = new EventSource(url)
  let reconnectTimer: ReturnType<typeof setTimeout>

  es.onopen = () => clearTimeout(reconnectTimer)
  es.onerror = () => {
    es.close()
    reconnectTimer = setTimeout(() => connect(walletAddress), 5_000)
  }

  es.addEventListener('prices', (e) => {
    const prices = JSON.parse(e.data)
    // handle price update
  })

  es.addEventListener('positions', (e) => {
    const updates = JSON.parse(e.data)
    // handle position update
  })

  es.addEventListener('offers', () => {
    // handle offer update
  })

  return () => {
    clearTimeout(reconnectTimer)
    es.close()
  }
}

const disconnect = connect('GsbwXfJraMomNxBcjK7xK2xQx5MQgQx4Ld8QkLeNmA3v')

Notes

  • Public mode (no apiKey) remains compatible with existing frontend EventSource usage
  • Price data arrives as a flat map of mintAddress → usdPrice (string decimals)
  • In scoped mode (apiKey provided), position events are filtered by API key; ?owner= adds wallet-level filtering
  • prices and offers are global signals in both modes (not API-key scoped)
  • Recommended client behavior: update local price store on prices, invalidate/refetch on offers and positions
  • If the connection drops, existing subscriptions do not need to be re-registered — just reconnect

Webhooks

Lavarage supports outbound webhooks so your server receives push notifications when position state changes, rather than polling the API.

Webhook URLs are managed by Lavarage. Contact [email protected] to configure your webhook URL and secret. Only HTTPS endpoints are accepted.

Event Types

EventTrigger
position.openedPosition opened on-chain and indexed
position.closedPosition fully closed
position.liquidatedPosition was liquidated
order.executedTP/SL order executed successfully
order.failedTP/SL order execution failed
ltv.warningPosition LTV approaching liquidation threshold

Payload Shape

{
  "type": "position.closed",
  "positionAddress": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
  "walletAddress": "GsbwXfJraMomNxBcjK7xK2xQx5MQgQx4Ld8QkLeNmA3v",
  "data": {},
  "timestamp": "2026-03-15T12:00:00.000Z"
}

Verifying the Signature

Every delivery includes two headers:

  • X-Lavarage-Signature: HMAC-SHA256 hex digest
  • X-Lavarage-Timestamp: ISO timestamp

The signed content is ${timestamp}.${JSON.stringify(payload)}.

import { createHmac } from 'crypto'

function verifyWebhook(
  rawBody: string,
  signature: string,
  timestamp: string,
  secret: string,
): boolean {
  const expected = createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex')
  return expected === signature
}

// Express example
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const valid = verifyWebhook(
    req.body.toString(),
    req.headers['x-lavarage-signature'] as string,
    req.headers['x-lavarage-timestamp'] as string,
    process.env.WEBHOOK_SECRET!,
  )
  if (!valid) return res.status(401).send('Invalid signature')

  const event = JSON.parse(req.body.toString())
  console.log('Event received:', event.type, event.positionAddress)
  res.status(200).send('OK')
})

Delivery Guarantees

  • Webhook delivery has a 10-second timeout
  • Delivery is fire-and-forget — failed deliveries are not retried
  • Ensure your endpoint responds with HTTP 2xx within 10 seconds
  • Webhooks are only delivered if your partner profile has a webhook URL and secret configured by the Lavarage team — contact [email protected] to set this up