# 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.

**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.

The test runs against the property's **current** competitor list at read time, so removing a competitor immediately returns their queries to the rankable set. It is deliberately a whole-word name match, so a competitor named after a common word can over-exclude; that is reversible by untracking the competitor.

### 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 and a `hasContent` flag) to find one to optimize. |
| `get_placement` | Read a placement's content 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. |

#### 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. |

**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 that finished passes its selection checks and the combined score clears the bar. | **Stop.** |
| `plateaued` | The last pass didn't move the score beyond its range. | **Stop**, and present the best version. |
| `regressed` | The rewrite made at least one assistant **meaningfully worse**, even though the combined score held up. | **Stop**, and present the **previous** version — `readiness.bestAuditId` names it. |
| `pending` | Nothing has finished scoring yet. | 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.

## 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`. Only call this when readiness is `revise_again`.

| Parameter | Type | Notes |
|---|---|---|
| `auditId` | string | The scored audit to revise. |
| `profile` | `harmonized` \| a platform key (optional) | Default `harmonized`, which is almost always right. Naming a single platform optimizes for that one alone and usually costs score on the others. |

**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.

## 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` or `regressed`, stop and follow `assistantInstruction`.
</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
