> ## Documentation Index
> Fetch the complete documentation index at: https://docs.octav.fi/llms.txt
> Use this file to discover all available pages before exploring further.

# Alerts

> Get notified in real time when on-chain activity matches your rules — via webhook, email, or Telegram.

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.

<CardGroup cols={3}>
  <Card title="Webhook" icon="webhook">
    HMAC-signed `POST` to your own HTTPS endpoint — the developer-facing destination
  </Card>

  <Card title="Email" icon="envelope">
    A formatted notification to any email address
  </Card>

  <Card title="Telegram" icon="paper-plane">
    A message to your chat, paired with @OctavBot
  </Card>
</CardGroup>

<Info>
  **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.
</Info>

***

## Create an alert rule

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

<Steps>
  <Step title="Trigger" icon="bolt">
    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.
  </Step>

  <Step title="Target" icon="crosshairs">
    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).
  </Step>

  <Step title="Conditions" icon="filter">
    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.

    <Note>
      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.
    </Note>
  </Step>

  <Step title="Delivery" icon="paper-plane">
    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.
  </Step>
</Steps>

***

## 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.

<Note>
  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.
</Note>

<Tabs>
  <Tab title="Webhook" icon="webhook">
    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.

    <Warning>
      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.
    </Warning>
  </Tab>

  <Tab title="Email" icon="envelope">
    Enter the email address that should receive the notification.
  </Tab>

  <Tab title="Telegram" icon="paper-plane">
    Pair your Octav account with **@OctavBot** using the one-click deep link
    (**Open @OctavBot on Telegram**). Once paired, *Alerts will be sent to your
    linked chat* — you don't enter a chat ID, it's resolved from your pairing.
    If pairing is ever lost, re-pair with @OctavBot and refresh.
  </Tab>
</Tabs>

***

## 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 / body** — `POST` 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`.

<AccordionGroup>
  <Accordion title="kind: alert — transaction event" icon="receipt" defaultOpen>
    Sent for `tx_success`, `tx_failed`, and `function_call` rules.

    ```json theme={null}
    {
      "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>`.
  </Accordion>

  <Accordion title="kind: alert — log event" icon="file-lines">
    Sent for `erc20_transfer` and `event_emitted` rules.

    ```json theme={null}
    {
      "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.
  </Accordion>

  <Accordion title="kind: rule_suspended" icon="ban">
    Sent once when a rule is auto-suspended for exceeding its rate limits.

    ```json theme={null}
    {
      "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" }
      ]
    }
    ```
  </Accordion>

  <Accordion title="kind: rule_replay_digest" icon="clock-rotate-left">
    Sent after a reconnect, summarizing catch-up events instead of delivering
    each one individually.

    ```json theme={null}
    {
      "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" }
      ]
    }
    ```
  </Accordion>
</AccordionGroup>

### 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.

<CodeGroup>
  ```js Node theme={null}
  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);
  }
  ```

  ```python Python theme={null}
  import hashlib
  import hmac
  import time


  def verify(raw_body: bytes, header: str, secret: str, tolerance_ms: int = 5 * 60 * 1000) -> bool:
      parts = {}
      for kv in header.split(","):
          key, _, value = kv.partition("=")
          parts[key.strip()] = value.strip()

      sig = parts["sig"].removeprefix("sha256=")
      body = raw_body.decode() if isinstance(raw_body, bytes) else raw_body
      expected = hmac.new(
          secret.encode(), f"{parts['t']}.{parts['did']}.{body}".encode(), hashlib.sha256
      ).hexdigest()

      fresh = abs(int(time.time() * 1000) - int(parts["t"])) < tolerance_ms
      return fresh and hmac.compare_digest(sig, expected)
  ```
</CodeGroup>

<Tip>
  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).
</Tip>

### 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.

<AccordionGroup>
  <Accordion title="Delivery statuses" icon="list-check" defaultOpen>
    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).
  </Accordion>

  <Accordion title="Auto-suspension" icon="ban">
    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**.
  </Accordion>

  <Accordion title="Catch-up after a reconnect" icon="clock-rotate-left">
    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.
  </Accordion>
</AccordionGroup>

***

## Related

<CardGroup cols={3}>
  <Card title="Address Book" icon="address-book" href="/docs/address-book">
    Label wallets and reuse them when targeting alerts
  </Card>

  <Card title="Transactions" icon="list" href="/docs/transactions">
    Review the on-chain activity your alerts watch
  </Card>

  <Card title="Reports" icon="file-chart-column" href="/docs/reports">
    Turn tracked activity into financial reports
  </Card>
</CardGroup>
