# Attack Alert Messages (WebSocket)

This document describes the JSON structure of the WebSocket messages Defimon
sends when it detects an on-chain attack.

Messages are delivered on two feeds:

- **`/ws/attacks`**: every detected attack, delivered immediately. Most of this
  document describes this feed.
- **`/ws/confirmed_attacks`**: a curated, lower-noise stream of detections that
  an LLM pipeline has reviewed and confirmed. It carries two kinds of
  confirmation: exploits that have already drained funds, and *early-detection*
  findings confirmed before anything is stolen. See
  [Confirmed feed](#confirmed-feed).

A client can subscribe to both feeds at the same time.

Each message is a single JSON object. The payload comes in two variants:

- **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 two extra fields,
  `victim_protocol` and `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": "Aave V3",
  "victim_label": "Aave V3 Pool"
}
```

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

## 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. Typically one of `"CRITICAL"`, `"HIGH"`, `"MEDIUM"`, `"LOW"`. `null` when severity could not be computed. |
| `attack_type` | string | The detection that fired. See [Attack types](#attack-types). |
| `transaction_hash` | string (0x + 64 hex) | The transaction that triggered the alert. |
| `exploit_address` | string (0x + 40 hex) | The contract the transaction interacts with (the suspected exploit contract or target). |
| `block_number` | int | Number of the block containing the transaction. |
| `block_timestamp` | int | Unix epoch seconds, e.g. `1731609600`. |
| `proc_time` | string | UTC time at which the alert was generated, e.g. `"2026-05-13 09:42:11.503127"`. |
| `attacker_address` | string (0x + 40 hex) | EOA or contract that initiated the suspected attack. |
| `input` | string | Raw transaction calldata, e.g. `"0xa9059cbb0000..."`. `"0x"` for `suspicious_contract_deployed`, which is tied to a deployment rather than a call. |
| `balance_change` | float \| null | Net change in the attacker's USD balance from this transaction, e.g. `248173.42`. Can be `0.0` or negative; `null` when not computed. Always `0` for the [early-detection types](#early-detection-pre-drain-types), since nothing has moved yet. |
| `matched_traces` | string | Comma-separated identifiers of the traces that matched the detection rules. Often `""`. For `suspicious_contract_deployed`, this field instead carries a human-readable reason the contract was flagged; see [Reading the `matched_traces` verdict](#reading-the-matched_traces-verdict). |
| `matched_logs` | string | Comma-separated identifiers of the logs that matched. Often `""`. |
| `matched_selectors` | string | Comma-separated 4-byte selectors that matched, e.g. `"0xa9059cbb,0x23b872dd"`. Often `""`. |
| `victim_address` | string \| null | The address with the largest USD loss. `null` for attack types without a single identifiable victim, and `""` for `suspicious_contract_deployed`, where no target has been identified yet. |
| `protocols` | object \| null | Enrichment data; see [Enriched data](#enriched-data-protocols). Always `null` for `suspicious_contract_deployed`, because enrichment describes value that has moved, and none has. |
| `victim_protocol` | string *(protected-address variant only)* | Human-readable name of the protocol that owns the matched protected address, e.g. `"Aave V3"`. |
| `victim_label` | string *(protected-address variant only)* | Label associated with the protected address, e.g. `"Aave V3 Pool"`. |

### Attack types

For network-wide exploit monitoring (alerts not tied to a specific protected
address), `attack_type` is one of:

- `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`
- `malicious_proxy_upgrade`
- `proxy_upgrade_frontrun` (CPIMP attack)
- `suspicious_contract_deployed` (early detection; see below)
- `suspicious_governance_proposal` (early detection; see below)

### Early detection (pre-drain) types

Two of the types above fire *before* an attack executes. Nothing has been
stolen when they arrive, so they behave differently from every other type and
should be handled as a separate class:

| | `suspicious_contract_deployed` | `suspicious_governance_proposal` |
| --- | --- | --- |
| Fires on | Deployment of a contract that looks like an attack contract | Submission of a governance proposal whose payload looks malicious |
| `transaction_hash` | The deployment transaction | The proposal transaction |
| `exploit_address` | The newly deployed contract | The contract the proposal targets |
| `attacker_address` | The deployer | The proposer |
| `victim_address` | `""` (no target identified yet) | The targeted protocol, when identifiable |
| `balance_change` | `0` | `0` |
| `protocols` | `null` | Enriched when the target can be resolved |
| `severity` | `HIGH` when the deployer's funding trail is strongly suspicious or the bytecode looks like an exploit contract; otherwise `MEDIUM` | `HIGH` (preliminary) |
| Confirmed as | `confirmed_exploit` | `confirmed_attack` |

Both types are high-volume and noisy by design. `suspicious_contract_deployed`
alone produces roughly 70 alerts a day on Ethereum, because it is tuned for
recall rather than precision: the goal is to spot an attacker's contract as
soon as it is deployed, while there is still time to act. Treat the raw alert
as something to watch and the confirmation as the signal to act on; see
[Confirmed feed](#confirmed-feed).

#### Reading the `matched_traces` verdict

`suspicious_contract_deployed` has no selectors or traces worth reporting, so
it uses `matched_traces` to explain why it fired, as key/value pairs separated
by ` · `:

```
tier=tornado_direct · funding=tornado/direct on mainnet · deployer nonce=0 · many external calls · hardcoded targets
```

- `tier`: which detection tier matched. One of `tornado_direct`,
  `tornado_1hop`, `tornado_2hop`, `railgun`, `operator_scored`,
  `unfunded_tight`.
- `funding`: where the deployer's gas came from, as `family/hop`. It may also
  include the chain the withdrawal happened on (which need not be the chain
  the contract was deployed to) and the intermediate funder's fan-out (how
  many addresses it funded). `unfunded` when tracing the funding backwards
  found no anonymizing source.
- `deployer nonce`: the deploying account's nonce, or `?` when unknown. A
  fresh account is a much stronger signal than an established one.
- Everything after those three is bytecode evidence, most incriminating first.
  It is present only for contracts whose bytecode could be analyzed.

The string is meant for human readers, and new fields may be added. Parse it
leniently (split on ` · `, then on the first `=`) or treat it as opaque text.

### Enriched data (`protocols`)

The `protocols` field carries enrichment data about the victim and the
addresses involved in the transaction. It takes one of three shapes:

#### 1. `null`

No enrichment is 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": "aave3"
  }
}
```

#### 3. Full enrichment

Returned for high-impact alerts. In addition to `victim_info`, it contains
every address-level balance change in 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": "aave3"
  },
  "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": "aave3",
      "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 is a liquidity pool tracked by CoinGecko. |
| `is_protocol` | bool | `true` for known protocols. |
| `site_url` | string \| null | Protocol website, e.g. `"https://aave.com"`. |
| `protocol_id` | string \| null | **DeBank** protocol id: a string slug, not a number. Unprefixed on Ethereum (e.g. `"aave3"`), prefixed with the DeBank chain id elsewhere (e.g. `"bsc_venus"`, `"matic_aave3"`). `null` when the victim is not a known DeBank protocol. On lightweight enrichment the key may be **absent** rather than `null`, so read it with a safe accessor. |

##### `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 means a loss, positive a gain. Synthetic protocol-summary rows use `1` as a placeholder. |
| `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 or pool TVL when known. |
| `site_url` | string | Known protocols only. |
| `protocol_id` | string | DeBank protocol matches only, e.g. `"aave3"`. Same slug format as `victim_info.protocol_id`. Omitted for CoinGecko pools, Etherscan-named contracts, unresolved contracts, and the synthetic protocol-summary rows (`address: ""`); those rows carry `name`, `tvl` and `site_url` but no id. |
| `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 in the token's smallest unit (not adjusted for
decimals) and can be negative.

##### `token_prices.token_data`

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

## End-to-end example

A WebSocket payload for an attack on a 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": "aave3"
    },
    "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": "aave3",
        "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": "Aave V3",
  "victim_label": "Aave V3 Pool"
}
```

## Confirmed feed

Detections of certain types also go through an LLM review. When the review
confirms that a finding is genuine, rather than a false positive such as
legitimate MEV, arbitrage or normal protocol activity, the service rebroadcasts
it as a **confirmed** message:

- **Endpoint:** `/ws/confirmed_attacks`, with the same API-key authentication
  as `/ws/attacks`. Both confirmation types below arrive here.
- **Timing:** a confirmation arrives after the regular alert, because the LLM
  review takes anywhere from a few seconds to a couple of minutes. Not every
  detection is confirmed: false positives are dropped.

### Two confirmation types

Confirmed messages are published under one of two alert types, depending on
what was confirmed:

| | `confirmed_attack` | `confirmed_exploit` |
| --- | --- | --- |
| Means | An exploit has executed and funds have moved | An attack contract has been confirmed on-chain, before it is used |
| Produced by | All other LLM-reviewed types | `suspicious_contract_deployed` |
| WebSocket | `/ws/confirmed_attacks` | `/ws/confirmed_attacks` |
| [Defimon Signals](/subscribe#signals) (Telegram) | Yes | No, WebSocket only |

`confirmed_exploit` is the **earliest actionable signal Defimon produces**: an
attacker's contract, reviewed and confirmed as an attack tool, before the
drain it was built for has happened.

**Both types are delivered over WebSocket**, so a `/ws/confirmed_attacks`
client receives both by default. The payload has no `alert_type` field; use
the underlying detection type to tell them apart:

```js
const isEarlyDetection =
  msg.attack_type === "suspicious_contract_deployed";
```

If your subscription restricts alert types (the `alerts` setting), it must
list `confirmed_exploit` explicitly to receive it. A subscription with
`alerts` unset receives every alert type, including both of these.

### Payload

The confirmed payload, for either type, 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` clients can parse a confirmed message
with their current parser and ignore the extra fields. The confirmed payload
adds:

| Field | Type | Description |
| --- | --- | --- |
| `llm_explanation` | string | LLM-generated report, formatted for Telegram, describing the exploit: affected protocol, loss amount, token and price, vulnerability type, a short description, and links to the transaction and victim. Always present. |
| `victim_protocol` | string \| null | Human-readable protocol name, or `null`. |
| `victim_label` | string \| null | Label of the protected address, or `null`. |

A confirmed early-detection message keeps its underlying type's placeholder
values: `balance_change` is `0`, `protocols` is `null`, `victim_address` is
`""`, and `matched_traces` carries the tier verdict. The useful information is
in `llm_explanation`, which describes what the contract is built to do and
which target it appears to be aimed at.

This differs from the `/ws/attacks` feed. There, `victim_protocol` and
`victim_label` are **omitted** for standard attacks and appear only in the
protected-address variant. On `/ws/confirmed_attacks` they are **always
present** and default to `null` when the attack does not touch a protected
address, so every confirmed message has the same shape. To detect a confirmed
attack on a protected address, check `victim_protocol != null` rather than
whether the key exists.

The `attack_type` field keeps the **underlying** detection type (e.g.
`suspicious_contract_call_with_profit`, or `suspicious_contract_deployed` for
an early-detection confirmation). `confirmed_attack` and `confirmed_exploit`
are used only for subscription routing and never appear as `attack_type`
values.
