# Attack WebSocket Message

This document describes the JSON structure of WebSocket messages delivered to
clients when an on-chain attack is detected.

Messages are delivered on two feeds:

- **`/ws/attacks`** — every detected attack, delivered immediately. This is the
  feed described in the bulk of this document.
- **`/ws/confirmed_attacks`** — a curated, lower-noise stream of attacks that
  have been post-processed and confirmed as genuine exploits by an LLM
  pipeline. See [Confirmed-attack feed](#confirmed-attack-feed). A single client
  may subscribe to both feeds at the same time.

Each message is a single JSON object. There are two variants of the payload:

- **Standard attack** — the attack does not touch a known protected address.
- **Protected-address attack** — the attack touches an address the recipient
  has registered for protection. The payload carries three extra fields
  (`victim_protocol_id`, `victim_protocol`, `victim_label`).

## Top-level schema

```json
{
  "network": "mainnet",
  "severity": "HIGH",
  "attack_type": "suspicious_contract_call_with_profit",
  "transaction_hash": "0x9c8b6f3b6f6a1b2a3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b",
  "exploit_address": "0x1111111111111111111111111111111111111111",
  "block_number": 21345678,
  "block_timestamp": 1731609600,
  "proc_time": "2026-05-13 09:42:11.503127",
  "attacker_address": "0x2222222222222222222222222222222222222222",
  "input": "0xa9059cbb000000000000000000000000abcdef...",
  "balance_change": 248173.42,
  "matched_traces": "",
  "matched_logs": "",
  "matched_selectors": "",
  "victim_address": "0x3333333333333333333333333333333333333333",
  "protocols": { "...see Enriched data below..." },

  "victim_protocol_id": 137,
  "victim_protocol": "Aave V3",
  "victim_label": "Aave V3 Pool"
}
```

The last three fields (`victim_protocol_id`, `victim_protocol`,
`victim_label`) are present **only** in the protected-address variant. For a
standard attack they are absent from the JSON document.

## Field reference

| Field | Type | Description / example values |
| --- | --- | --- |
| `network` | string | Source chain. One of `mainnet`, `bsc`, `arbitrum`, `polygon`, `optimism`, `base`, `avax`, `hyperliquid`. |
| `severity` | string \| null | Risk level. Typical values: `"HIGH"`, `"MEDIUM"`, `"LOW"`, `"CRITICAL"`. May be `null` when severity could not be computed. |
| `attack_type` | string | Type of detection that fired. See [Attack types](#attack-types). |
| `transaction_hash` | string (0x + 64 hex) | The transaction the alert is bound to. |
| `exploit_address` | string (0x + 40 hex) | Contract being interacted with (the suspected exploit / target). |
| `block_number` | int | Block height containing the transaction. |
| `block_timestamp` | int | Unix epoch seconds, e.g. `1731609600`. |
| `proc_time` | string | UTC timestamp of when the alert was generated, e.g. `"2026-05-13 09:42:11.503127"`. |
| `attacker_address` | string (0x + 40 hex) | EOA / contract that initiated the suspected attack. |
| `input` | string | Raw transaction calldata, e.g. `"0xa9059cbb0000..."`. |
| `balance_change` | float \| null | Net USD value moved by the attacker for this transaction, e.g. `248173.42`. Can be `0.0` or negative; `null` when not computed. |
| `matched_traces` | string | Comma-separated trace identifiers that matched the detection rules. Often `""`. |
| `matched_logs` | string | Comma-separated log identifiers that matched. Often `""`. |
| `matched_selectors` | string | Comma-separated 4-byte selectors that matched, e.g. `"0xa9059cbb,0x23b872dd"`. Often `""`. |
| `victim_address` | string \| null | Address suffering the largest loss in USD; may be `null` for attack types without a single identifiable victim. |
| `protocols` | object \| null | Enrichment payload — see [Enriched data](#enriched-data-protocols). |
| `victim_protocol_id` | int *(protected-address variant only)* | Protocol ID of the matched protected address, e.g. `42`. |
| `victim_protocol` | string *(protected-address variant only)* | Human-readable protocol name, e.g. `"Aave V3"`. |
| `victim_label` | string *(protected-address variant only)* | Label associated with the protected address, e.g. `"Aave V3 Pool"`. |

### Attack types

Possible values of `attack_type` for network-wide exploit monitoring (i.e. alerts that are not scoped
to a specific protected address), subscribe to the following set of
`attack_type` values:

- `suspicious_contract_call_with_profit`
- `highly_complex_transaction_with_profit`
- `mev_tx_with_unusual_profit`
- `exploit_in_initcode`
- `abnormal_token_minting`
- `suspicious_large_transfer`
- `access_transfer_to_suspicious_address`
- `access_transfer_to_suspicious_address_with_profit`

### Enriched data (`protocols`)

The `protocols` field carries enrichment about the victim and the addresses
involved in the transaction. Three shapes are possible:

#### 1. `null`

No enrichment was attached to this alert. The field is emitted as JSON `null`.

#### 2. Lightweight enrichment

Returned when only victim information is available:

```json
{
  "is_full_enrichment": false,
  "victim_info": {
    "is_eoa": false,
    "address": "0x3333333333333333333333333333333333333333",
    "name": "Aave V3",
    "symbol": "aUSDC",
    "tvl": 4123456789.12,
    "is_coingecko_pool": false,
    "is_protocol": true,
    "site_url": "https://aave.com",
    "protocol_id": 42
  }
}
```

#### 3. Full enrichment

Returned for high-impact alerts and contains, in addition to `victim_info`,
the full set of address-level balance changes for the transaction:

```json
{
  "is_full_enrichment": true,
  "hacker_profit": 248173.42,
  "victim_info": {
    "is_eoa": false,
    "address": "0x3333333333333333333333333333333333333333",
    "name": "Aave V3",
    "symbol": "aUSDC",
    "tvl": 4123456789.12,
    "is_coingecko_pool": false,
    "is_protocol": true,
    "site_url": "https://aave.com",
    "protocol_id": 42
  },
  "balance_changes": [
    {
      "address": "0x2222222222222222222222222222222222222222",
      "balance_change_usd": 248173.42,
      "is_eoa": true
    },
    {
      "address": "0x3333333333333333333333333333333333333333",
      "balance_change_usd": -248173.42,
      "is_eoa": false,
      "name": "Aave V3",
      "symbol": "aUSDC",
      "tvl": 4123456789.12,
      "site_url": "https://aave.com",
      "protocol_id": 42,
      "is_protocol": true
    },
    {
      "address": "0x4444444444444444444444444444444444444444",
      "balance_change_usd": -1532.10,
      "is_eoa": false,
      "name": "USDC/WETH 0.05%",
      "symbol": "USDC",
      "tvl": 123456789.0,
      "is_coingecko_pool": true,
      "is_protocol": false
    },
    {
      "address": "0x5555555555555555555555555555555555555555",
      "balance_change_usd": -42.0,
      "is_eoa": false,
      "name": "Unverified contract",
      "symbol": null,
      "tvl": null,
      "is_protocol": false
    },
    {
      "address": "",
      "balance_change_usd": 1,
      "is_eoa": false,
      "name": "Uniswap V3",
      "symbol": null,
      "tvl": 5000000000.0,
      "site_url": "https://uniswap.org",
      "is_protocol": true
    }
  ],
  "raw_token_balances": {
    "0x2222222222222222222222222222222222222222": {
      "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48": "248173420000"
    },
    "0x3333333333333333333333333333333333333333": {
      "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48": "-248173420000"
    }
  },
  "token_prices": {
    "token_data": {
      "ethereum:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48": {
        "price": "1.00",
        "decimals": "6",
        "symbol": "USDC"
      },
      "ethereum:0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2": {
        "price": "3450.12",
        "decimals": "18",
        "symbol": "WETH"
      }
    }
  }
}
```

##### `victim_info` fields

| Field | Type | Example |
| --- | --- | --- |
| `is_eoa` | bool | `false` |
| `address` | string | `"0x3333...3333"` |
| `name` | string \| null | `"Aave V3"`, `"Unverified contract"`, or `null` |
| `symbol` | string \| null | `"aUSDC"`, `"USDC"`, or `null` |
| `tvl` | number \| null | USD TVL when known; `null` otherwise. |
| `is_coingecko_pool` | bool | `true` when the address was classified as a CoinGecko-tracked liquidity pool. |
| `is_protocol` | bool | `true` for known protocols. |
| `site_url` | string \| null | Protocol site URL, e.g. `"https://aave.com"`. |
| `protocol_id` | int \| null | Numeric protocol id, e.g. `42`. |

##### `balance_changes[]` entry fields

Each entry always includes `address`, `balance_change_usd`, and `is_eoa`.
The remaining fields depend on how the address was classified (known
protocol, CoinGecko pool, verified contract, or unresolved):

| Field | Type | When present |
| --- | --- | --- |
| `address` | string | Always (may be `""` for synthetic protocol-summary rows). |
| `balance_change_usd` | number | Always. Negative = loss, positive = gain. Synthetic protocol-summary rows use `1` as a sentinel. |
| `is_eoa` | bool | Always. |
| `name` | string \| null | Set unless the address is an EOA. `"Unverified contract"` for unresolved contracts. |
| `symbol` | string \| null | Token symbol when known. |
| `tvl` | number \| null | Protocol/pool TVL when known. |
| `site_url` | string | Known protocols only. |
| `protocol_id` | int | Known-protocol matches only. |
| `is_protocol` | bool | Non-EOA entries. |
| `is_coingecko_pool` | bool | Set to `true` only on CoinGecko pool matches. |

##### `raw_token_balances`

Maps `owner_address → { token_address: signed_amount_as_string }`. Amounts
are raw on-chain integers (un-decimalled) and may be negative strings.

##### `token_prices.token_data`

Maps `"{network}:{token_address_lower}"` to `{ price, decimals, symbol }`,
where `price` and `decimals` are strings.

## End-to-end example

WebSocket payload for an attack on a tracked protected address with full
enrichment:

```json
{
  "network": "mainnet",
  "severity": "HIGH",
  "attack_type": "suspicious_contract_call_with_profit",
  "transaction_hash": "0x9c8b6f3b6f6a1b2a3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b",
  "exploit_address": "0x1111111111111111111111111111111111111111",
  "block_number": 21345678,
  "block_timestamp": 1731609600,
  "proc_time": "2026-05-13 09:42:11.503127",
  "attacker_address": "0x2222222222222222222222222222222222222222",
  "input": "0xa9059cbb0000000000000000000000003333333333333333333333333333333333333333000000000000000000000000000000000000000000000000000000003b9aca00",
  "balance_change": 248173.42,
  "matched_traces": "",
  "matched_logs": "",
  "matched_selectors": "0xa9059cbb",
  "victim_address": "0x3333333333333333333333333333333333333333",
  "protocols": {
    "is_full_enrichment": true,
    "hacker_profit": 248173.42,
    "victim_info": {
      "is_eoa": false,
      "address": "0x3333333333333333333333333333333333333333",
      "name": "Aave V3",
      "symbol": "aUSDC",
      "tvl": 4123456789.12,
      "is_coingecko_pool": false,
      "is_protocol": true,
      "site_url": "https://aave.com",
      "protocol_id": 42
    },
    "balance_changes": [
      {
        "address": "0x2222222222222222222222222222222222222222",
        "balance_change_usd": 248173.42,
        "is_eoa": true
      },
      {
        "address": "0x3333333333333333333333333333333333333333",
        "balance_change_usd": -248173.42,
        "is_eoa": false,
        "name": "Aave V3",
        "symbol": "aUSDC",
        "tvl": 4123456789.12,
        "site_url": "https://aave.com",
        "protocol_id": 42,
        "is_protocol": true
      }
    ],
    "raw_token_balances": {
      "0x2222222222222222222222222222222222222222": {
        "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48": "248173420000"
      },
      "0x3333333333333333333333333333333333333333": {
        "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48": "-248173420000"
      }
    },
    "token_prices": {
      "token_data": {
        "ethereum:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48": {
          "price": "1.00",
          "decimals": "6",
          "symbol": "USDC"
        }
      }
    }
  },
  "victim_protocol_id": 42,
  "victim_protocol": "Aave V3",
  "victim_label": "Aave V3 Pool"
}
```

## Confirmed-attack feed

Attacks of certain types are additionally post-processed by an LLM pipeline
(the `/explain-hack` command). When that pipeline confirms a transaction is a
genuine exploit (as opposed to a false positive such as legitimate MEV /
arbitrage or normal protocol activity), the service emits a **confirmed
attack** on a separate feed:

- **Endpoint:** `/ws/confirmed_attacks` (same API-key authentication as
  `/ws/attacks`).
- **Subscription:** to receive these over Telegram or to filter by them, use
  the alert type `confirmed_attack` in your subscription `alerts` list. This is
  independent of the underlying detection type, so you can subscribe to the
  curated confirmed stream without subscribing to the raw attack types.
- **Timing:** confirmed attacks arrive after the regular alert (the LLM
  round-trip takes from a few seconds up to a couple of minutes). Not every
  attack produces a confirmed attack — false positives are dropped.

### Payload

The confirmed-attack payload is a **strict superset** of the standard attack
payload — every field documented above is present with the same name and type,
so existing `/ws/attacks` consumers can parse a confirmed-attack message with
their current parser and ignore the extra fields. The confirmed payload adds:

| Field | Type | Description |
| --- | --- | --- |
| `llm_explanation` | string | LLM-generated, Telegram-ready report describing the exploit: affected protocol, loss amount, token/price, vulnerability type, a short description, and TX / victim links. Always present. |
| `victim_protocol_id` | int \| null | Protocol ID of the matched protected address, or `null`. |
| `victim_protocol` | string \| null | Human-readable protocol name, or `null`. |
| `victim_label` | string \| null | Label of the protected address, or `null`. |

Note the difference from the `/ws/attacks` feed: there, the three
`victim_protocol_*` fields are **omitted** for standard attacks and only appear
in the protected-address variant. On `/ws/confirmed_attacks` they are **always
present**, defaulting to `null` when the attack does not touch a protected
address — so the confirmed feed has a single, uniform shape. Detect a
protected-address confirmed attack with a null check
(`victim_protocol_id != null`) rather than key presence.

The `attack_type` field retains the **underlying** detection type (e.g.
`suspicious_contract_call_with_profit`); the `confirmed_attack` label is used
only for subscription/feed routing, not as the value of `attack_type`.
