Skip to main content

IDA Agent

This page describes how the IDA Agent is built, how events reach it, how detection results travel back, and which interfaces are available to systems that integrate with it. The business description of the feature is on the IDA Agent page.

IDA is deployed as a small set of cooperating services next to the platform, not inside it. It holds no keys, issues no transactions, and has no write path to the marketplace. Its only inputs are decoded events, and its only outputs are alerts.

Components

ComponentRuntimeResponsibilityPort
ida-backendNode.js, TypeScript, ExpressFeeds, event bus, journal, detector fan-out, alert store and lifecycle, labeling, REST and SSE API8090
ida-detectorPython, FastAPIDetection engines organised into layers; consumes events, returns alerts8091
ida-adminNext.js, ReactIDA Monitor, the operator console3000
ida-outputNode.js, ExpressReference alert sink, the minimal implementation of a downstream consumer8092

The services are orchestrated with Docker Compose. A development deployment additionally runs a local EVM node with the DEUSS contracts deployed on it, which makes the whole pipeline reproducible without access to a shared chain. Persistence is configurable per module: an in-memory backend for disposable environments, MongoDB where the history must survive a restart.

Architecture Overview

Screenshot

Event Ingestion

A feed is the unit of ingestion. Each feed has an identifier, a type and its own configuration, and the set of active feeds is a matter of deployment configuration rather than of code. The available types cover chain listeners that subscribe to contract logs over a websocket and decode them against the deployed ABIs, file and fixture sources used for reproducible test runs, scenario generators that synthesise defined behavioural patterns, and webhook endpoints for external producers.

Every event, regardless of its origin, passes through the same normalisation step before it reaches the bus:

  • receivedTimestamp is set to the moment IDA accepted the event.
  • eventTimestamp is derived from the event itself, falling back through the block time and finally to the reception time when the producer supplies nothing better.
  • The event source is overwritten with the identifier of the feed that delivered it, so provenance cannot be forged by the payload.

Both timestamps are retained and are selectable throughout the console, because the difference between them is what distinguishes live activity from a replayed or delayed stream.

External producers

An external system delivers events by posting them to the webhook endpoint of its own feed:

POST /api/v1/feed/webhook/<feed-id>
x-api-key: <the API key configured for that feed>

The key is issued per producer and is unrelated to the credential that protects the administrative API. A request with the wrong key is rejected with 401, and a request to a feed that is not currently started is rejected with 503. Payloads are bounded: at most 1000 events per batch, field values limited in length, and keys beginning with an underscore rejected, since those are reserved for internal bookkeeping.

A producer may additionally expose a small control interface, which lets IDA list the scenarios that the producer can run, query its status, and start or stop a run. This is what makes a recorded or generated dataset drivable from the console instead of from a shell on the producer host. Feed health is reported by the feed itself and surfaces in the console as a running, stopped or error state, together with the number of events emitted and the time of the last one.

Detection

Detection modules are external to the backend and are addressed over HTTP, which keeps the detection stack independent of the ingestion stack. It also means a partner can supply a detector in any language, as long as it honours the interface.

Detector interface

A detector exposes three endpoints:

EndpointPurpose
POST /eventsReceives normalised events, one request per event, delivered in order
GET /healthReports liveness and basic counters; polled by the backend
GET /capabilitiesOptional. Returns the capability manifest described below

Events are delivered to a given detector sequentially, so a module that maintains state across events sees them in the order they were ingested. Alerts travel back on a separate call, POST /api/v1/detectors/{id}/alerts, which decouples the moment of detection from the moment the triggering event was delivered. A detector that needs a window of history before it can decide is therefore not forced to answer synchronously.

An alert is submitted in a compact shape:

{
"severity": "critical | high | medium | low",
"title": "short human-readable summary",
"description": "the reasoning behind the finding",
"confidence": 0.87,
"relatedEventIds": ["..."],
"data": { "engine-specific evidence": "..." }
}

The backend supplements the record with its own identifier, the time of receipt, the initial status and the audit trail. Detectors normally apply their own gating before submitting, so that findings below a configured minimum severity or confidence never reach the operator queue.

Capability manifests

A detector describes itself. The manifest it returns enumerates its detection layers, the engines within each layer, and every alert type an engine can raise, together with the meaning of the alert type and the condition that fires it. The console renders this hierarchy directly, which is what makes the coverage of the platform auditable at any moment rather than documented separately.

The backend treats the manifest as untrusted input. It is rebuilt field by field rather than passed through, collection sizes are bounded, and the summary counters are recomputed from the validated content instead of being taken from the producer. The endpoint answers 200 with the document, 400 when the detector type has no such concept, and 502 when a detector that should provide one cannot be reached.

Alert type catalogue

The tables below are the manifest of the reference detector as it runs today: five layers, eight engines, twenty-five alert types and three shared services. Layer 4 is not used. Layer 0 reconstructs protocol state from the event stream and raises no alerts of its own; every other layer builds on that state.

The severity given is the typical value for the type. A few types escalate on the strength of the evidence, and two of them, marked below, sit under the default gating threshold and are therefore not delivered unless the threshold is lowered.

Three services are shared by the engines rather than owned by any one of them: the cluster substrate, which merges wallets and entities that behave as one actor, the fair-value model, which maintains a reference price and yield for each instrument, and the actor profiler, which maintains per-entity behavioural profiles.

Layer 1, hard rules

Engine rules, stateless. Protocol invariants broken outright: unauthorised minting, unregistered participants, privilege escalation.

Alert typeSeverityFires when
R1_UNAUTHORIZED_MINTcriticalA transfer originates from the zero address and the calling address is not in the learned set of known protocol contracts
R2_UNREGISTERED_SENDERmediumA non-mint transfer's sender is neither a registered entity wallet, a previously seen participant, nor a protocol contract
R2_UNREGISTERED_RECEIVERlow (gated)A transfer's receiver is neither a registered entity wallet, a previously seen participant, nor a protocol contract
R3_UNREGISTERED_BOND_ISSUERhighA bond publication names an issuer wallet that is not a known participant
R4_BOND_PUBLICATION_BURSTmediumA publisher registers more than the configured number of bonds inside the burst window
R5_BOND_ISSUED_WITHOUT_PUBLISHhighAn issuance carries an ISIN with no prior publication in state
R6_EXCESSIVE_ROLE_FLAGShighA role bitmask exceeds the configured maximum for the emitting contract
R7_NOOP_STATUS_UPDATElow (gated)An entity status update reports an identical old and new status
R8_SELF_ROLE_GRANTcriticalA role grant has the same address as both grantee and caller

Layer 2, statistical anomaly

Engine statistical, stateful. Behaviour that is unusual for this market rather than forbidden by it.

Alert typeSeverityFires when
S1_STATISTICAL_ANOMALYmediumThe fitted isolation forest scores the entity's current profile below the decision margin, once the minimum-sample and minimum-entity gates are met

Layer 3, graph anomaly

Engine graph, stateless. Structure that no single transaction reveals.

Alert typeSeverityFires when
G1_CIRCULAR_TRANSFERhighThe current transfer closes a path back to its sender within the cycle window, in the same token, over edges where both ends are end-user wallets
G2_FAN_OUTmediumA sender reaches at least the configured number of distinct recipients inside the fan-out window, and is neither a protocol contract nor an issuer or publisher wallet

Layer 5, market microstructure

Five engines covering price formation, position building and timing. This is where market abuse patterns in the MAR sense are recognised.

Alert typeEngineSeverityFires when
W_SELF_DEALwashhighThe deal's buyer wallet and the offer's owner wallet are the same address or belong to the same registered entity. Escalates to critical when those self-deals form a rising price ramp
W_RING_WASHwashmediumBuyer and seller are separate entities that the cluster substrate merged on behavioural evidence, and the cluster has repeatedly traded with itself on the bond at a rising price
P_YIELD_DEVIATIONyieldmediumThe executed yield deviates from the cluster-excluded fair value beyond the significance threshold, over enough reference prints, and no proportionate credit update explains it
P_FALSE_MACROyieldhighA deviation is significant against the cluster-excluded baseline but not against the baseline that still contains the actor's own prints: the apparent broad move was one cluster
P_YIELD_DRIFTyieldmediumThe bond's median yield deviates from its cohort median beyond the significance threshold, with enough prints on both sides
P_UNJUSTIFIED_CREDITyieldhighA significant move coincides with a scoring update, but the observed move exceeds what the change in default probability justifies, or moves in the opposite direction
C_CONCENTRATIONconcentrationmediumA cluster's share of issued supply reaches the alert threshold, with separate thresholds for multi-wallet and single-wallet holdings
C_CONCEALED_CONTROLconcentrationcriticalA qualifying corner spans two or more entities and no single entity holds a majority of the cluster's position
C_FLOAT_REMOVALconcentrationhighA qualifying corner's sell-side offers, measured against its holdings, fall below the two-sided ratio
C_SECTOR_HOARDINGconcentrationhighA cluster spanning two or more instruments of one cohort holds the alert share of that cohort's distributed float
F_WAVEflashmediumSeveral distinct buyer clusters trade the bond inside the wave window at a qualifying price ramp
F_INSIDER_DUMPflashhighDuring a wave, the selling cluster disposes of a large fraction of its pre-wave position at a substantial uplift over the pre-wave median
X_CORNER_THEN_MARKcorrelationcriticalA mark-leg finding names a cluster and bond for which a corner-leg finding was recorded inside the correlation window

Thresholds referenced above are configuration values of the detector, not constants of the protocol, and the console shows the exact condition and the parameter names for each type.

Alert Lifecycle

An alert is created with the status NEW and moves through REVIEWED, ESCALATED and finally one of the two terminal states, RESOLVED or FALSE-ALARM. Alerts in the first three states count as active. Every transition is appended to the alert's history with its author and a note, and closing an alert requires that note, so no finding is dismissed without a recorded reason.

Independently of the operational status, an alert carries a classification that expresses the legal reading of the finding: Anomaly for machine-identified non-standard behaviour, Suspicious for behaviour a reviewer has assessed as undesirable, and Potential Abuse for a case that may fall under the market abuse regulation. The separation is deliberate: the operational status records what the team did with the case, the classification records what the case is.

Delivered alerts are handed to the configured alert outputs through a single-consumer queue, which guarantees ordering and prevents a slow downstream consumer from reordering the stream. Outputs are pluggable; the reference implementation is a REST sink that receives each alert as it is stored. The same alerts are pushed to any live subscriber of the alert stream, which is how the console updates without polling.

Web2 Labeling

On-chain identifiers carry no meaning for a reviewer, so IDA maintains a separate labeling module that resolves them to real-world entities. Label sources write raw facts as they observe them, for example from registry events or from the deployed contract set. Responders read those facts and assemble the answer for a given identifier, resolving a wallet through its owning entity, or a bond through its issuer and publisher.

Three label types are supported today: wallet, entity and bond. Labels are applied when data is read and are never written back into the stored event, so enrichment can improve over time without altering the recorded evidence. Resolution is available over the API as well as in the console, and it is what allows an alert to name the parties involved rather than their addresses.

REST API and Streaming

The backend exposes a documented REST API, published as an OpenAPI specification and browsable at GET /api/docs. The endpoints are grouped by concern:

GroupCovers
HealthLiveness of the service, unauthenticated
JournalEvent queries, single event retrieval, statistics, known event types
WalletsWallet statistics and the registered wallet set
FeedsFeed inventory, per-feed status, scenario listing, start, stop, pause, resume and playback speed
DetectorsDetector inventory, health, statistics, capabilities, forced health check, test alert, and the inbound alert endpoint
AlertsAlert queries, statistics, single alert retrieval, status transitions, configured outputs
LabelsLabel queries and resolution by type and identifier
StreamsServer-sent event streams for events and for alerts

Two live streams are available, GET /api/v1/stream/events and GET /api/v1/stream/alerts. Both are server-sent event endpoints, which keeps the console current without polling and gives an integrator a simple way to mirror the stream.

Access is controlled by a service credential presented in request headers. Only the health endpoint is public, and the producer webhooks authenticate with their own per-feed keys rather than with the service credential.

Integration with DEUSS

IDA consumes the events of the deployed DEUSS contract set, currently around fifteen contracts covering entity and bond registries, the token, interest discovery, the marketplace, escrow, wallet factories and the EBSI-facing components. The contract ABIs and the deployment descriptor are supplied to the backend as deployment artefacts, and contract names are resolved from them, so no contract address is compiled into IDA. A redeployment is a configuration change rather than a code change.

In the outbound direction, alerts leave through the pluggable alert outputs. Delivery into a DEUSS back-office queue is a natural next step and is not implemented today; the reference sink demonstrates the contract that such a consumer would implement.