# Scoring tools

Two read-only tools let an assistant evaluate the citation value of a domain or a prospective placement:

- `score_publisher_value` — the **AI Placement Value Score (AIPVS)** for one or more publisher domains.
- `score_placement_quality` — the **AI Placement Quality Score (PQS)** for a prospective placement, optionally combined with a publisher's AIPVS.

Both are **read-only**: scoring never creates a publisher record, edits anything, or queues enrichment. When a domain hasn't been enriched yet, the score comes back as an estimate flagged with `needsEnrichment: true`.

A third set of tools scores **real** coverage in bulk — see [Bulk placement scoring for an organization](#bulk-placement-scoring-for-an-organization) below. Those ones write.

## `score_publisher_value` (AIPVS)

Scores one or more publisher domains 0–100 — how valuable a citation from that domain is for AI visibility. AIPVS combines three layers: **organic authority**, **AI citation accessibility**, and **AI training influence**.

| Parameter | Type | Notes |
|---|---|---|
| `domains` | string[] | 1–10 publisher domains to score. |
| `propertyId` | string (optional) | Score in this property's brand context — adds category relevance and brand uplift. From `list_properties`. |

Each result includes the publisher's **score**, **tier**, **AI impact multiplier**, per-layer detail, and `needsEnrichment`. Passing a `propertyId` scores each publisher *for that brand* (category fit and brand-specific uplift); omitting it scores in general.

<Callout>
Any publisher flagged `needsEnrichment: true` has an **estimated** score until it's enriched. Treat those as directional.
</Callout>

For the full methodology, see [AI Placement Value Score](/docs/methodology/ai-placement-value-score).

## `score_placement_quality` (PQS)

Scores a **prospective** placement 0–100 — how much citation value a placement would earn, given its type, position, editorial control, and link attribute. Pass a `publisherDomain` (and optionally `propertyId`) to also compute that publisher's AIPVS and the **combined total placement value** (AIPVS × PQS ÷ 100).

| Parameter | Type | Notes |
|---|---|---|
| `placementType` | enum | Where/how the brand appears on the page (see below). |
| `editorialControl` | enum | How much control the brand has over the content. |
| `linkAttribute` | enum | The link's `rel` attribute. |
| `assumedPosition` | number (optional) | Position on the page, `0` = top … `1` = bottom (default `0.15`). |
| `publisherDomain` | string (optional) | Also compute this publisher's AIPVS and the total placement value. |
| `propertyId` | string (optional) | Brand context for the AIPVS layer. From `list_properties`. |

### Accepted values

| `placementType` | `editorialControl` | `linkAttribute` |
|---|---|---|
| `dedicated_article` | `full_authorship` | `dofollow` |
| `roundup_top` | `collaborative` | `nofollow` |
| `roundup_top_third` | `quoted` | `sponsored` |
| `roundup_middle_third` | `mentioned` | `ugc` |
| `roundup_bottom_third` | `unpredictable` | `no_link` |
| `passing_mention` | | |
| `sidebar_widget_biobox` | | |
| `quote_only` | | |

The result returns the `pqs` (with per-sub-score detail and the effective weights) and, when a `publisherDomain` is given, the publisher's `aipvs` and the combined `totalPlacementValue`. Without a `publisherDomain`, `aipvs` and `totalPlacementValue` are `null`.

For the full methodology, see [AI Placement Quality Score](/docs/methodology/ai-placement-quality-score).

## Bulk placement scoring for an organization

`score_placement_quality` scores a placement you are *considering*. To score coverage you have **already earned** — a list of real URLs — use `score_placement_urls`. It fetches each page, finds the brand mention, and returns a real PQS with placement type, link attribute, sentiment, and author.

This is the only **organization-scoped** surface on the connector. Every other account tool starts from a `propertyId`; these start from an `organizationId` (from `list_my_organizations`), because the brands you score against don't have to be properties on your account at all.

<Callout>
These three tools are the connector's other **write** surface. `score_placement_urls` creates a coverage group, brand records, and placements in your organization, and triggers page fetches. It is free today; a monthly cap can be configured per deployment, and a call that would exceed it is refused. Nothing is ever published.
</Callout>

### `score_placement_urls`

| Parameter | Type | Notes |
|---|---|---|
| `organizationId` | string | From `list_my_organizations`. You must be a member of it. |
| `rows` | `{ url, brand }[]` | 1–200 pairs. The same URL may appear twice against two different brands — that's two placements, not a duplicate. |
| `name` | string (optional) | A name for the coverage group, shown in the app. Defaults to a timestamped "MCP import". |
| `brandOverrides` | record (optional) | Per-brand `{ companyName?, aliases? }`, keyed by the same brand string used in `rows`. Only applied to brands this call creates. |

#### How a brand is resolved

The `brand` on each row is not a property id — it's whatever identifies the brand:

| You pass | Read as | What happens |
|---|---|---|
| A bare domain — `acme.com` | A **BRAND** | Matched against the organization's existing properties (clients, prospects, and brands scored earlier). A match is **reused**; no record is created. |
| A URL with a path — `https://acme.com/products/widget` | A **PRODUCT**, anchored on that page | Matched on that seed URL within the organization, then reused or created the same way. |
| Anything without a domain-shaped host | — | Reported in `invalidRows` with a reason. The rest of the batch still imports. |

A brand with no match becomes a hidden **scoring-only brand record**: a property row carrying the company name and the aliases the scorer needs to spot mentions, flagged so it never appears in the Clients grid, the Prospects list, or `list_properties`. It costs nothing and runs no reports. If you later add that domain as a real property, the hidden record is promoted rather than duplicated, and the placements you've already scored stay attached.

Aliases are derived automatically from the company name and domain. Pass `brandOverrides` when the user knows better — for instance when the site's title isn't the name the press uses.

<Callout>
The mention matcher is a case-insensitive substring search, so keep any alias you supply long and distinctive. A short one false-positives inside ordinary words ("Ford" inside "afford").
</Callout>

#### The poll loop

`score_placement_urls` returns immediately with a `groupId`, `added`, `duplicates`, `invalid`, `invalidRows`, and `brands: { created, reused }` so you can tell the user exactly which brand records were created. Scoring itself runs in the background — expect roughly 30 seconds per placement, five in parallel.

Poll `get_coverage_group(groupId)` every ~5 seconds until `done` is `true`:

| Parameter | Type | Notes |
|---|---|---|
| `groupId` | string | From `score_placement_urls` or `list_coverage_groups`. |
| `includeAipvs` | boolean (optional) | Add each publisher's AIPVS, scored in that item's own brand context (default `true`, up to 100 distinct brand/publisher pairs). |
| `limit` | number (optional) | Max items returned, newest first (default 200, max 500). `counts.items` is always the group's full size. |

The result carries `counts` (`items`, `pending`, `running`, `completed`, `failed`) and a `done` flag — `done` is true once nothing is pending or running, and it answers for the **whole** group, not just the page of items returned. An item whose placement hasn't been created yet counts as pending. Each item returns its `url`, `domain`, `title`, the `brand` it was scored against, its `placement` (status, `failureCode`, `pqsScore`, `pqsTier`, `pqsTierLabel`, placement type, link attribute, sentiment, author), and its `aipvs`.

<Callout>
AIPVS is network-bound, so pass `includeAipvs: false` while a batch is still running and read it once the placements have completed.
</Callout>

A placement that finishes with a `failureCode` of `no_brand_mention` usually means the brand's aliases are wrong for how that outlet writes the name — fix the aliases in the app and re-score.

`get_coverage_group` also reads a **property's** coverage group, where every item is scored against that property's own brand. Use `list_coverage_groups(organizationId)` to find groups you scored earlier; it returns the organization's cross-brand batches by default, and takes `scope: "property" | "all"` or a `propertyId` for per-client coverage groups.

## When to use which

- **"Is this outlet worth pitching?"** → `score_publisher_value` on the domain(s), ideally with your `propertyId` for brand context.
- **"How good would *this specific* placement be?"** → `score_placement_quality` with the placement's shape, plus `publisherDomain` to fold in the outlet's value and get a single total.
- **"Score this list of coverage we earned."** → `score_placement_urls`, then poll `get_coverage_group`. This is the one that reads the real page.

## Related

- [AI Placement Value Score methodology](/docs/methodology/ai-placement-value-score)
- [AI Placement Quality Score methodology](/docs/methodology/ai-placement-quality-score)
- [Placement scoring dashboard](/docs/dashboards/placement-scoring) — the same bulk scoring in the app, with the import wizard and CSV/PDF export
- [Coverage API](/docs/api/coverage-automation) — the REST equivalent of `score_placement_urls`, for automation platforms
- [Publisher Lookup dashboard](/docs/dashboards/publisher-lookup) · [Placements dashboard](/docs/dashboards/placements)
- [Prompts](/docs/mcp/prompts) — the `evaluate_publishers` prompt drives `score_publisher_value`
