VIZ.cx
← Learn

Stream live operations

10 minintermediate

wss://api.viz.cx/ws/ops emits a message for every operation applied at the chain head, typically 1.5–4.5 seconds after it was signed (3s blocks, 1.5s poller). This makes it easy to react to live awards, transfers, and other events without polling.

Step 1 — Connect and log all operations

Each message is a flat JSON object with four fields: op_id, timestamp, op_type, and body. The body is the operation's payload — its shape depends on op_type.

const ws = new WebSocket('wss://api.viz.cx/ws/ops')

ws.onopen = () => console.log('connected')

ws.onmessage = (event) => {
  const item = JSON.parse(event.data)
  // item shape:
  // { op_id: '82134935.0', timestamp: '2026-08-01T02:01:27',
  //   op_type: 'award', body: { initiator: 'alice', receiver: 'bob', ... } }
  console.log(item.timestamp, item.op_type, item.body)
}

ws.onclose = () => console.log('disconnected')
ws.onerror = (e) => console.error('ws error', e)

Step 2 — Filter by operation type or account

Most apps care about a subset of ops. Filter server-side with the op_type and account query params so unwanted traffic never crosses the wire. Common types: award, transfer, account_create, delegate_vesting_shares, custom.

// Server-side: only 'award' ops ever reach this socket.
const ws = new WebSocket('wss://api.viz.cx/ws/ops?op_type=award')

ws.onmessage = (event) => {
  const { body } = JSON.parse(event.data)

  console.log(
    `${body.initiator} awarded ${body.receiver}`,
    `energy: ${body.energy / 100}%`,
    body.memo ?? ''
  )
}

// Or filter by account — matches these body fields, across every op type:
//   from, to, receiver, account, benefactor, witness,
//   required_active_auths, required_regular_auths
// wss://api.viz.cx/ws/ops?account=alice
//
// Note: an award's 'initiator' is NOT one of them — ?account=alice catches
// awards alice RECEIVED, not ones she sent. Filter senders in JS.
//
// Both params combine, and you can still narrow further in JS on fields
// the server doesn't index:
// if (body.energy < 5000) return

Step 3 — Resolve the block number

Need to link an op back to the explorer? op_id encodes its block.

// op_id is '<block>.<op-fraction>' — the block number is the integer part.
const blockNum = Math.floor(Number(item.op_id))
console.log(`https://viz.cx/block/${blockNum}`)

Step 4 — Add reconnect logic

WebSocket connections drop. A simple 3-second retry on onclose is sufficient for most use cases.

function connect() {
  const ws = new WebSocket('wss://api.viz.cx/ws/ops?op_type=award')

  ws.onmessage = (event) => {
    const { body } = JSON.parse(event.data)
    console.log(`award: ${body.initiator} → ${body.receiver}`)
  }

  ws.onclose = () => {
    console.log('reconnecting in 3s…')
    setTimeout(connect, 3000)
  }

  ws.onerror = () => ws.close()

  return ws
}

const ws = connect()

Two things to know

The feed carries real operations only — virtual ops (author rewards, curation payouts, vesting withdrawals) are produced when a block becomes irreversible and never appear here. Read those from account history instead.

And because these are head blocks, not irreversible ones, an op you see is overwhelmingly likely to stick but is not yet final. For anything with money attached, treat the stream as a notification and confirm against account history.