# Citation Optimizer tools

The Citation Optimizer tools let a connected assistant drive the full **score → revise → re-score** loop: pick the searches to optimize for, find the right page, score it for AI citation readiness across several assistants at once, generate an improved draft, and re-score to measure the lift — repeating until the content is publish-ready. They also cover the step *before* the page exists: a keyword brief that says what to write.

This is the one place in the MCP server that **writes** to your account — but only in a contained way. It creates *your own* scoring runs, **draft** revisions and briefs so you can iterate. It never publishes anything, never edits your live pages, and never changes your existing reports or data. For the scoring model itself, see the [Citation Optimizer methodology](/docs/methodology/citation-optimizer).

## Two shapes of the loop

| | Audit (preferred) | Single run (legacy) |
|---|---|---|
| Scores | One page against a **query set**, on every assistant your plan covers | One page against **one query**, on ChatGPT |
| Start with | `score_citation_audit` | `score_citation_pipeline` |
| Poll | `get_citation_audit` | `get_pipeline_run` |
| Revise | `revise_citation_audit` | `revise_content` |

The audit is what the product is about. A page that appears in the results for several related searches is far more likely to be cited than one that wins a single query, and the four pipelines disagree often enough that optimizing against one of them in isolation can cost you score on the others. The audit runs them together and **harmonizes** the findings into one score and one list.

The single-run tools still work unchanged, and they are the shape the free plan gets.

<Callout>
Which assistants you can score against depends on two things: which pipelines are live (the tools report this in `scoreablePlatforms`, and it can change without a release) and which ones your plan covers. The free tier is ChatGPT only; paid plans cover ChatGPT, Google AI Overviews, Google AI Mode and Claude. Asking for one outside your plan is refused with the list you *can* use.
</Callout>

## What it costs

**Scoring costs no credits.** It is metered instead: each plan includes a number of **pages** a month, and a page counts **once** however many times you re-run it with different searches or different assistants. Re-scoring a rewrite never counts at all, so once you are in the loop on a page, the loop is free. Over the allowance, the tools refuse with the numbers (`used`, `cap`, `remaining`) so you can tell the user whether to wait for the reset or upgrade.

**Rewrites and briefs spend credits.** They draw on the plan's monthly rewrite allowance first, then credits. When neither covers it, the refusal carries the allowance and the balance so you can say exactly what is needed.

## Fire-and-poll

Scoring, revising and brief-writing all run as background jobs, so those tools are **fire-and-poll**: they enqueue work and return an id immediately, and you poll a companion tool until it's done.

| Kick off (returns an id) | Poll until done |
|---|---|
| `score_citation_audit` → `auditId` | `get_citation_audit(auditId)` until `completed` |
| `revise_citation_audit` → `revisionId` | `get_revision(revisionId)` until `completed` |
| `rescore_revision` → `auditId` or `runId` | `get_citation_audit` / `get_pipeline_run` |
| `generate_citation_outline` → `outlineId` | `get_citation_outline(outlineId)` until `completed` |
| `score_citation_pipeline` → `runId` | `get_pipeline_run(runId)` until `completed` / `stopped` |
| `revise_content` → `revisionId` | `get_revision(revisionId)` until `completed` |

## Step 1 — Pick what to optimize for

You need a **query set** and a **target** (a page, URL, or draft).

### `list_tracked_fanouts` — start here

Lists a property's **real tracked fan-out queries** — the queries the platform's pipeline actually generated (from the property's latest AI Visibility report), highest-impact first. Starting here beats inventing keywords: you optimize for queries the platform is observed to produce. Each fan-out carries an `isGap` flag (the brand doesn't rank for it yet).

| Parameter | Type | Notes |
|---|---|---|
| `propertyId` | string | From `list_properties`. |
| `platform` | `chatgpt` \| `claude` \| `google_aio` \| `google_aim` (optional) | Default `chatgpt`. A platform with no live pipeline returns tracked fan-outs with `scoreable: false`. |
| `limit` | number (optional) | Max fan-outs (default 25, max 100). |
| `includeSiteScoped` | boolean (optional) | Include `site:`-scoped fan-outs in the list. Default `false` — see below. |
| `includeCompetitorNamed` | boolean (optional) | Include fan-outs that **name a competitor** and not this brand. Default `false` — see below. |

Build the audit's query set from this list: one member as the `primary` (the search the page is really for) plus the related fan-outs. You can also include searches the user brings — that's fully supported, it just carries a different provenance label.

#### Site-scoped fan-outs are evidence, never targets

A site-scoped fan-out is one the model aimed at a single site — `site:example.com pricing` — instead of searching the open web. Nothing can rank for a query scoped to somebody's domain, so these are **evidence about which sites the model already trusts**, not opportunities:

- They are **excluded** from `list_tracked_fanouts` by default, and from SERP gap tracking.
- They are never counted in a platform's `count` in `availability`, so the "how much is there to work on" number can't drift. The separate `siteScopedCount` reports how many exist.
- A site-scoped search may be added to an audit's query set as a **fan-out**, where it is kept as evidence for the named-in-searches diagnostic and marked `scoreable: false`. As the **primary**, it is refused — every recommendation the audit produced would be impossible to act on.
- `score_citation_pipeline` rejects them outright.

Pass `includeSiteScoped: true` only when you want to *show* that evidence — for example, to answer "which sites does ChatGPT consult directly when it researches us?". Each included row carries `siteScoped: true`, the `siteScopeDomain` it pointed at, a `siteScopeClass` of `own` / `competitor` / `third_party`, and a `siteScopeNote`.

#### Competitor-named fan-outs are informational too

Some fan-outs consult a specific brand without the operator — they just name it (`CompetitorX pricing`). A page about this brand's product cannot answer a question about a competitor's, so these are handled exactly like scoped rows:

- **Excluded** from `list_tracked_fanouts` by default, and from SERP gap tracking.
- Never counted in a platform's `count` in `availability`; the separate `competitorNamedCount` reports how many exist.
- Included rows carry `competitorNamed: true`, the `namedCompetitors` they matched, and a `competitorNamedNote`. Don't score them.
- If one reaches an audit's query set anyway (a search the user typed, say), it is treated like a site-scoped fan-out: kept as evidence and marked `scoreable: false`. As the **primary**, it is refused.

**Comparison fan-outs are not affected.** A query naming this brand *and* a competitor (`Us vs CompetitorX`) is a real, rankable search — it stays in the default list and is fully scoreable.

"Competitor" means two things here. The first is the property's **tracked** competitors, read at query time, so removing one immediately returns their queries to the rankable set. The second is other brands the assistants named in the property's latest AI Visibility report that a model has **judged to be competitors**: companies that sell a substitute for what this brand sells, to the same buyers. Platforms, publishers, data sources, partners and tools the answers name are not competitors, so a search like `how to get cited by ChatGPT` stays scoreable for a brand whose work is about ChatGPT. Each name is judged once per property and the verdict is reused.

The test is deliberately a whole-word name match, so a competitor named after a common word can over-exclude; for a tracked competitor that is reversible by untracking it.

### Find the target page

| Tool | Use it to |
|---|---|
| `match_pages_for_fanout` | Rank a property's pages by **content similarity** to a query — pick the closest existing page so two of your pages don't compete for the same citation. |
| `list_property_pages` | List / search a property's pages by path or title, to resolve a page the user *names* ("optimize my pricing page") into a `propertyPageId`. |
| `list_placements` | List a property's PR placements (with PQS, author byline, publish date and a `hasContent` flag) to find one to optimize. |
| `get_placement` | Read a placement's content, author byline (`authorName`, `authorUrl`) and `publishedAt` so you can score it as a draft. |

## Step 2 — Score the audit

### `score_citation_audit`

Scores one page, URL, or pasted draft against a **query set** on several assistants at once. Runs in the background — returns an `auditId`; poll `get_citation_audit`. **Provide exactly one** of `propertyPageId`, `url`, or `draftMarkdown`.

| Parameter | Type | Notes |
|---|---|---|
| `propertyId` | string | The property id. |
| `queries` | array | The query set. Each member is `{ query, role?, provenance?, groundingSearchId? }`. **Exactly one** member must have `role: "primary"`; the rest default to `fanout`. Two to six members is the useful range. |
| `platforms` | array (optional) | Assistants to score against. Defaults to every one your plan covers. |
| `pageType` | enum (optional) | `product`, `homepage`, `informational`, `press_release`, `general`, `unknown` (default `informational`). Drives the rewrite template. |
| `propertyPageId` / `url` / `draftMarkdown` | string (optional) | The target. Exactly one. |
| `metaTitle` / `metaDescription` | string (optional) | Optional page meta. |
| `outlineId` | string (optional) | The brief this draft was written from. Links the score back to the brief. |

#### Searches that are kept but never scored

Two kinds of search can sit in the query set without being scored, because no page of this brand's can win them: a `site:`-scoped search (`site:example.com pricing`) and a search that names a competitor and not this brand (`CompetitorX pricing`). Both are **kept as evidence** and marked `scoreable: false`. Either one as the **primary** is refused. A comparison naming both brands (`Us vs CompetitorX`) is an ordinary scoreable search.

#### Provenance is not book-keeping

Every query carries where it came from, and the label survives into the score, the rewrite, and every screen the user sees:

| `provenance` | Means |
|---|---|
| `tracked` | This brand's assistants were **observed** running this search. Pass the `groundingSearchId` from `list_tracked_fanouts` with it. |
| `freetext` | The user typed it. Theirs, not ours. |
| `generated_unverified` | You or a tool suggested it. **Nobody has seen an assistant run it for this brand.** |

Use `tracked` only for a search that actually came out of `list_tracked_fanouts`. By the time a brief or a results screen reaches a person, there is no way left to tell a suggestion from an observation, so the label is the only thing keeping the two apart.

### `get_citation_audit`

The poll target, and the whole picture: status, the combined score with the range it is measured within, each assistant's own score and checks, the merged recommendation list, and the **readiness verdict**.

| Parameter | Type | Notes |
|---|---|---|
| `auditId` | string | The audit id. |

Alongside the verdict it returns the ids you need to act on it:

| Field | Means |
|---|---|
| `parentAuditId` | On a rescore, the audit it was compared against. `null` on an original audit. |
| `rescoredRevisionId` | On a rescore, the revision it measured. `null` on an original audit. |
| `bestVersion` | `{ auditId, revisionId, howToFetch }`: the version to present and the exact call that returns its text. |

`bestVersion` is how "present the previous version" becomes something you can do. A rescore measured a revision, and that revision was generated **from** the previous version, so it holds both texts:

| The verdict keeps | `bestVersion.howToFetch` |
|---|---|
| The previous version | `get_revision('<rescoredRevisionId>').originalMarkdown` |
| This rescore | `get_revision('<rescoredRevisionId>').revisedMarkdown` |
| An original audit (no parent) | The page, URL or draft that was submitted, unchanged |

**The range is not decoration.** Citation decisions agree only about 85% of the time between identical re-runs, so the score comes with a band (at least ±8 points) and a move inside that band has not been shown to be a move. Never report a few points as an improvement.

**Recommendations come grouped by kind:**

| Kind | What it means | What to do |
|---|---|---|
| `consensus` | Several assistants asked for the same thing. | Do these first — they pay off everywhere. |
| `platform_specific` | One assistant asked alone. | Worth doing, weighted by how much that assistant matters to this brand. |
| `conflict` | Two asks that cannot both be satisfied in the same passage. | **Follow the `resolution`**, which allocates different parts of the page to each ask. Averaging two incompatible instructions produces a rewrite that satisfies neither. |

### STOP rules

`readiness.recommendation` is one of:

| Verdict | What it means | What to do |
|---|---|---|
| `revise_again` | There's meaningful headroom. | Call `revise_citation_audit`. |
| `publish_ready` | Every assistant passes its selection checks and the combined score clears the bar. | **Stop.** |
| `plateaued` | The last pass didn't move the score beyond its range, up or down. | **Stop**, and present `bestVersion`. |
| `regressed` | The rewrite made at least one assistant **meaningfully worse** (named in `readiness.regressedPlatforms`), or lowered the combined score by more than its range. | **Stop**, and present the **previous** version: `bestVersion` names it and says how to fetch it. |
| `incomplete` | An assistant could not be scored this time (named in `readiness.missingPlatforms`), so the verdict covers only the others. | **Stop.** Re-run the scoring before deciding, and don't revise on this verdict. |
| `pending` | Nothing has finished scoring yet, or an assistant is still scoring. | Keep polling. |

Whenever `readiness.assistantInstruction` is present, it is an explicit instruction to end the loop. Follow it. `regressed` is the one that is easy to miss: the rewrite objective is "raise the combined score **without making any assistant worse**", so a version that traded Claude for the average has not met it, and the combined number alone will not tell you.

**Which version to keep** is decided like for like: both versions are compared with the same weights, over only the assistants that produced a score in both. A version has to score strictly higher to replace the previous one, so a tie keeps the previous version.

**A failed assistant is not a zero.** One assistant failing does not fail the audit: the combined score is computed over the assistants that were measured. But a decision made on three assistants when four were asked for is a different decision, so the verdict says `incomplete` and names the missing one. There is no way to re-run one assistant inside an audit; re-run the audit with `score_citation_audit` (the same content and query set). `rescore_revision` hands back the existing rescore rather than running it again.

A role check that was **skipped** (Claude's search coverage check cannot rank a draft that isn't published, for example) counts as neither a pass nor a fail, so a draft can still reach `publish_ready`.

## Step 3 — Revise

### `revise_citation_audit`

Generates a revised draft grounded in **every** assistant's findings at once, with the conflicts resolved by allocation rather than by averaging. Background job — returns a `revisionId`; poll `get_revision`.

| Parameter | Type | Notes |
|---|---|---|
| `auditId` | string | The scored audit to revise. |
| `profile` | `harmonized` \| `chatgpt` \| `claude` \| `google_aio` \| `google_aim` (optional) | Default `harmonized`, which is almost always right. Naming a single platform optimizes for that one alone and usually costs score on the others. |
| `force` | boolean (optional) | Revise even though the verdict is not `revise_again`. Only when the user explicitly asked for another pass **after** being told the verdict. |

**Gated on the verdict.** Unless the audit's readiness is `revise_again`, the call is refused and nothing is spent. The refusal states the verdict and its instruction; for `regressed` it names the previous version's audit to revise instead (`reviseInsteadAuditId`). Spending credits on a version the verdict said to stop on is exactly what this prevents.

**Spends credits.** Refused when the organization has used its monthly rewrite allowance and has no credits left to cover the next one; the refusal carries the allowance and balance.

### `get_revision`

The poll target: when completed, returns the revised markdown, revised meta, JSON-LD, and a **change log** tracing each edit back to the recommendation that motivated it. `auditId` tells you which loop the revision belongs to.

| Parameter | Type | Notes |
|---|---|---|
| `revisionId` | string | The revision id. |

Two fields matter for what you show the user:

- **`originalMarkdown`** is the exact text the rewrite was generated from. When `get_citation_audit` keeps the previous version, `bestVersion.howToFetch` points here, so this is the text to present.
- **`unverifiedClaims`** lists facts the rewrite added that the source didn't contain (figures, names, verify markers), each with `kind`, `text` and `where` (`body`, `meta` or `json_ld`). Present them as **"check these before publishing"**, and never present the draft as ready to publish while any remain.

## Step 4 — Re-score (close the loop)

### `rescore_revision`

Re-scores a completed revision to measure the improvement, and matches whatever it came from:

- a revision from an **audit** re-scores as a **child audit** — the same query set, the same pinned competitor pool, every assistant again — and returns an `auditId`;
- a revision from a **single run** returns a `runId`.

Read the `kind` field (`"audit"` or `"run"`) to know which poll target to use.

| Parameter | Type | Notes |
|---|---|---|
| `revisionId` | string | The completed revision to re-score. |

Pinning the competitor pool is what makes the before/after comparison mean anything: the rewrite is measured against the identical competitors rather than against whatever happens to rank today. Re-scoring is always free and never counts against the monthly page allowance.

## Re-weighting (free)

### `reweight_citation_audit`

Changes how much each assistant counts toward the combined score, and returns the recomputed score and verdict. It recomputes from results already collected, so it costs nothing, uses no page from the allowance, and is instant.

| Parameter | Type | Notes |
|---|---|---|
| `auditId` | string | The audit id. |
| `weights` | object | Platform key → weight, e.g. `{ chatgpt: 40, google_aio: 30, google_aim: 20, claude: 10 }`. Any non-negative numbers; they are normalized. |

Use it when the user tells you where their audience actually is ("most of our buyers still start on Google"). Naming an assistant the audit did not run is refused rather than quietly ignored. The new mix is saved on the audit — so a shared link shows the same score and the same verdict the sender saw — and becomes the property's default for its next audit.

## Before the page exists — keyword briefs

### `generate_citation_outline`

Writes a section-by-section brief for a page that hasn't been written yet, from a single keyword. Scoring answers "why was this page not cited"; the brief answers "what should the page say in the first place".

It resolves the searches to write for (this brand's tracked fan-outs first, then suggested ones **clearly labeled as suggestions**), reads what the pages that already rank cover and what none of them answers, and fills the page-type template into a brief: per-section target searches, word budgets, must-include terms, and the rules that decide whether a passage can be quoted.

| Parameter | Type | Notes |
|---|---|---|
| `propertyId` | string | The property id. |
| `keyword` | string | The search this page should be found for. A `site:`-scoped keyword is refused. |
| `pageType` | `homepage` \| `product` \| `informational` \| `press_release` | Each has its own template, so this is not cosmetic. There is no fallback — `general` and `unknown` are not brief page types. |
| `platforms` | array (optional) | Assistants to write for. Defaults to every live pipeline. |

**Spends credits**, on the same allowance as a rewrite.

### `get_citation_outline`

The poll target: the searches the brief was built for (each labeled with where it came from), the coverage gaps, and the brief itself — sections, FAQ, meta, and the opportunity nobody has covered.

When you report a brief back, **keep the provenance labels**. A search marked `generated_unverified` is one we suggested; presenting it as a search this brand's assistants were observed running is the single mistake this flow exists to prevent.

Then write the draft and score it with `score_citation_audit`, passing `outlineId` — it scores against the **same** searches the brief was built from, so the two screens tell one story.

## The loop end-to-end

<Steps>

<Step>
**Choose the searches.** `list_tracked_fanouts` — one primary plus its fan-outs (or searches the user brings, labeled `freetext`).
</Step>

<Step>
**Find the target.** `match_pages_for_fanout` / `list_property_pages` / `list_placements` + `get_placement`. Or, for a page that doesn't exist yet, `generate_citation_outline` → `get_citation_outline` and write the draft first.
</Step>

<Step>
**Score.** `score_citation_audit` → poll `get_citation_audit` until it completes.
</Step>

<Step>
**Check readiness.** If `revise_again`, continue. If `publish_ready`, `plateaued`, `regressed` or `incomplete`, stop, follow `assistantInstruction`, and present `bestVersion`.
</Step>

<Step>
**Revise.** `revise_citation_audit` → poll `get_revision`.
</Step>

<Step>
**Re-score.** `rescore_revision` → poll `get_citation_audit`. Loop back to *Check readiness*.
</Step>

</Steps>

## Related

- [Citation Optimizer methodology](/docs/methodology/citation-optimizer) — how the checks and the readiness verdict work
- [Citation Optimizer dashboard](/docs/dashboards/citation-optimizer) — the same loop in the app
- [Scoring tools](/docs/mcp/scoring) — AIPVS / PQS scoring for publishers and placements
