# Alerts (/docs/alerts)

An alert watches one number. When the number breaks your rule, the alert **fires**: you get a message, and another one when it's back to normal. **Alerts** in the sidebar shows every alert with its state, latest value and recent history.

<CopyPrompt preset="alerts">
  Hand this to your agent. It looks at your game and proposes a few alerts worth having, then sets them up with you.
</CopyPrompt>

## Game metrics [#game-metrics]

**Game metric** is the first choice in the alert form. Game metrics are computed from what your game servers send through the SDK, so they're live: you don't wait 1–2 days for the Creator Dashboard. Each one is checked every minute and compared with **the same window yesterday at this time**.

| Metric | The number | Window |
| --- | --- | --- |
| **Playing now** | Players in a session right now (the same number as [Live](/docs/live)) | right now |
| **Servers running** | Servers that sent a heartbeat in the last 2 minutes | right now |
| **Errors** | Script errors (`$error`, counting grouped repeats) | today, or the last 5 min – 6 h |
| **Server crashes** | Servers whose heartbeats stopped for 3+ minutes without a shutdown | today, or the last N minutes |
| **Players**, **New players**, **Sessions** | Unique players, first-time players, sessions started | today, or the last N minutes |
| **Robux**, **Purchases**, **Payers** | From `$purchase` | today, or the last N minutes |
| **Playtime**, **Avg session** | Hours played; average minutes per session | today, or the last N minutes |
| **D1 retention** | Yesterday's new players who have played today so far | today |

"Today" runs from midnight in your project's timezone up to now, and yesterday is cut at the same time of day. So a quiet morning isn't compared with a whole day.

The form has presets for the alerts most games want. Every number in a preset stays editable:

- **Players playing now drops below N**: a floor under your concurrent players.
- **Playing now falls X% vs yesterday**: catches a broken update before the evening peak.
- **No servers running**: Servers running drops below 1. The game is down, or HTTP requests got switched off.
- **Errors in the last 15 minutes above N**: a bad publish or a broken script.
- **Robux today below yesterday**: Robux today falls 10% or more vs yesterday at this time.
- **New players in the last hour drops X%**: discovery or a sponsored ad stopped bringing players in.

Messages read like *Playing now: 744 (was 1,284, −42%) — falls 30% or more vs yesterday at this time*.

## Other things an alert can watch [#other-things-an-alert-can-watch]

| Watch | The number | Good for |
| --- | --- | --- |
| **Event** | A count, unique players, or sum or average of a property, for the last complete hour, day or week. Live: the last 5 to 60 minutes, up to now. Filter on event properties to count only some, e.g. `$server_type = public` | Your own events: `datastore_error`, `match_started` |
| **Insight** | A saved [trend](/docs/product-analytics/insights): its latest complete period. A saved funnel: its overall conversion % | Tutorial completion |
| **SQL query** | A number from the first row of a [saved query](/docs/product-analytics/sql) | Anything else |

For SQL, sort so the row you care about comes first, for example `ORDER BY day DESC`. Event, insight and SQL alerts can be checked live (every minute), hourly, daily or weekly. To catch something in your own code, send an event when it happens, such as `RoChart.trackServer("datastore_error", { key = "inventory" })`, and alert when it **goes above** a few per 10 minutes.

## When it fires [#when-it-fires]

- **drops below** or **goes above** a number;
- **changes by** a percentage, going up, down or either way. Game metrics compare with the same window yesterday at this time. Event and trend alerts compare with the period before, and funnel and SQL alerts with the previous check.

The form shows the current value as you build the alert, and whether it would fire right now. A new alert checks once straight away.

## Where you're told [#where-youre-told]

Every check is recorded on the alert's page, and fires and recoveries appear in the activity log. Add up to five destinations:

- **Discord**: paste a channel webhook URL (channel **Settings → Integrations → Webhooks → New Webhook → Copy Webhook URL**). It starts with `https://discord.com/api/webhooks/`. Alerts arrive as embeds: red when one fires, green when it's back to normal, with the value, yesterday's value, a link to the alert and a *RoChart* footer. If Discord rate-limits a message, RoChart waits as long as Discord asks and tries again.
- **Slack**: paste an [incoming webhook](https://api.slack.com/messaging/webhooks) URL.
- **Telegram**: add the RoChart bot to a channel or group, then enter its chat id. See [Destinations → Telegram](/docs/destinations#telegram).
- **Webhook**: RoChart sends a JSON `POST` you can point at an automation or an agent:

```json
{
  "id": "alrt_4_fired_1762070412000",
  "type": "alert.fired",
  "alert": { "id": 4, "name": "Players dropped", "url": "https://app.rochart.co/p/1/alerts/4" },
  "project": { "id": 1, "name": "Lava Tycoon" },
  "value": 744,
  "previous": 1284,
  "period": "right now",
  "condition": { "type": "change", "percent": 30, "direction": "down" },
  "message": "Playing now: 744 (was 1,284, −42%) — falls 30% or more vs yesterday at this time",
  "at": "2026-11-02T18:00:12.000Z"
}
```

`type` is `alert.fired`, `alert.resolved` or `alert.test`. A recovery carries the same `alert.id`, so whatever started on the fire can close itself. **Send test** posts a test message to every destination. Destinations must be public `https` addresses. A failed delivery (no answer, `429` or `5xx`) is tried twice more within about ten seconds, or after the wait Discord asks for.

### Checking a webhook came from RoChart [#checking-a-webhook-came-from-rochart]

Every webhook RoChart sends, from alerts and [destinations](/docs/destinations#webhook), is signed in the [Standard Webhooks](https://www.standardwebhooks.com) format with your project's signing secret (**Show signing secret** under a webhook, in the alert or destination form):

- `webhook-id`: the message id, the same on retries. Ignore ids you've already handled.
- `webhook-timestamp`: when it was sent, in unix seconds.
- `webhook-signature`: `v1,` and a base64 HMAC-SHA256 of `<id>.<timestamp>.<body>`.

The Standard Webhooks libraries check all three for you:

```ts
import { Webhook } from 'standardwebhooks'

const wh = new Webhook(process.env.ROCHART_WEBHOOK_SECRET) // whsec_…
const payload = wh.verify(rawBody, request.headers) // throws if it isn't from RoChart
```

## Pause, check, delete [#pause-check-delete]

Switch an alert off to pause it. **Check now** runs it immediately and notifies you if its state changes. Editing what it watches or when it fires starts it fresh.

## Agents [#agents]

Agents can do all of this over MCP: `create_alert`, `list_alerts`, `get_alert`, `update_alert`, `check_alert` and `delete_alert`. A game alert is `source: { kind: "game", metric: "playing_now" }` with `frequency: "live"`. Add `windowMinutes` for rolling metrics such as `errors`, and `filters` for platform, country or place version. A good pattern is to point a webhook at your agent, so a firing alert starts an investigation: *"Players dropped 40% — which platform or place version changed?"*
