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

# Alert rules

> Configure a rule: scope it by venue, symbol and wallet, add conditions and relative thresholds, set its parameters, and edit it in place.

A rule points one endpoint at one event type and carries your own configuration. The platform declares what each event can be scoped and conditioned on; you choose the values. The model is the one you know from spreadsheet conditional formatting: a range, then rules.

<Warning>
  **A rule with no `filters` matches every event of its type.** Every venue and symbol the event covers, every watched address for an `account.*` event, and no conditions, with each parameter at its declared default. On a market-wide event that can be a lot of deliveries, so narrow it here and check the rate with the [estimate](/webhooks/estimate-and-dry-run#estimate) before you create it.
</Warning>

## A first rule

Every Hyperliquid BTC liquidation of \$250,000 or more:

```bash theme={"theme":"github-dark"}
curl -X POST https://api.0xarchive.io/v1/webhooks/subscriptions \
  -H "X-API-Key: $OXARCHIVE_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "endpoint_id": "f4797c46-ada6-4c09-a076-5689a974e4be",
    "event_type": "market.liquidation",
    "filters": {
      "venue": "hyperliquid",
      "symbols": ["BTC"],
      "conditions": [{ "metric": "notional_usd", "op": ">=", "value": 250000 }]
    }
  }'
```

The configuration goes in `filters` (`config` is accepted as the same key) and has three parts:

* **Scope keys** (`venue`, `symbols`, and `addresses` on account events) choose where the event is watched.
* **`conditions`** compare the event's declared metrics with your values, here `notional_usd` at least 250,000.
* **`params`** define what an occurrence is, such as a burst's window. This rule sets none, so `max_age_s` takes its default.

Every key is checked against the event's declaration in the [catalog](/webhooks/events#the-live-catalog). A filter the event does not accept, an undeclared metric or param, an operator that does not fit the metric's type, an out-of-range value or an unknown enum member is refused with a 400 that names what is allowed. Scope keys, `conditions` or `params` sent at the top level of the body, outside `filters`, are refused too, rather than creating a rule without them.

## Scope

* `venue` (string or list of strings): restrict to venues the event covers. Absent means every venue it covers.
* `symbols` (list of strings): restrict to these markets, in venue-native form. Absent means every market.
* `addresses` (list of strings, `account.*` events only): a subset of your [watched addresses](/webhooks/watched-addresses). Absent means all of them. Each one must already be on your watched list.

### Symbol conventions

Symbols are venue-native. A symbol written in the wrong form matches nothing.

* Hyperliquid perps: the bare coin, `BTC`.
* HIP-3: dex prefix and coin, `xyz:AAPL`. A bare `AAPL` matches nothing.
* Hyperliquid Spot: the dashed pair, `HYPE-USDC`.
* HIP-4 outcome sides: `#` and the asset id, `#20481`.
* Lighter (`lighter`): the bare market symbol, `BTC`.
* Lighter on Robinhood Chain (`rh-lighter`): the bare perp symbol, `BTC`, or the dashed spot pair with the USDG quote, `AAPL-USDG`.
* `account.transfer` and `market.pga_payment`: the token, not a pair, such as `USDC` or `HYPE`.

## Conditions

Conditions compare a metric the event declares with a value. A rule takes up to 16 conditions, and all of them must hold. No conditions means every occurrence: `account.fill` with no conditions is every fill of a watched address, and `market.liquidation` with no conditions is every liquidation on the covered venues from the 100 USD scan floor up.

* Numbers: `greater_than`, `greater_than_or_equal`, `less_than`, `less_than_or_equal`, `equal`, `not_equal`, `between`, `not_between`, `in`, `not_in`.
* Text and enumerated values: `equal`, `not_equal`, `in`, `not_in`, `contains`, `not_contains`, `starts_with`, `ends_with`.
* True or false values: `equal`, `not_equal`.
* Timestamps: `before`, `after`, `equal`, with RFC 3339 values.
* Any type: `is_empty`, `is_not_empty`.

Symbol spellings (`>=`, `<`, `!=` and the like) are accepted and stored in word form. `between` and `not_between` take `[low, high]`; `in` and `not_in` take a non-empty list; `is_empty` and `is_not_empty` take no value. A custom-formula operator (`formula`) is reserved and refused for now; combine conditions instead.

`min_notional_usd` at the top level of `filters` is shorthand for a `notional_usd greater_than_or_equal` condition, or `usdc_value` on `account.transfer`, the one event whose notional metric has that name. It is refused on events that declare no notional metric.

Which metrics an event declares, with their types, units and enum values, is in the catalog: `GET /v1/webhooks/event-types` returns `metrics`, `operators` and `params` for each event.

### Relative values

Four metrics let a threshold mean the same thing on a large market and a small one. They are conditionable like any other metric.

* `notional_pct_volume_1h` and `notional_pct_volume_24h`: the event's notional as a percent of that market's trailing volume. On `market.liquidation`, `account.liquidated`, `account.fill` and `market.liquidation_burst`.
* `notional_pct_oi`: as a percent of that market's open interest. On the same events, perps only.
* `notional_pct_wallet_volume_30d`: as a percent of that wallet's own 30-day volume. On `account.fill`.

```json theme={"theme":"github-dark"}
{"conditions": [{"metric": "notional_pct_volume_1h", "op": ">=", "value": 2}]}
```

That is "a liquidation worth at least 2 percent of this market's last hour of volume", on every market at once. Market references are refreshed every minute and wallet volumes every 10 minutes. A relative value is `null`, so a condition on it does not fire, when its denominator is under 10,000 USD, which keeps a thin market from producing a huge percentage. `notional_pct_oi` is `null` on spot markets, and the volume percentages are `null` on Lighter on Robinhood Chain. A payload with a relative value carries `reference_at`, the time the denominator was measured.

## Params

Some events are defined by a window or a level, so the value changes what an occurrence is rather than filtering one. This is the full list; defaults are in parentheses.

* `market.liquidation_burst`: `window_s` one of 60, 300, 900, 3600 (300); `threshold_mode` one of `usd`, `pct_oi`, `pct_volume_1h` (`usd`); `threshold_usd` from 1,000, used in `usd` mode (1,000,000); `threshold_pct` 0.01 to 100, used in the two percent modes (1).
* `market.oi_delta`: `window_s` one of 300, 900, 3600 (900); `threshold_pct` 0.1 to 1000 (10).
* `market.breadth_cross`: `threshold` 1 to 99 percent (30); `hysteresis_pct` 0 to 50 points (5).
* `oracle.jump`: `threshold_pct` 0.1 to 50 (2).
* `oracle.stall`: `window_s` one of 60, 120, 300, 900, 3600 (300).
* `ingest.stall` and `ingest.recovered`: `threshold_s` one of 60, 120, 300, 600, 1800, 3600 (600). A market is judged against it only when the threshold is at least five times its usual update interval. Lighter books update about once a minute, so on the Lighter venues most markets are judged from 600 seconds and the sparsest only from 1800.
* `chain.block_stall`: `threshold_s` one of 10, 20, 30, 60, 120, 300, 600 (20).
* `chain.block_time_degraded`: `degraded_pct` one of 10, 20, 30, 50 (20); `consecutive_minutes` one of 1, 2, 3, 5 (2).
* `billing.credit_low`: `levels_pct`, a list of percent-remaining levels, each 0 to 100 (`[25, 10, 0]`).
* `market.liquidation`, `market.pga_payment`, `hip4.settlement` and every `account.*` event: `max_age_s` 60 to 86400 (3600, or 7200 for `hip4.settlement` and `account.hip4_settled`). See below.

`market.liquidation_burst` can be written in relative terms: `{"params": {"window_s": 60, "threshold_mode": "pct_oi", "threshold_pct": 1}}` is "liquidations inside a minute worth one percent of that market's open interest", a meaningful threshold on every perp without picking a dollar figure per market. A market without a reference does not fire the percent modes. The default mode is `usd`, so a rule written before modes existed keeps firing exactly as it did.

Omitted params take the declared default. A value outside the range or off the menu is refused with a 400 naming the allowed values. A declared param may also be written at the top level of `filters` (`"threshold_s": 60`) and is routed into `params`; on `market.liquidation_burst`, a top-level `threshold` means `threshold_usd`. Two rules with different params are two different triggers, and their deliveries carry different event ids.

### max\_age\_s and late deliveries

`max_age_s` is the oldest occurrence you still want delivered, in seconds behind real time. In normal operation it never comes into play. After an outage on our side, the engine catches up as far back as your `max_age_s` and delivers those occurrences with `late_ms` set, and with `late: true` when they are more than 10 minutes behind, instead of dropping them. Anything older is skipped; fetch it from the REST API or an export. Set it low when a stale alert is worse than no alert, such as a trading trigger, and high when completeness matters more, such as an audit trail.

## What the API stores

A fuller rule, on fills by a watched address:

```json theme={"theme":"github-dark"}
{
  "endpoint_id": "f4797c46-ada6-4c09-a076-5689a974e4be",
  "event_type": "account.fill",
  "filters": {
    "venue": "hyperliquid",
    "symbols": ["BTC", "ETH"],
    "addresses": ["0x2b1e0bcefada121c5bc484a546e3ca0e2a8bee5b"],
    "params": {"max_age_s": 900},
    "conditions": [
      { "metric": "notional_usd", "op": "greater_than_or_equal", "value": 25000 },
      { "metric": "side", "op": "in", "value": ["buy"] }
    ]
  }
}
```

The response echoes the stored configuration, normalized: operators in their word form, addresses lowercased, every declared param present with defaults filled in, and `min_notional_usd` mirrored from the loosest `notional_usd` lower bound among your conditions.

```json theme={"theme":"github-dark"}
{
  "success": true,
  "data": {
    "id": "3c1f0a52-8d6b-4f0e-9b1a-6f2c4d8e9a10",
    "endpoint_id": "f4797c46-ada6-4c09-a076-5689a974e4be",
    "event_type": "account.fill",
    "filters": {
      "venue": "hyperliquid",
      "symbols": ["BTC", "ETH"],
      "addresses": ["0x2b1e0bcefada121c5bc484a546e3ca0e2a8bee5b"],
      "params": {"max_age_s": 900},
      "conditions": [
        { "metric": "notional_usd", "op": "greater_than_or_equal", "value": 25000 },
        { "metric": "side", "op": "in", "value": ["buy"] }
      ],
      "min_notional_usd": 25000
    },
    "enabled": true,
    "status": "active",
    "created_at": "2026-09-17T10:12:09Z"
  }
}
```

## Edit or switch off a rule

`PATCH /v1/webhooks/subscriptions/{id}` with `{"config": {...}}` replaces the whole configuration in place, validated exactly like create (`filters` is accepted as the key too). `{"enabled": false}` switches delivery off without losing the configuration, and `{"enabled": true}` switches it back on, so tuning a threshold never means deleting and recreating the rule. The response is the updated rule in the same shape as create.

```bash theme={"theme":"github-dark"}
curl -X PATCH https://api.0xarchive.io/v1/webhooks/subscriptions/3c1f0a52-8d6b-4f0e-9b1a-6f2c4d8e9a10 \
  -H "X-API-Key: $OXARCHIVE_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "config": {
      "venue": "hyperliquid",
      "symbols": ["BTC", "ETH"],
      "addresses": ["0x2b1e0bcefada121c5bc484a546e3ca0e2a8bee5b"],
      "params": {"max_age_s": 900},
      "conditions": [
        { "metric": "notional_usd", "op": ">=", "value": 50000 },
        { "metric": "side", "op": "in", "value": ["buy"] }
      ]
    }
  }'
```

This raises the fill rule above from 25,000 to 50,000 and keeps everything else. Because the new configuration replaces the old one, send every key you want to keep. A key you leave out returns to its default: without `addresses` the rule covers every watched address, without the `side` condition it covers both sides, and without `params` its `max_age_s` returns to 3600.

`enabled` is your switch, and it is separate from the engine's own pause when a rule reaches the plan's daily allowance. Resuming a paused rule never flips your switch, and switching a rule on never resumes it. See [Paused rules](/webhooks/delivery-and-retries#paused-rules). `DELETE /v1/webhooks/subscriptions/{id}` removes a rule for good.

## Next step

<Card title="See how often the rule would fire" icon="chart-column" href="/webhooks/estimate-and-dry-run" horizontal>
  Replay the configuration over recent history before you create it, and pick a threshold from the rate you want.
</Card>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.