---
title: "How it works"
description: "The ingestion pipeline, per-platform freshness, and the mention lifecycle."
canonical: https://docs.mentio.dev/how-it-works
markdown: https://docs.mentio.dev/how-it-works.mdx
---

# How it works

The ingestion pipeline, per-platform freshness, and the mention lifecycle.

## The pipeline

Mentio runs a staged pipeline: platforms are fetched on a schedule, normalized into raw items, matched against every workspace's keywords, classified by an LLM, and delivered.

Ingestion happens once, globally. When two workspaces track the same term, the platform fetches and stores those posts once, then matches them to both. Your mention feed is the workspace-scoped view over that shared ingest.

## Per-platform freshness

Each platform is polled on its own rhythm, from real time (Bluesky) to once a day (the app stores). The [Platforms](/platforms) tab has the table, and a page per platform on what is found and what comes with it. A mention can only be as fresh as the platform's own API surface; the pipeline adds under a minute on top.

## Matching rules

A term matches case-insensitively, on every platform the keyword tracks, in one of two ways:

* **As the phrase.** The post holds the term as written. Every mention of this kind bills, relevant or not.
* **As close words.** For a term of two words or more, the post holds every meaningful word of it within a few words of the others, in any order. Plurals count (`organization` and `organizations`), and so do joined, hyphenated and spaced spellings (`nonprofit`, `non-profit`, `non profit`). Filler words like *for*, *to* and *the* are not required. A close-words post is kept, alerted and billed **only when the classifier scores it relevant**; otherwise it is dropped and costs nothing. It reads `keyword.matchedAs: "close_words"` on the mention. Close words apply to the posts a platform returns when Mentio searches for your keyword; Bluesky's live stream and the Hacker News, DEV and news feeds match the phrase only.

Put the term in double quotes (`"social media api"`), or set `matching.exactPhrase`, for the phrase alone. A single word and a case-sensitive term are always matched as written.

Short terms people actually write match far more than sentences: `fundraising agency` finds what `looking for a corporate fundraising agency` misses, and the classifier picks out the posts asking for one.

Two layers of rules narrow that before anything is stored, which is what makes them different from an alert's filter: a post a rule rejects never becomes a mention, is never classified and never bills.

| Layer     | Where                                                                                                              | Rules                                                                                                                                                                                                                                                                                                          |
| --------- | ------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Keyword   | `matching` on [`POST`](/api/keywords/create-keyword) and [`PATCH /v1/keywords/{id}`](/api/keywords/update-keyword) | `requiredTerms` the post must also contain (`requiredMode` `any` or `all`); `excludedTerms` that drop it, a `*` at an end being a wildcard (`beta.*`); `excludedAuthors` in the same forms an alert's mute list takes; `caseSensitive` for an acronym (`RAG`, never `rag`); `exactPhrase` for the phrase alone |
| Workspace | [`GET`](/api/filters/get-filters) and [`PATCH /v1/filters`](/api/filters/update-filters)                           | `excludedTerms` and `excludedAuthors` for every keyword; `excludedRepos` (GitHub `owner/name`); `subreddits.only` (an allowlist) and `subreddits.excluded`                                                                                                                                                     |

A keyword also takes a `context` sentence the classifier reads for that term only ("Arc is our browser; ignore the geometry word"). Unlike the rules, it changes the score, not what is stored. Rules apply to new posts from the next poll; stored mentions keep their place.

## Mention lifecycle

The pipeline's internal stages never appear in the API. Four fields tell you where a mention stands:

| Field            | Values                    | Meaning                                                                                                                        |
| ---------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `status`         | `open`, `ignored`, `done` | Your triage. Every mention starts `open`; you set the other two with [`PATCH /v1/mentions/{id}`](/api/mentions/update-mention) |
| `relevant`       | `true`, `false`           | Scored at or above the relevance threshold (currently 40). `false` until the classifier has run                                |
| `delivered`      | `true`, `false`           | At least one alert channel has received it                                                                                     |
| `classification` | object or `null`          | `null` until the classifier has run. `classification.failed` is `true` when the classifier errored out on this mention         |

Every matched mention stays queryable whatever it scored: noise (`relevant: false`) is never delivered, but `GET /v1/mentions?relevant=false` still lists it. Ignored and done mentions are excluded from delivery.

### Cross-posts

When the same author posts the same text again for the same keyword within three days (one announcement pasted into five subreddits, a launch post sent twice), the later posts are **cross-posts** of the first. The first is listed as usual and carries the others in `duplicates` (id, platform, url, publishedAt); each copy has `duplicateOf` set to the first one's id and is left out of the list, the JSON export, the digest's lists and instant alerts. A copy takes the first post's classification, so the two are never relevant apart. Pass `includeDuplicates=true` to list copies as mentions of their own. The CSV export keeps one row per mention, copies included, with a `duplicate_of` column.

Two posts count as copies when their text is nearly the same, holds the same numbers and links to the same sites, so a reworded title on the same body groups, while a release note for another version, a dependency bump with another hash or a job post for another role does not. On YouTube, where a channel reuses one description across its videos, only an identical text groups. Posts with no known author are never grouped.

Every copy is still a real post and a real match: it **bills** like any mention, counts toward a keyword's mention cap and is counted in the digest's totals. If the first post's classification fails, its copies are listed and alerted on their own.

## Classification

The classifier reads your [company profile](/api/company/get-company) (or the [group's](/guides/groups#a-company-description-per-group) own description, when its keyword is in a group that has one) and scores each mention. The result is the `classification` object:

| Field          | Values                                                                                                                                                                                                                                                                                                                                                                                             |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `relevance`    | 0 to 100                                                                                                                                                                                                                                                                                                                                                                                           |
| `sentiment`    | `positive`, `neutral`, `negative`                                                                                                                                                                                                                                                                                                                                                                  |
| `intents`      | Intent and topic tags: `buy_intent`, `question`, `complaint`, `praise`, `comparison`, `churn_intent`, `bug_report`, `pricing`, `hiring`, `event`, `promotional`, `testimonial` (a user vouching for it from their own use), `industry_insight` (data, a trend or analysis about the field), `launch` (a product, feature or release announced), `feedback` (a suggestion or request to improve it) |
| `automated`    | `true` when the post reads as machine-made: a bot, a templated post, a generated digest                                                                                                                                                                                                                                                                                                            |
| `language`     | The language the post is written in, as an ISO 639-1 code (`en`, `es`); `null` when unknown                                                                                                                                                                                                                                                                                                        |
| `country`      | Where the author is, as a lowercase ISO 3166-1 alpha-2 code (`us`, `in`), read from their profile location and the post; `null` when nothing tells                                                                                                                                                                                                                                                 |
| `organization` | `true` when the author is a company, brand or organization rather than a person (a LinkedIn company page always is)                                                                                                                                                                                                                                                                                |
| `confidence`   | How sure the classifier is of its relevance verdict, 0 to 1; `null` when the verdict came without one                                                                                                                                                                                                                                                                                              |
| `uncertain`    | `true` when the verdict deserves a look: confidence under 0.4, or the model that wrote the note disagreed with it. A flag, never a filter on what you see                                                                                                                                                                                                                                          |
| `note`         | One-line explanation, at most 200 characters                                                                                                                                                                                                                                                                                                                                                       |
| `failed`       | `true` when the classifier errored; a failed classification is never billed                                                                                                                                                                                                                                                                                                                        |
| `feedback`     | Your verdict, when you gave one: `relevant`, `sentiment`, when, and the values it replaced                                                                                                                                                                                                                                                                                                         |

### Your verdict

The classifier can be overruled. [`PATCH /v1/mentions/{id}`](/api/mentions/update-mention) with `relevant: false` marks a mention as noise: its relevance becomes 0, it leaves the relevant feed, the digests, the counts and the analytics. `relevant: true` does the opposite for one the classifier missed (relevance 100), and `sentiment` corrects the label. `null` withdraws a verdict and restores the classifier's value, which `classification.feedback.original` kept. Verdicts never bill or unbill a mention, and every keyword reports how many it received under `stats.feedback`. A mention still queued for classification answers `409 classification_pending`.

<Callout title="Company context is the biggest lever">
  Relevance scoring is only as good as the context you provide. Describe what your company does, its use cases, its competitors and your own relevance guidelines with `PATCH /v1/company`; the classifier reads the `context` composed from them. A keyword's own `context` refines it for one term.
</Callout>

## Author data

The people behind the mentions are stored once for every workspace: the public post, the author's public name, handle, avatar and counts, and on some platforms public profile facts (bio, company, location, website, linked accounts, and an email address only where the person published it on their profile). Mentio processes this on the basis of legitimate interests (GDPR Art. 6(1)(f)); the notes, tags and outreach a workspace adds about a person are the workspace's own, with the customer as controller. The [MCP tools](/mcp) never return a person's email address: `profile.email` is always `null` over MCP, while the REST API and the dashboard show it where the person published it. Anyone can ask for their posts and profile to be removed at [mentio.dev/privacy/remove](https://mentio.dev/privacy/remove/); we complete every request within 30 days, and the person's posts leave every workspace.
