> The index of these docs, for agents: https://traktor.fun/llms.txt

# Alerts

An account's alert rules, and the Telegram chats their matches are sent to.

Each rule runs on Traktor's servers as the data it reads arrives, so it keeps matching while your code is offline, and what it matches lands in the account's [notifications](https://traktor.fun/docs/notifications.md).

## Credentials

Alerts belong to the account, not to a key, so every credential the [account routes](https://traktor.fun/docs/authentication.md#account-routes) take reads and writes the same rules. The routes here that take a credential, the notification routes and the two on `/v1/browser-sessions/current` share a per-minute budget per account, apart from the plan's: the Requests limit on each block. Past it they answer `429 rate_limited` with `Retry-After`.

## Rules

The grammar is closed: `GET /v1/alert-capabilities` publishes it, and a rule naming anything it does not list is refused. A rule as the API stores it:

```json
{
  "v": 1,
  "source": "flow",
  "windowMinutes": 60,
  "scope": { "chains": ["bnb"], "platforms": [], "tokens": [], "wallets": [], "side": "all" },
  "match": { "op": "all", "conditions": [{ "metric": "usdAmount", "op": "gte", "value": 1000 }] },
  "trigger": { "mode": "crossing", "cooldownSeconds": 300, "hysteresisPct": 5 }
}
```

- `source` is what the rule tests: `flow` each trade as Traktor stores it, `token_window` a token's figures over `windowMinutes`, and `signal` each signal the feed detects. A signal rule takes no window, and never matches a signal for a token that no launchpad we know made, as no board lists one, a verified stock aside, nor for a major such as a stablecoin or a wrapped or bridged coin.
- `scope` narrows the source, and an empty list means every member. A `token_window` or `signal` rule cannot name wallets or a side.
- `match` holds the conditions, all of them or any (`op`). A condition compares a metric with `value`, or with `min` and `max` under `between`; a signal rule's condition names a `signal` and a `minTier` instead, and since each event is one signal, its `all` can't name two different ones. A figure Traktor did not measure satisfies no condition, a ceiling included.
- `trigger` says when a match notifies, subject by subject: a wallet in a token on a `flow` rule, a token on a `token_window` rule. `crossing` notifies when the subject starts to match, and again only after it has left the match by more than `hysteresisPct` percent of the bound; a reading whose measured figures can't settle the match doesn't count as leaving it. `repeat` notifies on every match. `once` notifies on the first and pauses the alert. None notifies a subject again within `cooldownSeconds` of its last. A signal notifies once, whatever the mode.
- A `flow` rule sees only the trades Traktor stores as they arrive: a row it recovers later, with `origin` `sweep` or `backfill`, never fires one.

A rule error is a `400` whose `details.pointer` names the part refused, as a JSON pointer such as `/match/conditions/0/metric`.

### `GET /v1/alert-capabilities`

What an alert rule can name: sources, metrics, signals, windows, trigger modes and limits, beside each tracked chain and app's freshness.

- URL: `https://api.traktor.fun/v1/alert-capabilities`
- Credential: None

```bash cURL
curl -s "https://api.traktor.fun/v1/alert-capabilities"
```

```ts TypeScript
const response = await fetch("https://api.traktor.fun/v1/alert-capabilities");
const body = await response.json();
```

```python Python
import requests

response = requests.get(
    "https://api.traktor.fun/v1/alert-capabilities",
)
print(response.json())
```

Response `200` (excerpt; the full body has more keys):

```json
{
  "data": {
    "sources": [
      "flow",
      "token_window",
      "signal"
    ],
    "triggerModes": [
      "crossing",
      "repeat",
      "once"
    ],
    "limits": {
      "alertsPerAccount": 100,
      "conditions": 12,
      "addressesPerScope": 50,
      "evaluationsPerAccountMinute": 10000,
      "trackedSubjectsPerAlert": 1000
    },
    "coverage": {
      "available": true,
      "asOf": "2026-10-03T12:00:04.000Z",
      "thresholds": {
        "staleAfterSeconds": 600,
        "downAfterSeconds": 3600
      },
      "pairs": [
        {
          "chain": "bnb",
          "platform": "fomo",
          "state": "live",
          "lastIngestedAt": "2026-10-03T12:00:01.870Z",
          "lastBlockTime": "2026-10-03T12:00:00.000Z",
          "lastCursorAt": "2026-10-03T12:00:03.000Z",
          "cursorBlock": 63012346,
          "sourceHead": 63012348,
          "sourceLagSeconds": 1.8,
          "lagSeconds": 2.1,
          "rowsLastHour": 9410
        }
      ]
    }
  },
  "meta": {
    "requestId": "req_01m40t5ek8b3x7r2mh5q",
    "generatedAt": "2026-10-03T12:00:04.210Z",
    "asOf": null,
    "caveats": []
  }
}
```

Errors: `method_not_allowed`, `internal_error`. Every code: https://traktor.fun/docs/errors.md

Beside the grammar, the answer carries `coverage`: under `pairs`, the freshness row of each tracked chain and app, its `state` judged by the `thresholds` beside it, or `available: false` and no rows when they could not be read. A row keeps the newest trade apart from the capture's health, so a quiet market is not taken for a stopped capture. `lastIngestedAt` is when the newest row was stored, and `lagSeconds` how long before `coverage.asOf` that was. The route answers with `Cache-Control: public, max-age=300`, so a copy can be up to five minutes old: read a row's age against `coverage.asOf`, not against when you asked. `lastCursorAt`, `cursorBlock`, `sourceHead` and `sourceLagSeconds` say when the capture last read and how far behind the chain it is. `rowsLastHour` counts the trades of the last hour by block time. A Coinbase Wallet row, on Base, marks its coverage `partial` and adds `sources`, holding its one source, `swap-tag`: the tagger that reads Coinbase's trades from the swap feed, whose `state` the row takes. Bankr's rows mark their coverage `partial` too, with no `sources`; each `coverageNote` says what Bankr's own record leaves out.

A chain that has stopped is a `state` inside a `200`.

## Writing rules

`GET /v1/alerts` lists the account's alerts newest first, unpaged. A create and a duplicate both count toward the Alerts limit on the create block, and past it answer `409 alert_limit_reached`.

A `PATCH` names at least one of `name`, `rule` or `enabled` beside its `revision`, and every change raises the revision by one. A stale revision is `409 alert_revision_conflict`, with the current one in `details.currentRevision`: read the alert again, then resend. A changed rule starts over, its subjects' trigger state cleared.

- A `once` alert that has notified is paused, with `oneShotCompletedAt` set. Enabling it again with its rule unchanged is `409 alert_once_completed`: change the rule, or duplicate the alert.
- A rule stored before alerts dropped a window, field or match it uses can still be read, but enabling it without sending a rule is `400 alert_rule_unsupported`: send the rule edited.
- A signal rule naming a signal the feed does not emit is `400 unknown_signal`, and one whose `all` names two different signals is `400 invalid_match`.
- `404 alert_not_found` reads the same for an alert on another account. `DELETE` leaves the notifications an alert wrote in place.

### `GET /v1/alerts`

Every alert on the account.

- URL: `https://api.traktor.fun/v1/alerts`
- Credential: API key, browser session or Traktor sign-in

Limits:

- Requests: 120 a minute, per account
- Session checks: 120 a minute, per IP
- Invalid keys: 30 a minute, per IP

```bash cURL
curl -s "https://api.traktor.fun/v1/alerts" \
  -H "Authorization: Bearer $TRAKTOR_API_KEY"
```

```ts TypeScript
const response = await fetch("https://api.traktor.fun/v1/alerts", {
  headers: { Authorization: `Bearer ${process.env.TRAKTOR_API_KEY}` },
});
const body = await response.json();
```

```python Python
import os
import requests

response = requests.get(
    "https://api.traktor.fun/v1/alerts",
    headers={"Authorization": f"Bearer {os.environ['TRAKTOR_API_KEY']}"},
)
print(response.json())
```

Response `200`:

```json
{
  "data": {
    "alerts": [
      {
        "id": "8f14e45f-ceea-467a-9575-2b0f3e8d1c2a",
        "name": "Large BNB buys",
        "enabled": true,
        "revision": 1,
        "triggerCount": 0,
        "lastTriggeredAt": null,
        "oneShotCompletedAt": null,
        "rule": {
          "v": 1,
          "source": "flow",
          "windowMinutes": 60,
          "scope": {
            "chains": [
              "bnb"
            ],
            "platforms": [],
            "tokens": [],
            "wallets": [],
            "side": "all"
          },
          "match": {
            "op": "all",
            "conditions": [
              {
                "metric": "usdAmount",
                "op": "gte",
                "value": 1000
              }
            ]
          },
          "trigger": {
            "mode": "crossing",
            "cooldownSeconds": 300,
            "hysteresisPct": 5
          }
        },
        "createdAt": "2026-10-03T12:00:00.000Z",
        "updatedAt": "2026-10-03T12:00:00.000Z"
      }
    ]
  },
  "meta": {
    "requestId": "req_01m40t5ek8b3x7r2mh5q",
    "generatedAt": "2026-10-03T12:00:04.210Z",
    "asOf": null,
    "caveats": []
  }
}
```

Errors: `missing_credentials`, `invalid_key`, `key_revoked`, `subscription_inactive`, `session_expired`, `sign_in_required`, `method_not_allowed`, `rate_limited`, `rate_limit_unavailable`, `internal_error`. Every code: https://traktor.fun/docs/errors.md

### `POST /v1/alerts`

Creates an alert from a name and a rule; it is enabled unless `enabled` says otherwise.

- URL: `https://api.traktor.fun/v1/alerts`
- Credential: API key, browser session or Traktor sign-in

| Parameter | In | Type | Required | Default | Bounds | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `name` | body | string | yes |  |  | What the alert is called, 1 to 80 characters. |
| `rule` | body | object | yes |  |  | The rule, as `GET /v1/alert-capabilities` describes it. |
| `enabled` | body | boolean | no | true |  | Whether it runs from the start. |

Limits:

- Requests: 120 a minute, per account
- Session checks: 120 a minute, per IP
- Invalid keys: 30 a minute, per IP
- Alerts: 100 kept, per account

```bash cURL
curl -s -X POST "https://api.traktor.fun/v1/alerts" \
  -H "Authorization: Bearer $TRAKTOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Large BNB buys","rule":{"v":1,"source":"flow","scope":{"chains":["bnb"]},"match":{"op":"all","conditions":[{"metric":"usdAmount","op":"gte","value":1000}]}}}'
```

```ts TypeScript
const response = await fetch("https://api.traktor.fun/v1/alerts", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.TRAKTOR_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({"name":"Large BNB buys","rule":{"v":1,"source":"flow","scope":{"chains":["bnb"]},"match":{"op":"all","conditions":[{"metric":"usdAmount","op":"gte","value":1000}]}}}),
});
const body = await response.json();
```

```python Python
import os
import requests

response = requests.post(
    "https://api.traktor.fun/v1/alerts",
    headers={"Authorization": f"Bearer {os.environ['TRAKTOR_API_KEY']}"},
    json={"name": "Large BNB buys", "rule": {"v": 1, "source": "flow", "scope": {"chains": ["bnb"]}, "match": {"op": "all", "conditions": [{"metric": "usdAmount", "op": "gte", "value": 1000}]}}},
)
print(response.json())
```

Response `201`:

```json
{
  "data": {
    "alert": {
      "id": "8f14e45f-ceea-467a-9575-2b0f3e8d1c2a",
      "name": "Large BNB buys",
      "enabled": true,
      "revision": 1,
      "triggerCount": 0,
      "lastTriggeredAt": null,
      "oneShotCompletedAt": null,
      "rule": {
        "v": 1,
        "source": "flow",
        "windowMinutes": 60,
        "scope": {
          "chains": [
            "bnb"
          ],
          "platforms": [],
          "tokens": [],
          "wallets": [],
          "side": "all"
        },
        "match": {
          "op": "all",
          "conditions": [
            {
              "metric": "usdAmount",
              "op": "gte",
              "value": 1000
            }
          ]
        },
        "trigger": {
          "mode": "crossing",
          "cooldownSeconds": 300,
          "hysteresisPct": 5
        }
      },
      "createdAt": "2026-10-03T12:00:00.000Z",
      "updatedAt": "2026-10-03T12:00:00.000Z"
    }
  },
  "meta": {
    "requestId": "req_01m40t5ek8b3x7r2mh5q",
    "generatedAt": "2026-10-03T12:00:04.210Z",
    "asOf": null,
    "caveats": []
  }
}
```

Errors: `invalid_alert_name`, `invalid_alert_enabled`, `alert_limit_reached`, `invalid_rule`, `invalid_scope`, `invalid_condition`, `unknown_metric`, `unknown_signal`, `metric_source_mismatch`, `invalid_trigger`, `invalid_source`, `invalid_window`, `invalid_match`, `alert_version_unsupported`, `too_many_conditions`, `too_many_addresses`, `invalid_json`, `invalid_body`, `unknown_field`, `body_too_large`, `missing_credentials`, `invalid_key`, `key_revoked`, `subscription_inactive`, `session_expired`, `sign_in_required`, `method_not_allowed`, `rate_limited`, `rate_limit_unavailable`, `internal_error`. Every code: https://traktor.fun/docs/errors.md

### `GET /v1/alerts/{id}`

One alert by its id.

- URL: `https://api.traktor.fun/v1/alerts/{id}`
- Credential: API key, browser session or Traktor sign-in

| Parameter | In | Type | Required | Default | Bounds | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `id` | path | uuid | yes |  |  | The alert's id. |

Limits:

- Requests: 120 a minute, per account
- Session checks: 120 a minute, per IP
- Invalid keys: 30 a minute, per IP

```bash cURL
curl -s "https://api.traktor.fun/v1/alerts/8f14e45f-ceea-467a-9575-2b0f3e8d1c2a" \
  -H "Authorization: Bearer $TRAKTOR_API_KEY"
```

```ts TypeScript
const response = await fetch("https://api.traktor.fun/v1/alerts/8f14e45f-ceea-467a-9575-2b0f3e8d1c2a", {
  headers: { Authorization: `Bearer ${process.env.TRAKTOR_API_KEY}` },
});
const body = await response.json();
```

```python Python
import os
import requests

response = requests.get(
    "https://api.traktor.fun/v1/alerts/8f14e45f-ceea-467a-9575-2b0f3e8d1c2a",
    headers={"Authorization": f"Bearer {os.environ['TRAKTOR_API_KEY']}"},
)
print(response.json())
```

Response `200`:

```json
{
  "data": {
    "alert": {
      "id": "8f14e45f-ceea-467a-9575-2b0f3e8d1c2a",
      "name": "Large BNB buys",
      "enabled": true,
      "revision": 1,
      "triggerCount": 0,
      "lastTriggeredAt": null,
      "oneShotCompletedAt": null,
      "rule": {
        "v": 1,
        "source": "flow",
        "windowMinutes": 60,
        "scope": {
          "chains": [
            "bnb"
          ],
          "platforms": [],
          "tokens": [],
          "wallets": [],
          "side": "all"
        },
        "match": {
          "op": "all",
          "conditions": [
            {
              "metric": "usdAmount",
              "op": "gte",
              "value": 1000
            }
          ]
        },
        "trigger": {
          "mode": "crossing",
          "cooldownSeconds": 300,
          "hysteresisPct": 5
        }
      },
      "createdAt": "2026-10-03T12:00:00.000Z",
      "updatedAt": "2026-10-03T12:00:00.000Z"
    }
  },
  "meta": {
    "requestId": "req_01m40t5ek8b3x7r2mh5q",
    "generatedAt": "2026-10-03T12:00:04.210Z",
    "asOf": null,
    "caveats": []
  }
}
```

Errors: `alert_not_found`, `route_not_found`, `missing_credentials`, `invalid_key`, `key_revoked`, `subscription_inactive`, `session_expired`, `sign_in_required`, `method_not_allowed`, `rate_limited`, `rate_limit_unavailable`, `internal_error`. Every code: https://traktor.fun/docs/errors.md

### `PATCH /v1/alerts/{id}`

Changes an alert's name, rule or whether it runs, against the revision you last read.

- URL: `https://api.traktor.fun/v1/alerts/{id}`
- Credential: API key, browser session or Traktor sign-in

| Parameter | In | Type | Required | Default | Bounds | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `id` | path | uuid | yes |  |  | The alert's id. |
| `revision` | body | integer | yes |  | from 1 | The revision you last read; a newer one answers 409. |
| `name` | body | string | no |  |  | A new name, 1 to 80 characters. |
| `rule` | body | object | no |  |  | A new rule. |
| `enabled` | body | boolean | no |  |  | Whether it runs. |

Limits:

- Requests: 120 a minute, per account
- Session checks: 120 a minute, per IP
- Invalid keys: 30 a minute, per IP

```bash cURL
curl -s -X PATCH "https://api.traktor.fun/v1/alerts/8f14e45f-ceea-467a-9575-2b0f3e8d1c2a" \
  -H "Authorization: Bearer $TRAKTOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"revision":1,"enabled":false}'
```

```ts TypeScript
const response = await fetch("https://api.traktor.fun/v1/alerts/8f14e45f-ceea-467a-9575-2b0f3e8d1c2a", {
  method: "PATCH",
  headers: { Authorization: `Bearer ${process.env.TRAKTOR_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({"revision":1,"enabled":false}),
});
const body = await response.json();
```

```python Python
import os
import requests

response = requests.patch(
    "https://api.traktor.fun/v1/alerts/8f14e45f-ceea-467a-9575-2b0f3e8d1c2a",
    headers={"Authorization": f"Bearer {os.environ['TRAKTOR_API_KEY']}"},
    json={"revision": 1, "enabled": False},
)
print(response.json())
```

Response `200`:

```json
{
  "data": {
    "alert": {
      "id": "8f14e45f-ceea-467a-9575-2b0f3e8d1c2a",
      "name": "Large BNB buys",
      "enabled": false,
      "revision": 2,
      "triggerCount": 0,
      "lastTriggeredAt": null,
      "oneShotCompletedAt": null,
      "rule": {
        "v": 1,
        "source": "flow",
        "windowMinutes": 60,
        "scope": {
          "chains": [
            "bnb"
          ],
          "platforms": [],
          "tokens": [],
          "wallets": [],
          "side": "all"
        },
        "match": {
          "op": "all",
          "conditions": [
            {
              "metric": "usdAmount",
              "op": "gte",
              "value": 1000
            }
          ]
        },
        "trigger": {
          "mode": "crossing",
          "cooldownSeconds": 300,
          "hysteresisPct": 5
        }
      },
      "createdAt": "2026-10-03T12:00:00.000Z",
      "updatedAt": "2026-10-03T12:00:00.000Z"
    }
  },
  "meta": {
    "requestId": "req_01m40t5ek8b3x7r2mh5q",
    "generatedAt": "2026-10-03T12:00:04.210Z",
    "asOf": null,
    "caveats": []
  }
}
```

Errors: `alert_not_found`, `route_not_found`, `invalid_alert_patch`, `invalid_alert_revision`, `invalid_alert_name`, `invalid_alert_enabled`, `alert_revision_conflict`, `alert_once_completed`, `alert_rule_unsupported`, `invalid_rule`, `invalid_scope`, `invalid_condition`, `unknown_metric`, `unknown_signal`, `metric_source_mismatch`, `invalid_trigger`, `invalid_source`, `invalid_window`, `invalid_match`, `alert_version_unsupported`, `too_many_conditions`, `too_many_addresses`, `invalid_json`, `invalid_body`, `unknown_field`, `body_too_large`, `missing_credentials`, `invalid_key`, `key_revoked`, `subscription_inactive`, `session_expired`, `sign_in_required`, `method_not_allowed`, `rate_limited`, `rate_limit_unavailable`, `internal_error`. Every code: https://traktor.fun/docs/errors.md

### `DELETE /v1/alerts/{id}`

Deletes an alert.

- URL: `https://api.traktor.fun/v1/alerts/{id}`
- Credential: API key, browser session or Traktor sign-in

| Parameter | In | Type | Required | Default | Bounds | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `id` | path | uuid | yes |  |  | The alert's id. |

Limits:

- Requests: 120 a minute, per account
- Session checks: 120 a minute, per IP
- Invalid keys: 30 a minute, per IP

```bash cURL
curl -s -X DELETE "https://api.traktor.fun/v1/alerts/8f14e45f-ceea-467a-9575-2b0f3e8d1c2a" \
  -H "Authorization: Bearer $TRAKTOR_API_KEY"
```

```ts TypeScript
const response = await fetch("https://api.traktor.fun/v1/alerts/8f14e45f-ceea-467a-9575-2b0f3e8d1c2a", {
  method: "DELETE",
  headers: { Authorization: `Bearer ${process.env.TRAKTOR_API_KEY}` },
});
console.log(response.status);
```

```python Python
import os
import requests

response = requests.delete(
    "https://api.traktor.fun/v1/alerts/8f14e45f-ceea-467a-9575-2b0f3e8d1c2a",
    headers={"Authorization": f"Bearer {os.environ['TRAKTOR_API_KEY']}"},
)
print(response.status_code)
```

Response `204`: no body.

Errors: `alert_not_found`, `route_not_found`, `missing_credentials`, `invalid_key`, `key_revoked`, `subscription_inactive`, `session_expired`, `sign_in_required`, `method_not_allowed`, `rate_limited`, `rate_limit_unavailable`, `internal_error`. Every code: https://traktor.fun/docs/errors.md

### `POST /v1/alerts/{id}/duplicate`

Copies an alert as a new one, created paused.

- URL: `https://api.traktor.fun/v1/alerts/{id}/duplicate`
- Credential: API key, browser session or Traktor sign-in

| Parameter | In | Type | Required | Default | Bounds | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `id` | path | uuid | yes |  |  | The alert's id. |

Limits:

- Requests: 120 a minute, per account
- Session checks: 120 a minute, per IP
- Invalid keys: 30 a minute, per IP

```bash cURL
curl -s -X POST "https://api.traktor.fun/v1/alerts/8f14e45f-ceea-467a-9575-2b0f3e8d1c2a/duplicate" \
  -H "Authorization: Bearer $TRAKTOR_API_KEY"
```

```ts TypeScript
const response = await fetch("https://api.traktor.fun/v1/alerts/8f14e45f-ceea-467a-9575-2b0f3e8d1c2a/duplicate", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.TRAKTOR_API_KEY}` },
});
const body = await response.json();
```

```python Python
import os
import requests

response = requests.post(
    "https://api.traktor.fun/v1/alerts/8f14e45f-ceea-467a-9575-2b0f3e8d1c2a/duplicate",
    headers={"Authorization": f"Bearer {os.environ['TRAKTOR_API_KEY']}"},
)
print(response.json())
```

Response `201`:

```json
{
  "data": {
    "alert": {
      "id": "2c9a7e10-5b3d-4f6e-a1c2-d3e4f5a6b7c8",
      "name": "Large BNB buys",
      "enabled": false,
      "revision": 1,
      "triggerCount": 0,
      "lastTriggeredAt": null,
      "oneShotCompletedAt": null,
      "rule": {
        "v": 1,
        "source": "flow",
        "windowMinutes": 60,
        "scope": {
          "chains": [
            "bnb"
          ],
          "platforms": [],
          "tokens": [],
          "wallets": [],
          "side": "all"
        },
        "match": {
          "op": "all",
          "conditions": [
            {
              "metric": "usdAmount",
              "op": "gte",
              "value": 1000
            }
          ]
        },
        "trigger": {
          "mode": "crossing",
          "cooldownSeconds": 300,
          "hysteresisPct": 5
        }
      },
      "createdAt": "2026-10-03T12:00:00.000Z",
      "updatedAt": "2026-10-03T12:00:00.000Z"
    }
  },
  "meta": {
    "requestId": "req_01m40t5ek8b3x7r2mh5q",
    "generatedAt": "2026-10-03T12:00:04.210Z",
    "asOf": null,
    "caveats": []
  }
}
```

Errors: `alert_not_found`, `alert_limit_reached`, `route_not_found`, `missing_credentials`, `invalid_key`, `key_revoked`, `subscription_inactive`, `session_expired`, `sign_in_required`, `method_not_allowed`, `rate_limited`, `rate_limit_unavailable`, `internal_error`. Every code: https://traktor.fun/docs/errors.md

## Testing a rule

A test runs the alert's stored rule over a bounded sample and leaves the rule's trigger state as it was, so save a change before you test it. A `null` `matches` comes with `truncated: true`: the sample was cut short, so no match in it is not a miss. `subjects` lists matching subjects up to a fixed number, so its length is not a count, and `dataAvailable` is `false` when the sample held nothing to test. Past the Alert tests limit on the block, a test answers `429 preview_rate_limited`.

### `POST /v1/alerts/{id}/test`

Runs an alert's rule over recent data without notifying; `matches` is true when a subject matched, null when none did and the read was cut short (`truncated`), and false otherwise.

- URL: `https://api.traktor.fun/v1/alerts/{id}/test`
- Credential: API key, browser session or Traktor sign-in

| Parameter | In | Type | Required | Default | Bounds | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `id` | path | uuid | yes |  |  | The alert's id. |

Limits:

- Requests: 120 a minute, per account
- Session checks: 120 a minute, per IP
- Invalid keys: 30 a minute, per IP
- Alert tests: 6 a minute, a burst of 3, per account

```bash cURL
curl -s -X POST "https://api.traktor.fun/v1/alerts/8f14e45f-ceea-467a-9575-2b0f3e8d1c2a/test" \
  -H "Authorization: Bearer $TRAKTOR_API_KEY"
```

```ts TypeScript
const response = await fetch("https://api.traktor.fun/v1/alerts/8f14e45f-ceea-467a-9575-2b0f3e8d1c2a/test", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.TRAKTOR_API_KEY}` },
});
const body = await response.json();
```

```python Python
import os
import requests

response = requests.post(
    "https://api.traktor.fun/v1/alerts/8f14e45f-ceea-467a-9575-2b0f3e8d1c2a/test",
    headers={"Authorization": f"Bearer {os.environ['TRAKTOR_API_KEY']}"},
)
print(response.json())
```

Response `200`:

```json
{
  "data": {
    "matches": true,
    "dataAvailable": true,
    "truncated": false,
    "evaluatedAt": "2026-10-03T12:00:03.000Z",
    "subjects": [
      {
        "subjectKey": "bnb:0x2222222222222222222222222222222222222222:0x1111111111111111111111111111111111111111",
        "observation": {
          "source": "flow",
          "chain": "bnb",
          "platform": "fomo",
          "tokenAddress": "0x2222222222222222222222222222222222222222",
          "wallet": "0x1111111111111111111111111111111111111111",
          "side": "buy",
          "metrics": {
            "usdAmount": 1204.5,
            "tokenAmount": 365000,
            "priceUsd": 0.0033,
            "lagSeconds": 2.1
          }
        }
      }
    ]
  },
  "meta": {
    "requestId": "req_01m40t5ek8b3x7r2mh5q",
    "generatedAt": "2026-10-03T12:00:04.210Z",
    "asOf": null,
    "caveats": []
  }
}
```

Errors: `alert_not_found`, `route_not_found`, `preview_rate_limited`, `missing_credentials`, `invalid_key`, `key_revoked`, `subscription_inactive`, `session_expired`, `sign_in_required`, `method_not_allowed`, `rate_limited`, `rate_limit_unavailable`, `internal_error`. Every code: https://traktor.fun/docs/errors.md

A rule not saved yet is tested the same way from its body, under the same Alert tests limit, and the tokens it matched come back named, each once in match order with `ticker` `null` where none is stored or the names could not be read. `capped` is `true` when the matches reached the fixed number, so more may match than came back, as they may whenever `truncated` is `true`. A rule the service would refuse answers as creating it would, before the test is counted.

### `POST /v1/alerts/preview`

Runs a rule that is not saved over recent data without notifying, as testing a saved alert does, and names the tokens it matched: each once, in match order, with `ticker` null where none is stored or the names could not be read. `capped` is true when the matches reached the preview's limit, so more may match, as they may whenever `truncated` is true.

- URL: `https://api.traktor.fun/v1/alerts/preview`
- Credential: API key, browser session or Traktor sign-in

| Parameter | In | Type | Required | Default | Bounds | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `rule` | body | object | yes |  |  | The rule, as `GET /v1/alert-capabilities` describes it. |

Limits:

- Requests: 120 a minute, per account
- Session checks: 120 a minute, per IP
- Invalid keys: 30 a minute, per IP
- Alert tests: 6 a minute, a burst of 3, shared with testing a saved alert, per account

```bash cURL
curl -s -X POST "https://api.traktor.fun/v1/alerts/preview" \
  -H "Authorization: Bearer $TRAKTOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"rule":{"v":1,"source":"flow","windowMinutes":60,"scope":{"chains":["bnb"],"platforms":[],"tokens":[],"wallets":[],"side":"all"},"match":{"op":"all","conditions":[{"metric":"usdAmount","op":"gte","value":1000}]},"trigger":{"mode":"crossing","cooldownSeconds":300,"hysteresisPct":5}}}'
```

```ts TypeScript
const response = await fetch("https://api.traktor.fun/v1/alerts/preview", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.TRAKTOR_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({"rule":{"v":1,"source":"flow","windowMinutes":60,"scope":{"chains":["bnb"],"platforms":[],"tokens":[],"wallets":[],"side":"all"},"match":{"op":"all","conditions":[{"metric":"usdAmount","op":"gte","value":1000}]},"trigger":{"mode":"crossing","cooldownSeconds":300,"hysteresisPct":5}}}),
});
const body = await response.json();
```

```python Python
import os
import requests

response = requests.post(
    "https://api.traktor.fun/v1/alerts/preview",
    headers={"Authorization": f"Bearer {os.environ['TRAKTOR_API_KEY']}"},
    json={"rule": {"v": 1, "source": "flow", "windowMinutes": 60, "scope": {"chains": ["bnb"], "platforms": [], "tokens": [], "wallets": [], "side": "all"}, "match": {"op": "all", "conditions": [{"metric": "usdAmount", "op": "gte", "value": 1000}]}, "trigger": {"mode": "crossing", "cooldownSeconds": 300, "hysteresisPct": 5}}},
)
print(response.json())
```

Response `200`:

```json
{
  "data": {
    "matches": true,
    "dataAvailable": true,
    "truncated": false,
    "evaluatedAt": "2026-10-03T12:00:03.000Z",
    "subjects": [
      {
        "subjectKey": "bnb:0x2222222222222222222222222222222222222222:0x1111111111111111111111111111111111111111",
        "observation": {
          "source": "flow",
          "chain": "bnb",
          "platform": "fomo",
          "tokenAddress": "0x2222222222222222222222222222222222222222",
          "wallet": "0x1111111111111111111111111111111111111111",
          "side": "buy",
          "metrics": {
            "usdAmount": 1204.5,
            "tokenAmount": 365000,
            "priceUsd": 0.0033,
            "lagSeconds": 2.1
          }
        }
      }
    ],
    "tokens": [
      {
        "chain": "bnb",
        "address": "0x2222222222222222222222222222222222222222",
        "ticker": "CAKE"
      }
    ],
    "capped": false
  },
  "meta": {
    "requestId": "req_01m40t5ek8b3x7r2mh5q",
    "generatedAt": "2026-10-03T12:00:04.210Z",
    "asOf": null,
    "caveats": []
  }
}
```

Errors: `preview_rate_limited`, `invalid_rule`, `invalid_scope`, `invalid_condition`, `unknown_metric`, `unknown_signal`, `metric_source_mismatch`, `invalid_trigger`, `invalid_source`, `invalid_window`, `invalid_match`, `alert_version_unsupported`, `too_many_conditions`, `too_many_addresses`, `invalid_json`, `invalid_body`, `unknown_field`, `body_too_large`, `missing_credentials`, `invalid_key`, `key_revoked`, `subscription_inactive`, `session_expired`, `sign_in_required`, `method_not_allowed`, `rate_limited`, `rate_limit_unavailable`, `internal_error`. Every code: https://traktor.fun/docs/errors.md

## Telegram

A link code's `url` has the form `https://t.me/<bot>?startgroup=link_<code>`. A group's admin whose Telegram is connected to the same account opens it and picks the group to add the bot to, or sends `/start link_<code>` or `/link <code>` in a group the bot is already in, and the bot sends the account's alerts and trade outcomes there from then on. In a group the bot reads commands from admins alone; it refuses a code in a private chat and spends a code once, and a new code replaces the account's last.

- Only Traktor's own sign-in mints a code, from **Create a link code** in Delivery on traktor.fun. An API key, a browser session or a Telegram Mini App session answers `403 web_sign_in_required`: a linked group keeps the account's alerts and trades after the key is revoked or the session ends.
- While the service has no bot, a code is `503 telegram_not_configured`. The other two routes never answer it, and the list gives `bot` as `null`.
- The list's `identity` is `null` until a Telegram account is connected to Traktor, and its `started` stays `false` until that person presses Start in the bot, which can message them only then.
- Unlinking the connected Telegram account's own chat disconnects that account too. `404 telegram_link_not_found` reads the same for another account's chat.
- A link's `kind` is Telegram's chat type: `private`, `group`, `supergroup` or `channel`. A channel takes no code: adding the bot to it as an admin that may post, from the Telegram connected to the account, links it, and it carries the account's alerts alone.

Connecting a person's own Telegram takes Traktor's own sign-in on the web, and is not part of this API.

### `POST /v1/telegram/link-codes`

A one-time code and its link, which a group's admin whose Telegram is connected to the account opens to send that group the account's alerts.

- URL: `https://api.traktor.fun/v1/telegram/link-codes`
- Credential: Traktor sign-in only

Limits:

- Requests: 120 a minute, per account
- Session checks: 120 a minute, per IP
- Invalid keys: 30 a minute, per IP
- Code lifetime: 10 minutes, per request
- Live codes: 1 at once, per account

```bash cURL
curl -s -X POST "https://api.traktor.fun/v1/telegram/link-codes"
```

```ts TypeScript
const response = await fetch("https://api.traktor.fun/v1/telegram/link-codes", {
  method: "POST",
});
const body = await response.json();
```

```python Python
import requests

response = requests.post(
    "https://api.traktor.fun/v1/telegram/link-codes",
)
print(response.json())
```

Response `201`:

```json
{
  "data": {
    "code": "q8Zr2Lw0yT5nB7vC1xK4mH9dF3sJ6gP0aE2uR8iO5lW",
    "url": "https://t.me/traktor_bot?startgroup=link_q8Zr2Lw0yT5nB7vC1xK4mH9dF3sJ6gP0aE2uR8iO5lW",
    "bot": "traktor_bot",
    "expiresAt": "2026-10-03T12:10:00.000Z"
  },
  "meta": {
    "requestId": "req_01m40t5ek8b3x7r2mh5q",
    "generatedAt": "2026-10-03T12:00:04.210Z",
    "asOf": null,
    "caveats": []
  }
}
```

Errors: `web_sign_in_required`, `telegram_not_configured`, `invalid_json`, `invalid_body`, `unknown_field`, `body_too_large`, `missing_credentials`, `invalid_key`, `key_revoked`, `subscription_inactive`, `session_expired`, `sign_in_required`, `method_not_allowed`, `rate_limited`, `rate_limit_unavailable`, `internal_error`. Every code: https://traktor.fun/docs/errors.md

### `GET /v1/telegram/links`

The chats linked to the account, the Telegram account connected to it, and the bot's name.

- URL: `https://api.traktor.fun/v1/telegram/links`
- Credential: API key, browser session or Traktor sign-in

Limits:

- Requests: 120 a minute, per account
- Session checks: 120 a minute, per IP
- Invalid keys: 30 a minute, per IP

```bash cURL
curl -s "https://api.traktor.fun/v1/telegram/links" \
  -H "Authorization: Bearer $TRAKTOR_API_KEY"
```

```ts TypeScript
const response = await fetch("https://api.traktor.fun/v1/telegram/links", {
  headers: { Authorization: `Bearer ${process.env.TRAKTOR_API_KEY}` },
});
const body = await response.json();
```

```python Python
import os
import requests

response = requests.get(
    "https://api.traktor.fun/v1/telegram/links",
    headers={"Authorization": f"Bearer {os.environ['TRAKTOR_API_KEY']}"},
)
print(response.json())
```

Response `200`:

```json
{
  "data": {
    "links": [
      {
        "chatId": "-1001234567890",
        "kind": "supergroup",
        "title": "Desk",
        "linkedAt": "2026-10-01T09:00:00.000Z"
      }
    ],
    "identity": {
      "telegramUserId": "123456789",
      "username": "example",
      "displayName": "Example",
      "linkedAt": "2026-09-30T09:00:00.000Z",
      "started": true
    },
    "bot": "traktor_bot"
  },
  "meta": {
    "requestId": "req_01m40t5ek8b3x7r2mh5q",
    "generatedAt": "2026-10-03T12:00:04.210Z",
    "asOf": null,
    "caveats": []
  }
}
```

Errors: `missing_credentials`, `invalid_key`, `key_revoked`, `subscription_inactive`, `session_expired`, `sign_in_required`, `method_not_allowed`, `rate_limited`, `rate_limit_unavailable`, `internal_error`. Every code: https://traktor.fun/docs/errors.md

### `DELETE /v1/telegram/links/{chatId}`

Stops sending the account's alerts to a chat.

- URL: `https://api.traktor.fun/v1/telegram/links/{chatId}`
- Credential: API key, browser session or Traktor sign-in

| Parameter | In | Type | Required | Default | Bounds | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `chatId` | path | string | yes |  |  | The chat's Telegram id. |

Limits:

- Requests: 120 a minute, per account
- Session checks: 120 a minute, per IP
- Invalid keys: 30 a minute, per IP

```bash cURL
curl -s -X DELETE "https://api.traktor.fun/v1/telegram/links/-1001234567890" \
  -H "Authorization: Bearer $TRAKTOR_API_KEY"
```

```ts TypeScript
const response = await fetch("https://api.traktor.fun/v1/telegram/links/-1001234567890", {
  method: "DELETE",
  headers: { Authorization: `Bearer ${process.env.TRAKTOR_API_KEY}` },
});
console.log(response.status);
```

```python Python
import os
import requests

response = requests.delete(
    "https://api.traktor.fun/v1/telegram/links/-1001234567890",
    headers={"Authorization": f"Bearer {os.environ['TRAKTOR_API_KEY']}"},
)
print(response.status_code)
```

Response `204`: no body.

Errors: `telegram_link_not_found`, `route_not_found`, `missing_credentials`, `invalid_key`, `key_revoked`, `subscription_inactive`, `session_expired`, `sign_in_required`, `method_not_allowed`, `rate_limited`, `rate_limit_unavailable`, `internal_error`. Every code: https://traktor.fun/docs/errors.md
