---
title: "Keyword health"
description: "See which keywords cost more than they're worth, where their noise comes from, and fix them with one PATCH."
canonical: https://docs.rumoro.dev/guides/keyword-health
markdown: https://docs.rumoro.dev/guides/keyword-health.mdx
---

# Keyword health

See which keywords cost more than they're worth, where their noise comes from, and fix them with one PATCH.

You pay for every matched mention, relevant or not. The health report tells you whether a keyword is worth what it costs, where its noise comes from, and which change would reduce it, with an estimate of what that change would have saved.

```ts tab="TypeScript"
const { data: health } = await rumoro.getKeywordHealth({ path: { id: 'kw_...' }, query: { range: '30d' } });
```

```python tab="Python"
health = rumoro.keywords.health("kw_...", range_="30d")
```

```bash tab="curl"
curl "https://api.rumoro.dev/v1/keywords/kw_.../health?range=30d" \
  -H "Authorization: Bearer $RUMORO_API_KEY"
```

`range` is `7d`, `30d` (the default) or `90d`. It counts UTC days up to today, by the time of the match. Reading the report changes nothing and costs nothing.

## Status

The first status that applies is used.

| Status | When |
| --- | --- |
| `paused` | Muted by you, stopped by an empty balance, or stopped by the noise brake |
| `capped` | At its monthly mention cap, including the 200 of the welcome credit. It matches again on the 1st (UTC) or when the cap goes up. |
| `noisy` | 20 or more scored matches, of which less than 30% are relevant (relevance 40 or higher, or marked relevant) |
| `new` | Younger than 7 days and not noisy, or changed in the last 7 days with fewer than 20 scored matches since |
| `quiet` | At least 7 days old, with no relevant match in the period |
| `healthy` | None of the above |

`reasons` explains the status in plain words. It also points out a platform where 90% or more of at least 10 scored matches were noise, and a keyword that takes half or more of the workspace's matches.

In `GET /v1/keywords`, each keyword has `stats.health`. It uses the same rules over 14 days, so it can differ from a 30-day report.

## Judged since your last change

When you change a keyword's matching rules, platforms or context, or unmute it, the status, `reasons`, noise analysis and suggestions only look at matches since that change, if it falls in the period. `stats.judgedSince` shows when that was, and `null` means the whole period counts.

Suggestions need 20 scored matches in that time. After a change they wait for 20 new ones, and `reasons` says so. A report never suggests again what you just applied.

## Numbers

`stats` always covers the whole period. It has `matches`, `relevant`, `filtered` (noise), `unscored`, `noiseShare`, `workspaceShare`, `byPlatform`, `weekly` (per 7 days) and `cost`, which matches the keyword's row in [`GET /v1/usage/breakdown`](/billing).

## Where the noise comes from

The report takes the newest scored posts of the period, up to 200 noise and 200 relevant ones, under the keyword's current rules and platforms. Reviews are left out, because they match by app and not by text.

- `noiseTerms` lists up to 10 words or two-word phrases that appear in at least 3 noise posts and are at least twice as common there (`noisePosts`, `relevantPosts`, `lift`). Words from the keyword itself never appear.
- `noiseAuthors` lists people with at least 3 noise posts and no relevant one, with the `entry` that `matching.excludedAuthors` would save. When a link doesn't point to one person, such as a GitHub app, the entry is their name.

## Suggestions

Each suggestion has a `patch` you can send to `PATCH /v1/keywords/{id}` unchanged. Lists in it are complete, so your existing entries are included.

### Types

| `type` | What it does |
| --- | --- |
| `platforms` | Removes platforms where 90% or more of at least 10 scored matches were noise, but never all of them. A keyword that tracked every platform gets an explicit list, so platforms added later are not included. |
| `excluded_terms` | Adds the fewest noise terms that cover the noise, which together appear in at most 2% of relevant posts. It needs at least 20 relevant posts, or none at all. |
| `excluded_authors` | Adds the authors who only brought noise |
| `required_terms` | For a keyword without required terms, one word that appears in 90% or more of its relevant posts and removes at least half of the noise. Sets `requiredMode` to `any`. |
| `context` | A new context for the classifier. It changes how new matches are scored, not what is matched or billed. Without `ai=true` it is only offered for `noisy` keywords, as the current context plus "Ignore posts about …" with the three biggest noise terms. |

### Effect

Every suggestion except `context` has an `effect`. Rumoro runs the new rules over the sample and scales the result up to the whole period when the sample is smaller (`exact` tells you which). `centsSaved` is the removed matches at $0.008 each.

```json
{
  "type": "excluded_terms",
  "values": ["moving", "database"],
  "why": "Exclude \"moving\", \"database\": common in its noise, absent from its relevant matches. In the last 30 days it would have removed 38 noise matches and 0 relevant ones.",
  "patch": { "matching": { "excludedTerms": ["moving", "database"] } },
  "effect": { "noiseRemoved": 38, "relevantRemoved": 0, "centsSaved": 30, "sample": { "noiseRemoved": 38, "noise": 41, "relevantRemoved": 0, "relevant": 26 }, "exact": true },
  "source": "rules"
}
```

### Apply one

Send the `patch` as it is.

```ts tab="TypeScript"
await rumoro.updateKeyword({ path: { id: 'kw_...' }, body: { matching: { excludedTerms: ['moving', 'database'] } } });
```

```python tab="Python"
rumoro.keywords.update("kw_...", matching={"excludedTerms": ["moving", "database"]})
```

```bash tab="curl"
curl -X PATCH https://api.rumoro.dev/v1/keywords/kw_... \
  -H "Authorization: Bearer $RUMORO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "matching": { "excludedTerms": ["moving", "database"] } }'
```

New rules apply from the next check, and mentions you already have stay. A keyword stopped by the noise brake starts again when its required or excluded terms, platforms or context change, as long as the balance covers another day. Excluding authors alone doesn't restart it, so send `"muted": false` as well.

## A context written by a model

With `ai=true`, a language model writes the context from the term, the current context and sample posts. `ai.status` tells you what happened.

| `ai.status` | Meaning |
| --- | --- |
| `off` | Not requested |
| `generated` | Written just now |
| `cached` | The same keyword and period, unchanged, within the last 24 hours |
| `unavailable` | No noise to learn from, or no usable answer. A `noisy` keyword gets the rule-based context instead. |
| `rate_limited` | More than 20 model calls in the last hour for this workspace |

## Limits

Reports are kept for 5 minutes and model contexts for 24 hours. Changing the keyword starts fresh. A workspace can read 30 reports a minute through the API, MCP and dashboard together, cached ones included. After that it gets `429 rate_limited` with `Retry-After`.

## Elsewhere

The Keywords list marks `noisy` and `quiet` keywords, and each keyword's **Health, last 30 days** section has an **Apply** button for every suggestion. In the CLI, run `rumoro keywords:health kw_... --range 30d`. In MCP, use `get_keyword_health` and send patches with `update_keyword`.
