> ## Documentation Index
> Fetch the complete documentation index at: https://docs.didit.me/llms.txt
> Use this file to discover all available pages before exploring further.

# Rules on Events

> Velocity, ratio, ordered-sequence and cross-subject correlation rules over event data - written in the same rule builder as your transaction rules.

[Events](/transaction-monitoring/events) are evaluated by the same engine as transactions. There is one rule model, one condition vocabulary and one set of actions - a rule simply declares which record kinds it applies to.

## Applies to

`scope.record_types` decides what a rule fires on.

| `record_types`               | Fires on                            |
| ---------------------------- | ----------------------------------- |
| omitted or `["transaction"]` | Transactions only - **the default** |
| `["event"]`                  | Events only                         |
| `["transaction", "event"]`   | Both                                |

<Note>
  Leaving it out keeps a rule transaction-only. Every rule written before events existed behaves exactly as it did, and a rule never starts matching logins because its scope happened to be empty.
</Note>

In the console this is the **Applies to** selector on a rule's Trigger step. Choosing Events reveals an **Event categories** checklist that narrows the rule further; leave it all unchecked to evaluate every category.

```json theme={null}
{
  "title": "Failed login burst",
  "scope": {
    "record_types": ["event"],
    "event_categories": ["security"]
  },
  "aggregation": [
    {
      "metric": "count",
      "operator": "gte",
      "value": 3,
      "window": "30m",
      "filters": {
        "subject_vendor_data": "__current__",
        "event.action_type": "login_failed"
      }
    }
  ],
  "actions": [
    { "type": "add_score", "value": 40 },
    { "type": "change_status", "value": "IN_REVIEW" }
  ]
}
```

`"__current__"` means "the same value as the event being evaluated" - that is how a rule says *per subject*, *per device* or *per round* without naming one.

## Event fields

Conditions read namespaced paths. The transaction vocabulary you already know (`action_type`, `subject_vendor_data`, `subject_ip_country`, `custom_values.<key>`) also resolves on an event.

| Group                             | Paths                                                                                                                                                                                                                                                                                        |
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Event                             | `event.category`, `event.action_type`, `event.occurred_at`, `event.amount`, `event.currency`, `event.direction`, `event.session_id`, `event.session_sequence`                                                                                                                                |
| Device and network                | `event.device.fingerprint`, `event.device.persistent_device_id`, `event.device.bot_score`, `event.ip.address`, `event.ip.country`, `event.ip.asn_number`, `event.ip.asn_organization`, `event.ip.is_vpn_or_tor`, `event.ip.is_data_center`, `event.ip.proxy_type`                            |
| Gameplay                          | `event.gameplay.game_id`, `event.gameplay.table_id`, `event.gameplay.round_id`, `event.gameplay.hand_id`, `event.gameplay.match_id`, `event.gameplay.market_id`, `event.gameplay.side`, `event.gameplay.role`, `event.gameplay.counterparty_vendor_data`, `event.gameplay.net_result_amount` |
| Payment and dispute               | `event.payment.reference_id`, `event.payment.payment_fingerprint`, `event.payment.instrument_reference_hash`, `event.payment.withdrawal_address_hash`, `event.dispute.dispute_id`, `event.dispute.chargeback_id`                                                                             |
| Security and responsible gambling | `event.security.changed_field`, `event.responsible_gambling.identity_match_ref`                                                                                                                                                                                                              |
| Bot and assistance telemetry      | `event.bot.timing_variance_ms`, `event.bot.actions_per_minute`, `event.bot.pointer_entropy`, `event.bot.reaction_time_ms_p50`, `event.bot.assistance_indicator`                                                                                                                              |
| Subject signals                   | `subject.email.risk_score`, `subject.email.social_count`, `subject.email.is_disposable`, `subject.phone.risk_score`, `subject.phone.line_type`, `subject.phone.social_count`                                                                                                                 |
| Anything else                     | `event.payload.<key>`, `custom_values.<key>`                                                                                                                                                                                                                                                 |

Subject signals are cached on the monitored user, not recomputed per event, so a rule can read them at ingestion speed.

## Aggregations

### Velocity

The reducers you already use on transactions - `count`, `sum`, `avg`, `min`, `max`, `distinct_count` - over a rolling `window` (`30m`, `24h`, `7d`, `30d`).

### Ratio

Two reducers over the same window. This is how wager-to-deposit, withdrawal-to-deposit, bonus-to-deposit and chargeback rates are expressed.

```json theme={null}
{
  "metric": "ratio",
  "window": "24h",
  "filters": { "subject_vendor_data": "__current__" },
  "numerator":   { "metric": "sum", "field": "event.amount", "filters": { "event.action_type": "bet_placed" } },
  "denominator": { "metric": "sum", "field": "event.amount", "filters": { "event.action_type": "deposit_completed" } },
  "operator": "lte",
  "value": 0.1
}
```

Deposit 1,000, wager 20, request a withdrawal, and the ratio is 0.02 - classic minimal-play laundering.

<Note>
  A ratio with **no denominator never matches**. No deposits is not a ratio of zero, it is no evidence, and firing there would flag every brand-new user.
</Note>

### Sequence

Ordered predicates inside a window. Each step matches on `action_type`, `event_category` or an arbitrary `match` filter, and can require `min_count` occurrences before the sequence advances.

```json theme={null}
{
  "metric": "sequence",
  "key": "ato_drain",
  "window": "24h",
  "steps": [
    { "event_category": "security", "action_type": "login_failed", "min_count": 3 },
    { "event_category": "security", "action_type": "mfa_changed" },
    { "event_category": "payment",  "action_type": "withdrawal_address_changed" },
    { "event_category": "payment",  "action_type": "withdrawal_requested" }
  ]
}
```

Credential stuffing, then the MFA takeover, then the payout redirect, then the drain. The rule fires on the event that completes the chain, and the matched evidence lists the event ids of every step.

Sequence state is kept per rule and subject, so it advances as events arrive instead of rescanning the timeline. Progress older than the window is discarded rather than counted.

### Correlation

A cross-subject join: reduce over every event that shares this event's group.

```json theme={null}
{
  "metric": "correlation",
  "window": "7d",
  "pair": true,
  "group_by": ["subject_vendor_data", "event.gameplay.counterparty_vendor_data"],
  "filters": { "event.action_type": "p2p_settlement" },
  "correlate": {
    "metric": "sum",
    "field": "event.gameplay.net_result_amount",
    "filters": { "event.gameplay.role": "winner" }
  },
  "operator": "gte",
  "value": 500
}
```

`"pair": true` matches the two ends as an unordered set, so A→B and B→A land in the same group - which is what lets one rule see both halves of a collusion. Chip dumping, opposite betting and affiliate self-referral are all this shape.

Always give a correlation rule narrowing `filters`. Without them it considers every event in the window.

## Real-time and asynchronous rules

Some rules can answer inside the ingestion request and some cannot.

| Runs                               | Which rules                                                         |
| ---------------------------------- | ------------------------------------------------------------------- |
| **In the request**                 | Velocity, ratio and bounded sequences (window up to 24 hours)       |
| **Within minutes, asynchronously** | Correlation rules, and sequences with a window longer than 24 hours |

An event whose rules are still pending comes back with `"correlation_pending": true`. Its status and score update in place once the asynchronous pass completes; re-running is idempotent, so a rule never applies its score twice.

This split is deliberate: a cross-subject ring query must not make a login wait.

## Actions

The same action vocabulary as transaction rules - `add_score`, `change_status`, `add_tags`, `add_note`, `add_to_list`, `open_case`.

Two differences on events:

* `change_status` to `AWAITING_USER` is a transaction-only outcome (there is no event remediation flow) and is applied as `IN_REVIEW`.
* `open_case` links the case to the **subject** rather than to a transaction, and records the matched event id on the case.

## Evidence

Every match stores what it saw: the aggregation window, the computed value, the expected value, sampled event ids, and the **source of each matched field**.

| Source     | Meaning                                                   |
| ---------- | --------------------------------------------------------- |
| `server`   | Didit derived it (IP, user agent, origin). Authoritative. |
| `sdk`      | The Didit SDK reported it from the device.                |
| `customer` | Your systems supplied it.                                 |

An analyst can therefore tell a server-stamped IP from a client-supplied hint before acting on a match. The console shows the source next to each matched field in the event drawer.

## Worked examples

<AccordionGroup>
  <Accordion title="Minimal-play laundering">
    Deposit, token play, fast withdrawal. Condition `event.action_type eq withdrawal_requested`, plus the wager-to-deposit ratio above at `lte 0.1` over `24h`.
  </Accordion>

  <Accordion title="Friendly fraud">
    Ratio of `count(chargeback_opened)` to `count(deposit_completed)` per subject over `30d`, `gte 0.5`. Group by `event.payment.payment_fingerprint` instead of the subject to catch one instrument across accounts.
  </Accordion>

  <Accordion title="Account-takeover drain">
    The `ato_drain` sequence above, with `add_score 90` and `change_status DECLINED`.
  </Accordion>

  <Accordion title="Chip dumping">
    The pair correlation above. Raise `value` to the amount that is material for your tables, and add `event.gameplay.game_id` to `group_by` to keep games separate.
  </Accordion>

  <Accordion title="Bot play and real-time assistance">
    Conditions on `event.action_type eq bot_signal_observed`, `event.bot.timing_variance_ms lte 10` and `event.device.bot_score gte 90`, with `add_tags`. Combine with a `distinct_count` of `event.device.fingerprint` per IP to find a farm rather than one script.
  </Accordion>

  <Accordion title="Self-exclusion evasion">
    Condition on `event.action_type eq self_exclusion_match` and `event.payload.re_registration_attempt eq true`, escalating with `open_case`.
  </Accordion>
</AccordionGroup>

## Next

<CardGroup cols={2}>
  <Card title="Event ingestion" icon="bolt" href="/transaction-monitoring/events">
    The envelope, the ten categories and the batch contract.
  </Card>

  <Card title="Rules API" icon="code" href="/transaction-monitoring/rules-api">
    Create, update and back-test rules programmatically.
  </Card>
</CardGroup>
