VIZ.cx
← Learn

Build a tip bot

15 minadvanced

Time to build something real. This bot listens to the live chain and turns a memo into an on-chain reward: whenever someone sends the bot a transfer with a memo like tip alice, it awards alicefrom the bot's energy. It's the whole loop — read the chain, react, write back — in about 40 lines.

Awards are funded by energy, not your balance, so a tip bot costs nothing but the energy that regenerates on its own (fully in 5 days). That makes VIZ a natural fit for social, high-frequency micro-rewards.

Step 1 — Install both packages

@viz-cx/api gives you the hosted live-op stream; @viz-cx/core signs and broadcasts the award.

npm install @viz-cx/core @viz-cx/api

Step 2 — Wire up the stream and the signer

One client reads (no key), one client writes (the bot's active WIF, from the environment). Run this on a server or in a background Node.js process — never in a browser.

import { createApiClient } from '@viz-cx/api'
import { createClient } from '@viz-cx/core'

const BOT = 'tipbot'

// api = the hosted viz.cx stream (read-only, no key needed)
const api = createApiClient()

// chain = a signing client that pays out awards from the bot's energy.
// Load the WIF from the environment — never hardcode it.
const chain = createClient({
  account: BOT,
  activeKey: process.env.VIZ_ACTIVE_KEY!,
  endpoint: 'https://node.viz.cx',
})

Step 3 — React to tips

streamOps is an async iterable. The server-side op_type + account filter means only transfers touching the bot ever arrive, so the loop stays cheap.

// Server-side filter: only transfers that involve the bot reach us.
const stream = api.streamOps({ op_type: 'transfer', account: BOT })

// Each message is { op_id, timestamp, op_type, body }.
// body is the operation itself: { from, to, amount, memo }.
for await (const { body } of stream) {
  if (body.to !== BOT) continue                 // ignore the bot's own sends

  // Command syntax the sender puts in the memo: "tip alice"
  const [cmd, target] = (body.memo ?? '').trim().split(/\s+/)
  if (cmd !== 'tip' || !target || target === body.from) continue

  await chain.award({
    receiver: target,
    energy: 100,                                // 1% of the bot's energy
    memo: `Tipped by ${body.from}`,
  })
  console.log(`${body.from} tipped ${target}`)
}

Step 4 — Make it robust

Long-running bots see dropped sockets and bad input. The stream reconnects itself; wrap each award so one failure can't stop the loop.

// streamOps auto-reconnects; surface status changes and keep going
// even if a single award fails (e.g. the target account is gone).
stream.onStatus((s) => console.log('stream:', s))

for await (const { body } of stream) {
  try {
    // ... command parsing + chain.award(...) as above
  } catch (err) {
    console.error('skip:', err)                 // never let one op kill the loop
  }
}

From here: rate-limit per sender, require a minimum transfer to fund tips, or split the award with beneficiaries. To move off a persistent socket entirely, see React to events with webhooks.