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 detections
that an LLM pipeline has reviewed and confirmed. It carries two kinds of
confirmation: exploits that have already drained value, and early detection
findings confirmed before anything is stolen. See
Confirmed feeds. 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:
victim_protocol_id, victim_protocol, victim_label).{
"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 | 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. |
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...". "0x" on suspicious_contract_deployed, which is bound to a deployment rather than a call. |
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. Always 0 on the early-detection types, where nothing has moved yet. |
matched_traces | string | Comma-separated trace identifiers that matched the detection rules. Often "". On suspicious_contract_deployed this field instead carries the human-readable reason the contract was flagged — see Reading the matched_traces verdict. |
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, and "" on suspicious_contract_deployed, where no target has been deduced yet. |
protocols | object | null | Enrichment payload — see Enriched data. Always null on suspicious_contract_deployed: enrichment describes value that moved, and none has. |
victim_protocol_id | int (protected-address variant only) | Defimon-internal numeric id of the protocol that owns the matched protected address, e.g. 42. Distinct from the DeBank protocol_id nested inside protocols — see Two different protocol ids. |
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". |
The payload carries two identifiers with confusingly similar names. They come from different systems and are not interchangeable:
victim_protocol_id (top level) | protocol_id (nested in protocols) | |
|---|---|---|
| Type | int | string |
| Source | Defimon's own protected-address registry | DeBank API |
| Example | 42 | "aave3", "bsc_venus" |
| Present when | The attack touched an address registered for protection | The address resolved to a known DeBank protocol |
| Answers | Which of my protected protocols was hit? | Which public DeFi protocol did DeBank match this address to? |
Do not join one against the other, and do not parse the DeBank slug as a number — it is an opaque string, chain-prefixed on every chain except Ethereum.
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_profithighly_complex_transaction_with_profitmev_tx_with_unusual_profitexploit_in_initcodeabnormal_token_mintingsuspicious_large_transferaccess_transfer_to_suspicious_addressaccess_transfer_to_suspicious_address_with_profitmalicious_proxy_upgradeproxy_upgrade_frontrun (CPIMP attack)suspicious_contract_deployed (early detection — see below)suspicious_governance_proposal (early detection — see below)Two of the types above fire before an attack executes, not after. Nothing has been stolen when they arrive, so they behave differently from every other type and are worth handling as their own class:
suspicious_contract_deployed | suspicious_governance_proposal | |
|---|---|---|
| Fires on | Deployment of a contract that looks like attack tooling | Submission of a governance proposal whose payload looks malicious |
| Bound to | 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 deduced yet | The targeted protocol, when identifiable |
balance_change | 0 | 0 |
protocols | null | Enriched where the target resolves |
severity | HIGH on strong funding provenance or launcher-shaped bytecode, else MEDIUM | HIGH, preliminary |
| Confirmed as | confirmed_exploit | confirmed_attack |
Both are suspicions raised in volume. suspicious_contract_deployed alone
runs at roughly 70 alerts a day across Ethereum, because it is deliberately
tuned for recall rather than precision: the point is to see the attacker's
tooling land while there is still time to act on it. Treat the raw alert as a
watch item and the confirmation as the signal — see
Confirmed feeds.
Both types also arrive on Telegram silently (no notification sound), for the same reason.
matched_traces verdictsuspicious_contract_deployed has no selectors or traces worth reporting, so
it reuses matched_traces to carry the reason it fired, as ·-separated
key/value pairs:
tier=tornado_direct · funding=tornado/direct on mainnet · deployer nonce=0 · many external calls · hardcoded targets
tier — which rung of the detection ladder 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, optionally
with the chain the withdrawal happened on (it need not be the chain the
contract was deployed to) and the intermediate funder's fan-out. unfunded
when the backwards walk found no anonymising source.deployer nonce — the deploying account's nonce, or ? when unknown. A
fresh account is a far stronger signal than an established one.The string is built for a human reader and its field set may grow. Parse it
leniently (split on ·, then on the first =) or treat it as opaque text.
protocols)The protocols field carries enrichment about the victim and the addresses
involved in the transaction. Three shapes are possible:
nullNo enrichment was attached to this alert. The field is emitted as JSON null.
Returned when only victim information is available:
{
"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"
}
}Returned for high-impact alerts and contains, in addition to victim_info,
the full set of address-level balance changes for the transaction:
{
"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 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 | 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"). This is not the same identifier as the top-level victim_protocol_id — see Two different protocol ids. 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 fieldsEach 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 | 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 for the synthetic protocol-summary rows (address: "") — those carry name/tvl/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_balancesMaps owner_address → { token_address: signed_amount_as_string }. Amounts
are raw on-chain integers (un-decimalled) and may be negative strings.
token_prices.token_dataMaps "{network}:{token_address_lower}" to { price, decimals, symbol },
where price and decimals are strings.
WebSocket payload for an attack on a tracked protected address with full enrichment:
{
"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_id": 42,
"victim_protocol": "Aave V3",
"victim_label": "Aave V3 Pool"
}Detections of certain types are additionally post-processed by an LLM pipeline
(the /explain-hack command). When that pipeline confirms a finding is genuine
(as opposed to a false positive such as legitimate MEV / arbitrage or normal
protocol activity), the service re-broadcasts it as a confirmed message:
/ws/confirmed_attacks (same API-key authentication as
/ws/attacks). Both confirmation kinds below arrive here.Confirmation is published under one of two alert types, and the difference is what the confirmed thing is:
confirmed_attack | confirmed_exploit | |
|---|---|---|
| Means | An exploit has executed and value has moved | Attack tooling has been confirmed on-chain, before it is used |
| Produced by | Every LLM-verified type except the one opposite | suspicious_contract_deployed |
| WebSocket | /ws/confirmed_attacks | /ws/confirmed_attacks |
| Defimon Signals | Yes | No — WebSocket only |
The split exists because a confirmed pre-drain warning and a confirmed drain
are different products: with one shared key, a channel could not take one
without the other. confirmed_exploit is therefore the earliest actionable
signal Defimon produces — an attacker's contract, reviewed and confirmed as
attack tooling, while the drain it was built for has not happened yet.
Both types are delivered over WebSocket, so a /ws/confirmed_attacks
consumer receives both by default. There is no alert_type field in the
payload: tell them apart by the underlying detection type —
const isEarlyDetection =
msg.attack_type === "suspicious_contract_deployed";Subscriptions that enumerate alerts must name confirmed_exploit explicitly
to receive it; a subscription with alerts unset means every alert type and
still receives both.
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 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 | Defimon-internal numeric protocol id of the matched protected address, or null. Same identifier as on the /ws/attacks feed — still unrelated to the DeBank protocol_id nested in protocols. |
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 inherits its underlying type's sentinel
values: balance_change is 0, protocols is null, victim_address is
"", and matched_traces carries the tiering verdict. The llm_explanation
is where the substance is — it names what the contract is built to do, and the
target it appears to be built for.
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, or suspicious_contract_deployed for
an early-detection confirmation). The confirmed_attack and confirmed_exploit
labels are used only for subscription routing, never as the value of
attack_type — which is exactly why attack_type is how you tell the two
apart on the socket.
Defimon detects exploits on major chains before they execute and streams them to you: human-readable alerts in Telegram, or structured JSON over WebSocket for your own systems.
signals $50/mo · websocket $200/mo · channel free
@DefimonAlerts