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

# Find people with your problem

> Build a keyword from a pain phrase instead of a brand name, then read only the mentions that need you.

Most monitoring starts from a name you already own. That works once people are
saying it. Before then — a product nobody has heard of, a name you picked last
week — the feed is empty and the tool looks broken.

This guide builds the other kind of keyword: one that starts from **what people
say when they have the problem you solve**, and reads back only the mentions
worth answering.

Nothing here is a separate product. It is `POST /v1/keywords` with different
terms, and `GET /v1/mentions` with two filters.

## 1. Search for the sentence, not the name

A brand keyword looks for one token. A discovery keyword looks for the phrases
people type when they are stuck:

```json theme={null}
{
  "name": "Brand monitoring pain",
  "subjectRole": "topic",
  "globalCriteria": {
    "market": "global",
    "terms": [
      "how do I track brand mentions",
      "alternative to Brand24",
      "tired of manually searching reddit"
    ]
  }
}
```

`terms` are OR'd into one upstream call, so several phrasings of the same
complaint cost one poll, not three.

A `topic` keyword sent without a `match` is created with `match: "word"`: an
item is kept only when it carries one of the phrases as written. That is what
keeps a phrase keyword readable, because most platforms answer a phrase with
whatever they think is near it. When people type your phrases too many
different ways and the keyword stays quiet, send `"match": "contains"` to widen
it. `"match": "platform"`, stated explicitly, keeps the platform's own matching.

<Note>
  Write the phrases in the words your customers use, not the words you use.
  "Brand monitoring" is your category; "I keep missing when people mention us"
  is their problem.
</Note>

## 2. Poll the places people describe problems in prose

Five sources carry that kind of sentence. Enable them together:

```json theme={null}
{
  "sources": [
    { "source": "reddit", "enabled": true },
    { "source": "hackernews", "enabled": true },
    { "source": "stackoverflow", "enabled": true },
    { "source": "lemmy", "enabled": true },
    { "source": "x", "enabled": true }
  ]
}
```

Each enabled Source is one slot and one charge per poll. See
[Credits](/credits#polling).

## 3. Switch the readings on

A phrase keyword catches everyone who used the phrase, and most of them are not
prospects: someone answering, someone quoting, someone complaining about a
different tool. Two readings sort them, and they are what step 4 filters on.

```json theme={null}
{
  "sentimentEnabled": true,
  "aiEnabled": true,
  "subjectRole": "topic"
}
```

`sentimentEnabled` reads every mention as `positive`, `neutral`, `negative`,
`question` or `mixed`. `aiEnabled` also writes the **intent** — what the item is
trying to do: `purchase_intent`, `comparison`, `question`, `complaint`, `praise`
or `other`.

Both cost credits per classified item. Without them, step 4 has nothing to
filter on.

A third reading tells a prospect from a bystander: an **agent step** asked of
every mention. Give it a boolean `problem_fit` field and it becomes what the
`opportunity` filter of step 4 reads:

```json theme={null}
{
  "aiStep": {
    "instruction": "Decide whether the person behind this item is describing the problem your product solves, and how badly it is hurting them. Judge only from what the item itself says.",
    "schema": {
      "problem_fit": {
        "type": "boolean",
        "description": "True only when they describe having the problem themselves."
      },
      "current_workaround": {
        "type": "string",
        "maxLength": 120,
        "description": "What they say they are doing about it today. Null when they do not say."
      }
    }
  }
}
```

It is not added for you on the API: a step costs credits per item, and a call
is never billed for a step it did not ask for. A keyword added as a Topic in the
dashboard carries this one from the start.

`subjectRole` costs nothing. It says what this keyword is about (here a problem
rather than a product), it sets the default `match` of step 1, and it is what a
reply draft reads to decide whose voice it writes in. Leave it out and the
keyword is `own`, which is right for a brand keyword and wrong for this one: a
draft would answer as if the pain phrase were your company's name.

## 4. Read back only what needs you

`GET /v1/mentions` filters on both readings. Each is repeatable, and repeated
values are OR'd:

```bash theme={null}
curl "https://api.nephia.cc/v1/mentions?intent=complaint&intent=question&sentiment=negative" \
  -H "Authorization: Bearer $NEPHIA_API_KEY"
```

That reads: *someone with a problem or a question, who is unhappy about it.*

Or ask for the **opportunities** directly:

```bash theme={null}
curl "https://api.nephia.cc/v1/mentions?opportunity=true" \
  -H "Authorization: Bearer $NEPHIA_API_KEY"
```

An opportunity is a mention of a `topic` or `competitor` keyword that is highly
relevant to it (`relevance: high`) and whose author is someone to answer. On a
keyword whose step has a boolean `problem_fit`, that is `problem_fit: true`
under the step it carries now; on any other, an intent of `purchase_intent`,
`comparison` or `question`. An `own` keyword has none. The same parameter works
on `GET /v1/keywords/{id}/results`, and the dashboard's Feed offers it as
**Opportunities**, at the head of the Intent filter.

| Ask | Query string |
| - | - |
| People describing a pain | `?intent=complaint` |
| People asking how to do it | `?intent=question` |
| People weighing tools | `?intent=comparison&intent=purchase_intent` |
| The unhappy ones, whatever they are doing | `?sentiment=negative` |
| What nothing has read yet | `?sentiment=unread` |
| Only the two forums that matter to you | `?source=reddit&source=hackernews` |
| Only what people actually reacted to | `?engagement_min=10` |
| A hundred likes on X, fifty score on Reddit | `?engagement=x:likes>=100&engagement=reddit:score>50` |

**Repeated values of one key are OR'd; different keys are AND'd.** So
`?source=reddit&source=hackernews&sentiment=negative&intent=complaint` reads:
*on Reddit or Hacker News, negative, and a complaint.* `source`, `sentiment` and
`intent` are all repeatable.

`unread` is the wire spelling of "not yet classified" — a real filter value, not
an absence. It is how you find what your readings have not caught up with.

<Warning>
  Filtering on `sentiment`, `intent`, `engagement_min` or `engagement` reads the
  polled stream only. Items kept from a one-off Explore run carry no readings and
  no counters, so they cannot satisfy those filters and are left out rather than
  shown misleadingly.
</Warning>

## Cutting the mentions nobody reacted to

`engagement_min` keeps mentions with at least that many **interactions** — likes,
replies, reposts, comments or score, whichever the Source reports. It is the
noise gate for a busy Source: on a typical month of X mentions, roughly seven in
ten have no interactions at all, so `?engagement_min=1` alone removes most of
what you would otherwise page through.

Three things it does not do, each of which will otherwise surprise you:

* **It never counts views.** A view is not an interaction, and on the Sources
  that report one it is an order of magnitude bigger than every other counter —
  including it would make this a filter on reach.
* **It leaves out mentions with no counters at all**, rather than treating them
  as zero. RSS items and AI answers report no audience, and mentions recorded
  before 4 September 2026 predate the field. None of them were measured, so none
  can satisfy a threshold. `?engagement_min=0` is therefore *narrower* than no
  filter: it asks for everything that was counted.
* **The numbers are captured when we collect a mention, and never refreshed.**
  A post caught two minutes after it went up is measured with the counters it
  had then. So a threshold reads against your most recent mentions, which are
  the ones with the least time to have earned anything.

A number that means one thing on one Source means another elsewhere: five points
on Hacker News is a story that landed, five likes on X is nothing. That is what
`engagement` below is for.

## Asking a different question of each Source

`engagement_min` thresholds one number for everything, and the sum it thresholds
is lossy: a post with a hundred replies and no likes clears `engagement_min=100`
exactly as a post with a hundred likes does, and those are not the same
mentions. *It was liked* and *it was argued with* is the distinction a sum
erases.

`engagement` is a **rule**, and it is repeatable:

```
<source|*>:<metric><operator><number>
```

```bash theme={null}
curl -G https://api.nephia.cc/v1/mentions \
  -H "Authorization: Bearer $NEPHIA_API_KEY" \
  --data-urlencode "engagement=x:likes>=100" \
  --data-urlencode "engagement=reddit:score>50"
```

* **Metrics** are `likes`, `replies`, `reposts`, `comments`, `score` and
  `views`, plus `total` for the same interaction sum `engagement_min` reads.
  `engagement_min=10` is exactly `engagement=*:total>=10`.
* **Operators** are `>=`, `>`, `=`, `<` and `<=`. The number is whole and may be
  negative — Reddit and Lemmy net downvotes out, so `?engagement=reddit:score<0`
  is a real question: the mentions that were argued with.
* **`*`** stands for any Source no named rule already covers. Pair it with
  `total` — `?engagement=*:total>=25` is "at least 25 interactions, wherever it
  was said", which is `engagement_min` with an operator of your choosing. A
  named counter under `*` is legal but rarely what you mean: `*:likes>=100`
  drops every Source that does not count likes, by the rule below.

Two rules about the scope, and both are the kind you discover by noticing rows
missing:

* **A Source no rule names passes.** `?engagement=x:likes>=100` narrows X and
  leaves Hacker News exactly as it was. Without that, every rule would also be a
  Source filter in disguise — and `?source=` already exists for that. A named
  rule overrides `*` for its own Source, and several rules on one Source are
  ANDed.
* **A metric that was never counted satisfies nothing — `<` included.** This is
  the `engagement_min` rule above, one level down and sharper: YouTube reports
  no likes at all, so `?engagement=youtube:likes<10` returns *none* of your
  YouTube mentions rather than all of them. Only a metric the Source actually
  reports can be filtered on.

Send `engagement` or `engagement_min`, never both: they are two spellings of one
filter, and a request carrying both is a `400`.

Every mention comes back with both readings on it, so you can sort further
without a second call:

```json theme={null}
{
  "id": "…",
  "source": "reddit",
  "title": "Tired of manually searching for our name every morning",
  "sentiment": "negative",
  "intent": "complaint",
  "url": "https://reddit.com/…",
  "occurredAt": "2026-09-01T08:12:00.000Z"
}
```

## 5. Qualify each one with your own question

The readings say what an item *is*. To ask whether it is **your** problem, give
the keyword an agent step (one instruction and a schema, applied to every item it
catches):

```json theme={null}
{
  "aiStep": {
    "instruction": "Decide whether the person behind this item is describing the problem your product solves, and how badly it is hurting them. Judge only from what the item itself says.",
    "schema": {
      "problem_fit": { "type": "boolean" },
      "urgency": { "type": "enum", "values": ["low", "med", "high"] },
      "current_workaround": { "type": "string", "maxLength": 120 }
    }
  }
}
```

The answers ride on each mention under `agent`, spelled with your own field
names:

```json theme={null}
{
  "agent": {
    "problem_fit": true,
    "urgency": "high",
    "current_workaround": "a saved search and a calendar reminder"
  }
}
```

`current_workaround` is the one with commercial value beyond triage: what
someone names as their current answer is what your reply has to be better than.

See [AI](/ai) for the schema's field types and what an agent pass costs.

## 6. Get told instead of polling

Do not poll `/v1/mentions` on a timer. A keyword delivers what it catches to a
[webhook](/webhooks), and every mention arrives with its readings already on it,
so the filter that reads well in step 4 becomes a condition in your own
handler.

## What this does not do

It finds people **describing a problem**. It does not find customers, and it
does not tell you they want to buy — a `problem_fit` of `true` is a reading of
one post, not a qualified lead. Treat the output as a list of conversations
worth joining, and expect to discard some of it.


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