# BenchLM Radar developer documentation

This is the agent-readable integration reference for BenchLM Radar.

- Human-readable documentation: https://radar.benchlm.ai/developers
- OpenAPI 3.1: https://radar.benchlm.ai/v1/schema.json
- Agent discovery: https://radar.benchlm.ai/.well-known/benchlm-radar.json
- Remote MCP: https://radar.benchlm.ai/mcp
- Production base URL: `https://radar.benchlm.ai`

## Choose an integration

| Use | Best for | Delivery | Scope |
| --- | --- | --- | --- |
| Event API | Agents, sync jobs, databases, search, and backfills | Your application pulls events | Complete Pro event stream, including Early Signals when enabled |
| Remote MCP | Codex, Claude, Cursor, OpenClaw, Hermes, and MCP clients | The client calls read-only Radar tools | Changes, event lookup, deprecations, and exact model lifecycle checks |
| Signed webhook | Automation, incident handling, and notifications | Radar pushes events | Alerts selected by the account's Delivery rules |

Use the API when the application needs a complete, replayable feed. Use a
webhook when it should react immediately to selected alerts. An integration can
use both: the webhook can trigger work and the API can reconcile the full feed.

## Remote MCP

Connect to `https://radar.benchlm.ai/mcp` with Streamable HTTP and the same
`Authorization: Bearer $RADAR_API_KEY` credential used by the event API. Every
authenticated MCP HTTP request counts against the Pro API allowance.

| Tool | Purpose |
| --- | --- |
| `list_changes` | List or incrementally poll source-linked events |
| `get_change` | Retrieve one event by stable ID |
| `get_deprecations` | List confirmed model deprecation and removal facts |
| `check_model` | Resolve an exact model ID or curated alias to its latest confirmed lifecycle fact |

`check_model` fails safely: `unknown` means Radar has no matching confirmed
fact, while `ambiguous` means the caller should retry with `provider`.
Replacement IDs, effective dates, and countdowns are returned only when the
structured source evidence supports them.

```bash
codex mcp add benchlm-radar \
  --url https://radar.benchlm.ai/mcp \
  --bearer-token-env-var RADAR_API_KEY
```

Claude Code:

```bash
claude mcp add --transport http --scope user benchlm-radar \
  https://radar.benchlm.ai/mcp \
  --header "Authorization: Bearer <key-from-secret-store>"
```

Cursor (`.cursor/mcp.json` or `~/.cursor/mcp.json`):

```json
{
  "mcpServers": {
    "benchlm-radar": {
      "url": "https://radar.benchlm.ai/mcp",
      "headers": {
        "Authorization": "Bearer ${env:RADAR_API_KEY}"
      }
    }
  }
}
```

Ready-to-download client files:

- Cursor config: https://radar.benchlm.ai/integrations/cursor/mcp.json
- OpenClaw package: https://radar.benchlm.ai/integrations/openclaw/benchlm-radar.zip
- Hermes package: https://radar.benchlm.ai/integrations/hermes/benchlm-radar.zip

Extract the OpenClaw or Hermes archive into the skills directory configured by
that client. Each archive contains a `benchlm-radar` directory with its skill
instructions and MCP dependency metadata. Create one revocable Pro key per
client and inject it from that client's secret store. Never place a complete
Radar key in a prompt or committed config.

## Event API

The event API is read-only and available to Pro accounts. Create a separate,
revocable key for each application or environment from **Developer access** in
Radar. The complete key is shown once and must be stored outside source control,
prompts, chat transcripts, and logs.

Only `GET /v1/events` accepts a Radar API key. Dashboard and destination
management endpoints use the signed-in browser session instead.

### Authentication and first request

Store the key as `RADAR_API_KEY` and send it as a Bearer token:

```bash
curl "https://radar.benchlm.ai/v1/events?limit=50" \
  -H "Authorization: Bearer $RADAR_API_KEY"
```

### Query parameters

| Parameter | Type | Default | Use |
| --- | --- | --- | --- |
| `limit` | integer, 1–200 | `50` | Maximum events in the page |
| `cursor` | string | none | Opaque `nextCursor` returned by the previous response |
| `provider` | string | none | Exact provider slug, such as `openai` or `anthropic` |
| `type` | string | none | Exact event type; `outage` includes the full outage lifecycle |
| `after` | ISO 8601 timestamp | none | Deprecated compatibility filter; use `cursor` for new integrations |

Without `cursor` or `after`, Radar returns the newest events first. A cursor
request returns events oldest-first so the integration can process them in
order. Do not parse, modify, or manufacture a cursor.

### Response

```json
{
  "data": [
    {
      "id": "evt_01JY...",
      "providerSlug": "openai",
      "eventType": "api_model_listed",
      "subjectType": "model",
      "subjectKey": "gpt-5",
      "title": "New OpenAI API model",
      "summary": "A model appeared in the first-party API catalog.",
      "sourceUrl": "https://developers.openai.com/api/docs/models",
      "canonicalUrl": "https://developers.openai.com/api/docs/models/gpt-5",
      "evidenceState": "api_listed",
      "confirmationState": "confirmed",
      "firstObservedAt": "2026-07-29T20:00:00.000Z",
      "sourcePublishedAt": null,
      "sourceUpdatedAt": null,
      "sourcePublishedPrecision": null,
      "sourceUpdatedPrecision": null,
      "detectedVia": "api",
      "correctedAt": null,
      "retractedAt": null
    }
  ],
  "nextCursor": "WzEsIjIwMjYtMDctMjlUMjA6MDA6MDAuMDAwWiIsImV2dF8wMUpZLi4uIl0",
  "hasMore": false,
  "usage": {
    "requests": 1,
    "limit": 100000,
    "period": "2026-07",
    "resetsAt": "2026-08-01T00:00:00.000Z"
  }
}
```

### Event field semantics

| Field | Meaning |
| --- | --- |
| `id` | Stable identifier. Deduplicate events with this value. |
| `eventType` | Machine-readable change type. Treat it as an extensible string. |
| `subjectType`, `subjectKey` | Entity affected by the event, when Radar can identify it. |
| `sourceUrl` | Original evidence link. Preserve it in downstream records and messages. |
| `confirmationState` | `confirmed` or `unconfirmed`. Unconfirmed events are Early Signals. |
| `evidenceState` | How Radar established the event. Treat it as an extensible string. |
| `firstObservedAt` | When Radar first observed the event. |
| `sourcePublishedAt`, `sourceUpdatedAt` | Publisher-provided time when available. Null means the source supplied no reliable time. |
| `sourcePublishedPrecision`, `sourceUpdatedPrecision` | `timestamp`, `date`, or null. Do not treat a date-only value as a precise time. |
| `correctedAt`, `retractedAt` | Set if Radar later corrected or retracted the event. |

Accept event and evidence string values the integration does not recognize yet.
Preserve unknown values instead of rejecting the entire event.

### Reliable polling

1. Request `/v1/events?limit=50` on the first run.
2. Process the response and deduplicate by `id`.
3. Save `nextCursor` only after processing succeeds.
4. If `hasMore` is `true`, request the new cursor immediately.
5. When `hasMore` is `false`, poll every 60 seconds.
6. Keep `sourceUrl` with every downstream record.

```bash
curl "https://radar.benchlm.ai/v1/events?cursor=$RADAR_CURSOR&limit=200" \
  -H "Authorization: Bearer $RADAR_API_KEY"
```

### Limits and errors

Pro includes 100,000 API requests per account usage period. Each authenticated
request consumes one request, including a request that later fails query
validation. Radar also applies a fixed 120-request-per-minute burst guardrail.

Responses include:

- `X-RateLimit-Limit`
- `X-RateLimit-Remaining`
- `X-RateLimit-Reset`
- `X-RateLimit-Burst-Limit`
- `X-RateLimit-Burst-Remaining`
- `X-RateLimit-Burst-Reset`
- `Retry-After` on `429 Too Many Requests`
- `X-RateLimit-Scope` on `429`: `burst` or `usage_period`

Errors use JSON with a stable machine-readable `code`. Branch on `code`, not
the human-readable `error` message:

```json
{ "error": "Event cursor is invalid", "code": "invalid_request" }
```

| Status | Meaning | Action |
| --- | --- | --- |
| `400` | Invalid cursor, provider, type, or timestamp | Correct the request; do not retry unchanged |
| `401` | Missing, malformed, revoked, or unknown key | Replace the key |
| `403` | The key's account no longer has Pro access | Restore Pro access or stop polling |
| `429` | Burst or account usage-period allowance reached | Read `X-RateLimit-Scope` and wait for `Retry-After` |
| `5xx` | Temporary Radar failure | Retry with capped exponential backoff and jitter |

## Signed webhooks

A signed webhook sends selected Radar alerts to an HTTPS endpoint. Create it in
**Delivery → Signed webhook**, copy the one-time signing secret, and send a test
event. The signing secret starts with `radar_whsec_`.

Destinations must use public HTTPS URLs. Radar rejects credentials in the URL,
local or private network targets, redirects, and endpoints that cannot be
reached within 10 seconds.

### Request headers

| Header | Use |
| --- | --- |
| `Content-Type` | `application/json` |
| `Radar-Signature` | `t=<unix>,v1=<hex HMAC-SHA256>` signature |
| `Radar-Event-Id` | Stable event ID; use it to deduplicate deliveries |
| `Idempotency-Key` | `radar-event-<event ID>` |
| `User-Agent` | `BenchLM-Radar/0.1 (+https://benchlm.ai/radar)` |

### Payload

Webhook fields use snake_case. API fields use camelCase.

```json
{
  "schema_version": "1.0",
  "id": "evt_01JY...",
  "type": "api_model_listed",
  "created_at": "2026-07-29T20:00:00.000Z",
  "data": {
    "provider": "openai",
    "subject": {
      "type": "model",
      "key": "gpt-5"
    },
    "title": "New OpenAI API model",
    "summary": "A model appeared in the first-party API catalog.",
    "evidence": "api_listed",
    "confirmation": "confirmed",
    "source_url": "https://developers.openai.com/api/docs/models",
    "source_published_at": null
  }
}
```

### Payload variations by event type

The webhook envelope and field names are invariant across event types. Every
payload declares `schema_version: "1.0"`; reject or quarantine versions the
integration does not support. There is no separate outage schema or release
schema. The specific event is described by `type`, `data.subject`, the title
and summary, evidence, and source fields.
`data.summary`, either value in `data.subject`, and
`data.source_published_at` can be null.

| Event family | Representative `type` values | Variation |
| --- | --- | --- |
| Models | `model_release`, `api_model_listed`, `model_removed` | The model or catalog item appears in `data.subject.key`, title, and summary. |
| Outages | `outage_started`, `outage_updated`, `outage_resolved` | Each lifecycle change is its own event. The source links to the first-party incident. |
| Pricing | `pricing_update`, `pricing_changed` | The title and summary state the source-confirmed pricing change. |
| Benchmarks | `benchmark_model_added`, `benchmark_rank_changed`, `benchmark_version_changed` | `data.subject.type` is `benchmark`; the source links to the original leaderboard. |
| API and docs | `api_contract_update`, `api_endpoint_documented`, `documentation_update` | The changed first-party contract or document is summarized and linked. |
| Products | `app_feature_update`, `desktop_app_update`, `cli_update`, `release_note` | The product, version, or feature is carried by the subject and descriptive fields. |
| Research | `research_published`, `official_post_published` | The source links to the first-party paper, research page, or post. |
| Test delivery | `destination_test` | Uses the same signed envelope with sample Radar values. Tests are immediate and are not retried. |

Event type values are extensible. Consumers must preserve and accept unknown
strings. Use the descriptive fields for display; do not derive customer-facing
prose by splitting the `type` string.

#### Outage example

```json
{
  "schema_version": "1.0",
  "id": "evt_outage_01JY...",
  "type": "outage_started",
  "created_at": "2026-08-14T14:01:12.000Z",
  "data": {
    "provider": "anthropic",
    "subject": { "type": "provider", "key": "anthropic" },
    "title": "Elevated API errors",
    "summary": "Anthropic reported elevated errors affecting its API.",
    "evidence": "documented",
    "confirmation": "confirmed",
    "source_url": "https://status.claude.com/incidents/example",
    "source_published_at": "2026-08-14T14:00:00.000Z"
  }
}
```

An update to the incident uses `outage_updated`; a resolution uses
`outage_resolved`. Each has its own stable `id` and must be processed as a
separate lifecycle event.

#### Benchmark example

```json
{
  "schema_version": "1.0",
  "id": "evt_benchmark_01JY...",
  "type": "benchmark_rank_changed",
  "created_at": "2026-08-14T15:10:00.000Z",
  "data": {
    "provider": "frontier-bench",
    "subject": { "type": "benchmark", "key": "frontier-bench-v0-1" },
    "title": "Claude Code moved to #2 on FrontierBench",
    "summary": "Claude Code now has 31.4% accuracy.",
    "evidence": "documented",
    "confirmation": "confirmed",
    "source_url": "https://hub.harborframework.com/datasets/frontier-bench/frontier-bench/latest",
    "source_published_at": null
  }
}
```

`created_at` is when Radar first observed the event, not the delivery attempt
time. The JSON `id` and `Radar-Event-Id` contain the same stable event ID.

### Verify the signature

Compute the HMAC over the timestamp, one period, and the exact raw request body:

```text
signed_payload = timestamp + "." + exact_raw_request_body
expected = hex(HMAC-SHA256(signing_secret, signed_payload))
```

Do not parse and re-serialize JSON before verification. Whitespace and property
order are part of the signature.

```js
import { createHmac, timingSafeEqual } from 'node:crypto'

export function verifyRadarWebhook(rawBody, signatureHeader, secret) {
  const values = Object.fromEntries(
    String(signatureHeader)
      .split(',')
      .map(part => part.split('=', 2))
  )
  const timestamp = Number(values.t)
  const signature = values.v1
  if (!Number.isSafeInteger(timestamp) || !/^[a-f0-9]{64}$/.test(signature)) {
    return false
  }

  // Reject replayed requests. Use a clock-synchronized server.
  if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false

  const expected = createHmac('sha256', secret)
    .update(String(timestamp))
    .update('.')
    .update(rawBody)
    .digest('hex')
  return timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expected, 'hex')
  )
}
```

Pass the exact UTF-8 text or bytes received by the server. Verify the signature
before parsing JSON or starting side effects.

### Acknowledge, deduplicate, and retry

Return any `2xx` response within 10 seconds after the event is durably accepted.
Queue slow work and respond before processing it.

Webhook delivery is at-least-once. Store `Radar-Event-Id` behind a unique
constraint and make processing idempotent.

Radar retries temporary failures up to eight delivery attempts with exponential
backoff, starting at 30 seconds and capped at one hour. A `429` response can
control the delay with `Retry-After`, also capped at one hour. These statuses
are permanent and pause the destination without retrying the same event: `400`,
`401`, `403`, `404`, `405`, `410`, and `422`.

Test delivery is immediate and is not retried. A successful test activates a
new or paused destination.

## Slack and Discord

Slack and Discord destinations are managed with signed webhooks in Delivery.
Paste an official Slack incoming-webhook URL or Discord webhook URL. Radar sends
the platform-native format. These connectors do not receive `Radar-Signature`,
because the destination belongs to Slack or Discord rather than the customer.

All destination types receive alerts selected by Delivery rules and preserve a
link to the original source. A Pro account can have up to five shared
destinations across Slack, Discord, and signed webhooks.

## Launch checklist

- Create one API key per application or environment.
- Store API keys and signing secrets outside source control, prompts, and logs.
- Persist `nextCursor` only after response processing succeeds.
- Deduplicate API and webhook events by `id`.
- Preserve `sourceUrl` or `data.source_url` with downstream work.
- Accept new event and evidence string values.
- Require a supported webhook `schema_version` before reading `data`.
- Preserve the raw webhook body until signature verification is complete.
- Reject webhook timestamps outside the integration's replay window.
- Persist or enqueue a webhook event before returning `2xx`.
- Return `4xx` only for permanent rejection and `5xx` for a temporary failure
  Radar should retry.
- Send a test webhook before enabling production side effects.

## Key and secret rotation

- Rotate an API key by creating a replacement, deploying it, and revoking the
  old key.
- Rotate a webhook secret from Delivery. Radar shows the replacement once,
  keeps signing with the current secret for 24 hours, then activates the new
  secret. Deploy both secrets during that overlap and remove the old one only
  after a delivery signed with the replacement succeeds.
- Deleting a destination erases its encrypted URL and signing secret and
  cancels its pending deliveries.
