Skip to main content

Subscribe to chain events

Real-time updates without polling.

SSE (Server-Sent Events)

One-way stream, plain HTTP. Easiest to deploy (no special infra).

import { OmbraClient } from "@ombrachain/sdk";
const client = new OmbraClient({ endpoint: "https://api.ombra-net.com" });

const sub = client.events.subscribeChain(
(event) => {
if (event.type === "block") {
console.log("block", event.data.index, "txs:", event.data.transactions.length);
} else if (event.type === "hello") {
console.log("connected at height", event.data.height);
}
},
(err) => console.error("SSE error", err),
);

// Later: sub.close();

Behind the scenes the SDK auto-reconnects on disconnect with exponential backoff (1s, 2s, 4s, 8s, cap 30s).

WebSocket (bi-directional channels)

For dynamic channel subscriptions or sending commands back:

const ws = new WebSocket("wss://api.ombra-net.com/api/ws");

ws.onopen = () => {
ws.send(JSON.stringify({ action: "subscribe", channel: "txs" }));
ws.send(JSON.stringify({ action: "subscribe", channel: "blocks" }));
};

ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.type === "tx") console.log("new tx:", msg.data.type, msg.data.hash);
if (msg.type === "block") console.log("new block:", msg.data.index);
};

// Heartbeat
setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ action: "ping" }));
}
}, 30_000);

Deduplication

If you combine SSE + polling fallback, dedupe by tx hash:

const seen = new Set<string>();

function handleTx(tx: any) {
if (seen.has(tx.hash)) return;
seen.add(tx.hash);
// process tx
if (seen.size > 10_000) {
// prune oldest
const arr = [...seen];
seen.clear();
arr.slice(-5_000).forEach((h) => seen.add(h));
}
}

Filter by tx type or address

The SSE stream sends every block — filter client-side:

client.events.subscribeChain((event) => {
if (event.type !== "block") return;
for (const tx of event.data.transactions) {
if (tx.type === "TRANSFER" && tx.to === MY_ADDRESS) {
console.log("received", tx.amount, "from", tx.from);
}
}
});

For high-volume filtering, prefer /api/alerts/subscribe with webhook — server-side filtering, no client load.

Reconnection patterns

For long-running listeners (background daemons, indexers):

async function runIndexer() {
while (true) {
try {
await new Promise<void>((resolve, reject) => {
const sub = client.events.subscribeChain(
(event) => { /* index event */ },
(err) => reject(err),
);
});
} catch (err) {
console.error("SSE crashed, reconnecting in 5s:", err);
await new Promise((r) => setTimeout(r, 5_000));
}
}
}

Catching up after downtime

SSE doesn't replay missed events. To recover from gaps:

  1. Track lastProcessedBlockIndex in your storage
  2. On reconnect: query /api/chain/blocks-full?from=lastProcessedBlockIndex+1&to=currentHeight (max 500 blocks per call)
  3. Process backlog → then resume SSE
let lastBlock = await loadCheckpoint();
const { height } = await client.chain.getStats();
while (lastBlock < height) {
const batch = await fetchJson(
`https://api.ombra-net.com/api/chain/blocks-full?from=${lastBlock + 1}&to=${Math.min(lastBlock + 500, height)}`,
);
for (const block of batch.blocks) processBlock(block);
lastBlock = batch.blocks[batch.blocks.length - 1].index;
await saveCheckpoint(lastBlock);
}
// Now SSE is up-to-date — start live subscription