docs

Attack WebSocket Message

View raw Markdown

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:

  • 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

{ "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

FieldTypeDescription / example values
networkstringSource chain. One of mainnet, bsc, arbitrum, polygon, optimism, base, avax, hyperliquid.
severitystring | nullRisk level. Typical values: "HIGH", "MEDIUM", "LOW", "CRITICAL". May be null when severity could not be computed.
attack_typestringType of detection that fired. See Attack types.
transaction_hashstring (0x + 64 hex)The transaction the alert is bound to.
exploit_addressstring (0x + 40 hex)Contract being interacted with (the suspected exploit / target).
block_numberintBlock height containing the transaction.
block_timestampintUnix epoch seconds, e.g. 1731609600.
proc_timestringUTC timestamp of when the alert was generated, e.g. "2026-05-13 09:42:11.503127".
attacker_addressstring (0x + 40 hex)EOA / contract that initiated the suspected attack.
inputstringRaw transaction calldata, e.g. "0xa9059cbb0000...". "0x" on suspicious_contract_deployed, which is bound to a deployment rather than a call.
balance_changefloat | nullNet 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_tracesstringComma-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_logsstringComma-separated log identifiers that matched. Often "".
matched_selectorsstringComma-separated 4-byte selectors that matched, e.g. "0xa9059cbb,0x23b872dd". Often "".
victim_addressstring | nullAddress 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.
protocolsobject | nullEnrichment payload — see Enriched data. Always null on suspicious_contract_deployed: enrichment describes value that moved, and none has.
victim_protocol_idint (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_protocolstring (protected-address variant only)Human-readable protocol name, e.g. "Aave V3".
victim_labelstring (protected-address variant only)Label associated with the protected address, e.g. "Aave V3 Pool".

Two different protocol ids

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)
Typeintstring
SourceDefimon's own protected-address registryDeBank API
Example42"aave3", "bsc_venus"
Present whenThe attack touched an address registered for protectionThe address resolved to a known DeBank protocol
AnswersWhich 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.

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
  • 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, 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_deployedsuspicious_governance_proposal
Fires onDeployment of a contract that looks like attack toolingSubmission of a governance proposal whose payload looks malicious
Bound toThe deployment transactionThe proposal transaction
exploit_addressThe newly deployed contractThe contract the proposal targets
attacker_addressThe deployerThe proposer
victim_address"" — no target deduced yetThe targeted protocol, when identifiable
balance_change00
protocolsnullEnriched where the target resolves
severityHIGH on strong funding provenance or launcher-shaped bytecode, else MEDIUMHIGH, preliminary
Confirmed asconfirmed_exploitconfirmed_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.

Reading the matched_traces verdict

suspicious_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.
  • Everything after those three is bytecode evidence, most incriminating first, and is present only for contracts with analysable bytecode.

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.

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:

{ "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 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
FieldTypeExample
is_eoaboolfalse
addressstring"0x3333...3333"
namestring | null"Aave V3", "Unverified contract", or null
symbolstring | null"aUSDC", "USDC", or null
tvlnumber | nullUSD TVL when known; null otherwise.
is_coingecko_poolbooltrue when the address was classified as a CoinGecko-tracked liquidity pool.
is_protocolbooltrue for known protocols.
site_urlstring | nullProtocol site URL, e.g. "https://aave.com".
protocol_idstring | nullDeBank 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 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):

FieldTypeWhen present
addressstringAlways (may be "" for synthetic protocol-summary rows).
balance_change_usdnumberAlways. Negative = loss, positive = gain. Synthetic protocol-summary rows use 1 as a sentinel.
is_eoaboolAlways.
namestring | nullSet unless the address is an EOA. "Unverified contract" for unresolved contracts.
symbolstring | nullToken symbol when known.
tvlnumber | nullProtocol/pool TVL when known.
site_urlstringKnown protocols only.
protocol_idstringDeBank 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_protocolboolNon-EOA entries.
is_coingecko_poolboolSet 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:

{ "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" }

Confirmed feeds

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:

  • Endpoint: /ws/confirmed_attacks (same API-key authentication as /ws/attacks). Both confirmation kinds below arrive here.
  • Timing: confirmations arrive after the regular alert (the LLM round-trip takes from a few seconds up to a couple of minutes). Not every detection produces a confirmation — false positives are dropped.

Two confirmation types

Confirmation is published under one of two alert types, and the difference is what the confirmed thing is:

confirmed_attackconfirmed_exploit
MeansAn exploit has executed and value has movedAttack tooling has been confirmed on-chain, before it is used
Produced byEvery LLM-verified type except the one oppositesuspicious_contract_deployed
WebSocket/ws/confirmed_attacks/ws/confirmed_attacks
Defimon SignalsYesNo — 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.

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 consumers can parse a confirmed-attack message with their current parser and ignore the extra fields. The confirmed payload adds:

FieldTypeDescription
llm_explanationstringLLM-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_idint | nullDefimon-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_protocolstring | nullHuman-readable protocol name, or null.
victim_labelstring | nullLabel 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.

Put Defimon in your stack

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

© 2026 Defimon by Decurity

Powered by QuickNode