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
pricesEmitted 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
positionsEmitted 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
positionsevent, re-fetchGET /api/v1/positions
es.addEventListener('positions', (e) => {
const updates = JSON.parse(e.data) // array, may be []
invalidatePositionsCache()
})offers
offersEmitted 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
heartbeatEmitted 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 (
apiKeyprovided), position events are filtered by API key;?owner=adds wallet-level filtering pricesandoffersare global signals in both modes (not API-key scoped)- Recommended client behavior: update local price store on
prices, invalidate/refetch onoffersandpositions - 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
| Event | Trigger |
|---|---|
position.opened | Position opened on-chain and indexed |
position.closed | Position fully closed |
position.liquidated | Position was liquidated |
order.executed | TP/SL order executed successfully |
order.failed | TP/SL order execution failed |
ltv.warning | Position 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 digestX-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
Updated 4 days ago