Skip to main content
Alerts watch the chain for you. Define a rule for the on-chain activity you care about — a token transfer, a successful or failed transaction, a specific function call or contract event — and Octav delivers a notification the moment it happens.

Webhook

HMAC-signed POST to your own HTTPS endpoint — the developer-facing destination

Email

A formatted notification to any email address

Telegram

A message to your chat, paired with @OctavBot
Availability — Alerts require a paid Octav wallet; the Alerts section is hidden until you have one. Each account can create up to 5 alerts by default (contact support to raise the quota). Alerts currently run on Ethereum Mainnet — more networks are on the way.

Create an alert rule

Open Alerts and choose New alert to launch the four-step wizard.

Trigger

Pick what fires the alert. Every alert starts from one on-chain event type:
  • ERC-20 Token Transfer — Fire when an ERC-20 Transfer log is emitted matching your filter.
  • Successful Transaction — Fire on confirmed, successful transactions touching your address.
  • Failed Transaction — Fire on reverted transactions touching your address.
  • Function Call — Fire when a function selector matches the transaction input.
  • Event Emitted — Fire on arbitrary contract events by topic0, optionally with param matchers.

Target

Pick the network and the address to watch. The address means something different per trigger:
  • Token contract (optional) for ERC-20 transfers
  • Wallet address for successful / failed transactions
  • Contract address for function calls and emitted events
For ERC-20 transfers you must set a From or To wallet — a token contract alone matches every transfer of that token across the network. You can also set an optional minimum value, a decimal amount in the token’s display units (e.g. 100 USDC).

Conditions

Refine when the rule matches.
  • Successful / Failed Transaction — match by direction: Sent by me, Received by me, or Either (default).
  • Function Call — pick a function from the contract’s verified ABI, or enter a function signature / 4-byte selector manually. Add per-argument matchers.
  • Event Emitted — pick an event (its topic0 is derived for you), or enter the topic0 and event signature manually. Add per-parameter matchers.
Matchers use exactly one operator each: eq, neq, gt, gte, lt, lte, or in. Tuple, struct, and array parameters aren’t supported yet.
ERC-20 transfers fold their filters (token, From / To, minimum value) into the Target step, so the wizard skips a separate Conditions step for them.

Delivery

Name the alert — the name shows in your alert list and is included in every delivery payload — and attach at least one destination. Save the alert to arm it.

Destinations

A destination is where a matching alert is sent. Add one from the wizard’s Delivery step or manage them from the Alerts page. A single alert can fan out to several destinations.
Each destination must be unique while active — one webhook per URL, one per email address, and a single Telegram link per account. Adding a duplicate is rejected; disable or delete a destination to free that slot.
Enter an HTTPS URL (for example https://hooks.example.com/incoming). Octav creates a signing secret for the destination and shows it to you once:
This is the only time you will see this secret. Store it securely — after this dialog closes we only keep a hashed copy.
Copy it somewhere safe before closing the dialog. If you lose it, rotate the secret from the destination’s edit dialog to generate a new one.
The webhook secret is shown only once and stored as a hash. You can’t read it back later — rotate it if it’s lost.

Webhook deep-dive (for developers)

Webhook destinations receive an HMAC-signed POST for every matching event. This section is the contract your receiver needs to implement.

The request Octav sends

  • Method / bodyPOST with Content-Type: application/json. The body is the JSON payload below.
  • Timeout — 10 seconds.
  • HTTPS only — Octav rejects non-HTTPS URLs and any host that resolves to a private, loopback, or link-local address. You can’t point a webhook at an internal host.
  • Headers
    X-Webhook-Signature: t=<ms-timestamp>,did=<deliveryUuid>,sig=sha256=<lowercase-hex>
    X-Webhook-Signature-Version: 2
    

Payload reference

Every payload carries a top-level version: 1 and a kind discriminator — one of alert, rule_suspended, or rule_replay_digest. For alert, the nested event.kind is one of block, tx, or log.

kind: alert — transaction event

Sent for tx_success, tx_failed, and function_call rules.
{
  "version": 1,
  "kind": "alert",
  "ruleUuid": "rule-uuid",
  "ruleName": "a rule",
  "ruleAddress": "0xwatched",
  "chain": "ethereum",
  "type": "tx_success",
  "emittedAt": "2026-04-22T00:00:00Z",
  "event": {
    "kind": "tx",
    "chain": "ethereum",
    "hash": "0xtx",
    "from": "0xfrom",
    "to": "0xto",
    "status": "success",
    "blockNumber": 42,
    "value": "1000",
    "function": {
      "selector": "0xa9059cbb",
      "name": "transfer",
      "args": { "to": "0xto", "amount": "1000" },
      "decoded": true
    }
  }
}
When the calldata can’t be decoded, function is { "selector": "0x…", "name": null, "args": null, "decoded": false } and a bounded rawInput hex string is included instead. If the raw input is too large it’s dropped in favor of "rawOmitted": true and "rawBytes": <n>.
Sent for erc20_transfer and event_emitted rules.
{
  "version": 1,
  "kind": "alert",
  "ruleUuid": "rule-uuid",
  "ruleName": "a rule",
  "ruleAddress": "0xwatched",
  "chain": "ethereum",
  "type": "erc20_transfer",
  "emittedAt": "2026-04-22T00:00:00Z",
  "event": {
    "kind": "log",
    "chain": "ethereum",
    "txHash": "0xtx",
    "address": "0xcontract",
    "topics": ["0xtopic0", "0xtopic1"],
    "blockNumber": 42,
    "logIndex": 3,
    "decodedEvent": {
      "name": "Transfer",
      "params": { "from": "0xfrom", "to": "0xto", "value": "5" },
      "decoded": true
    },
    "rawData": "0xdata"
  }
}
As with tx events, an undecoded log carries decodedEvent.decoded: false plus a bounded rawData, or rawOmitted / rawBytes when the data is too large to include.
Sent once when a rule is auto-suspended for exceeding its rate limits.
{
  "version": 1,
  "kind": "rule_suspended",
  "ruleUuid": "rule-uuid",
  "ruleName": "a rule",
  "reason": "rate exceeded",
  "suspendedAt": "2026-04-22T00:00:00Z",
  "rate": {
    "softCount": 120,
    "hardCount": 1200,
    "softMax": 100,
    "softWindowSec": 300,
    "hardMax": 1000,
    "hardWindowSec": 3600
  },
  "lastDeliveries": [
    { "uuid": "d1", "eventFingerprint": "fp1", "chain": "ethereum", "createdAt": "2026-04-22T00:00:00Z" }
  ]
}
Sent after a reconnect, summarizing catch-up events instead of delivering each one individually.
{
  "version": 1,
  "kind": "rule_replay_digest",
  "ruleUuid": "rule-uuid",
  "ruleName": "a rule",
  "chain": "ethereum",
  "delivered": 10,
  "suppressed": 40,
  "windowFromBlock": 100,
  "windowToBlock": 200,
  "sampleEvents": [
    { "eventFingerprint": "fp1", "emittedAt": "2026-04-22T00:00:00Z" }
  ]
}

Verify the signature

The X-Webhook-Signature header packs three fields: t (the millisecond timestamp), did (the delivery UUID), and sig (the signature). The signature is HMAC-SHA256(secret, "{t}.{did}.{body}") in lowercase hex. Reconstruct that string from the header fields and the raw request body, compare in constant time, and reject stale timestamps to guard against replay.
const crypto = require("crypto");

function verify(rawBody, header, secret, toleranceMs = 5 * 60 * 1000) {
  const p = Object.fromEntries(
    header.split(",").map((kv) => {
      const i = kv.indexOf("=");
      return [kv.slice(0, i).trim(), kv.slice(i + 1).trim()];
    })
  );
  const sig = p.sig.replace(/^sha256=/, "");
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${p.t}.${p.did}.${rawBody}`)
    .digest("hex");
  const a = Buffer.from(sig, "hex");
  const b = Buffer.from(expected, "hex");
  const fresh = Math.abs(Date.now() - Number(p.t)) < toleranceMs;
  return fresh && a.length === b.length && crypto.timingSafeEqual(a, b);
}
Sign over the raw request body, not a re-serialized object — even a whitespace difference breaks the HMAC. Capture the raw bytes before parsing (for example express.raw({ type: "application/json" }) in Express, or request.get_data() in Flask).

Respond & retry

  • Return any 2xx status to acknowledge a delivery.
  • Non-2xx responses and timeouts are retried up to 5 attempts with exponential backoff (base 2 seconds).
  • Use the delivery UUID (did) as an idempotency key so a retried delivery isn’t processed twice.

Security

  • Only the bounded public payload is sent — internal fields are stripped at the boundary.
  • Large raw input / data is capped (~8 KB) and replaced with rawOmitted / rawBytes.
  • Octav rejects non-HTTPS URLs and any host resolving to a private or loopback address.

Delivery history & suspension

Open any alert to see its delivery history.

Delivery statuses

Each delivery shows one of:
  • Pending — queued or in flight
  • Success — acknowledged with a 2xx
  • Failed — exhausted its retries; you can Retry it manually
  • Dropped — not attempted (for example, the destination was removed)
Two synthetic rows can also appear: Rule suspended and Catch-up digest (see below).
If a rule fires faster than its rate limits allow, Octav automatically suspends it to protect you and your destinations. You receive a rule_suspended webhook and an in-app banner — This alert is suspended — with a Reactivate button. A rule’s status pill reads Active, Suspended, or Disabled.
After Octav reconnects to the chain, missed events may be folded into a single rule_replay_digest delivery summarizing what was delivered and suppressed during the gap, rather than one delivery per event.

Address Book

Label wallets and reuse them when targeting alerts

Transactions

Review the on-chain activity your alerts watch

Reports

Turn tracked activity into financial reports