# Self-serve billing, limits and receipts: API and MCP

For agents and scripts that use Linkio with an account API key (`acc_…`). Managed accounts are not billed this way.
Every number on this page is checked against the code on every build; the auth headers, scopes and 402 bodies
are in [API for account callers](/docs/api#api-for-accounts), the routes a key can call in [HTTP routes](/docs/api/routes).

`GET /api/pricing/self-serve` (public, no key needed) returns the numbers every page states: Linkio's
per-job fees (`fees`, the meter's own prices), `platformMarkupPercent` on provider cost when a job runs on
Linkio keys, and `publisher` (median / quartile listed guest-post prices, refreshed hourly). An agent that
quotes a price to its user should read it from here, never from page copy.

Creating a key (`POST /api/account/api-keys`, or the API keys page) needs only an active account; there
are no plans or subscriptions to hold first. Money is checked per job: a paid call is refused with a 402
until the account has prepaid balance, the free start (its OpenAI and DataForSEO keys healthy) or a card on file.

## How you are billed
- Every job pays provider cost on Linkio's keys + 20% margin + a compute fee for its job type (whole cents). Prices live in a settings row and are read with no deploy.
- Work on your own provider keys (OpenAI, DataForSEO, Hunter, MillionVerifier, QuickEnrich, Instantly) pays the compute fee only, for every feature the key passed the capability probe for (`features` on the key; the list is under Own keys).
- Every API or MCP write call (create, update, run) costs **1c per API or MCP write call**, counted per UTC day and charged once a day as one ledger line under the feature `requests`. Reads are free. An MCP tool call that writes is counted once, not again for the HTTP call it makes.
- A failed job pays no fee. Work you start is charged to your account, whatever your plan; work Linkio's team starts for you is not.
- Prepaid credits are used first, then the card on file (credit line $10, then $100 after the first successful charge; charged at thresholds and at month end). After 2 successful charges, the most you can set as your monthly budget rises to $1,000. A new account with its OpenAI and DataForSEO keys live gets a $10 free start for 90 days.
- A step re-run after a reset is a new job and is charged again; a retry inside the same run is charged once.

## Limits
- Monthly budget (hard or warn), per-feature caps, per-key daily and monthly caps, approvals for one-off overruns, alerts at 50/80/100% of the budget.
- A refused start answers `402` with a machine-readable body. One `code` field names the reason: budget (`budget_exceeded`, `budget_paused`), approval (`needs_approval`: the owner, or a key with `billing:write` through `usage_approvals_decide`, decides; then repeat the call, optionally with the header `X-Linkio-Approval`), card (`card_required`, `payment_failed`, `credit_line`). Each body carries `estimateCents` and the route or page that lifts the block; the three shapes are quoted in [API for account callers](/docs/api#api-for-accounts).
- Routes: `GET/PUT /api/account/usage-limits`, `POST /api/account/usage-limits/increase-requests`, `/approvals`, `/features`. MCP: `usage_limits_get`, `usage_limits_update`, `usage_limit_increase_request`, `usage_limit_increase_withdraw`, `usage_approvals_list`, `usage_approvals_decide`, `usage_feature_cap_set`, `usage_api_key_caps_set`.
- Reads are free but capped: 5000 reads per key per UTC day over both channels, since an MCP tool call reaches the API with your key. Over the cap a read answers `429` with `code` `DAILY_READ_CAP`, `cap`, `used`, `resetsAt` and a `Retry-After` header. Writes are priced instead and not counted here.
- The MCP server rate-limits each key: 60 calls per minute with a burst of 20 (token bucket). Every answer carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-RatePerMin`; over the limit it answers `429` with `code` `RATE_LIMIT_EXCEEDED` and a `Retry-After` header. The HTTP API has no rate limit of its own beyond the read cap.

## Receipt
After any paid call you can read where you stand:
- `GET /api/account/usage/receipt` (needs `billing:read`): `accountId`, `period`, `asOf`, `spentCents`, `heldCents`, `budgetCents`, `budgetRemainingCents`, `resetsAt`, `prepaidCents`, `unbilledCents`, `cardStatus`, `cardHeadroomCents`, `canStartPaidWork`, `feesCents`.
- **MCP:** every write tool result carries `usageReceipt` with the same fields.
- **API:** every 2xx answer of a route marked "receipt" in [HTTP routes](/docs/api/routes) carries the same receipt (without `accountId` and `asOf`) in the header `X-Linkio-Usage-Receipt` (JSON) plus `X-Linkio-Usage-Receipt-Url`. The header appears when the key holds `billing:read`; it is read at most once every 15 seconds per account. A 4xx or 5xx answer carries no receipt.
- `GET /api/account/usage` gives the full report by day, feature and key.

## Own keys

Features a saved key can serve once a check grants it, by provider (the key panel has one card per provider and lights the features of a checked key):
OpenAI: Pre-screen and Find Sites discovery (`pre-screen`), Article generation (`article`), Topic generation (`topic-generation`), Market context (`market-context`), Anchor optimization (`anchor-optimization`), AI Mentions (`ai-mentions`), Target page AI (`target-page-ai`), Exchange page picker (`exchange-osg`), Outreach polish (`outreach-polish`), Domain classification (`prospect-classify`), Page checks (topical fit) (`prospect-enhance`);
DataForSEO: Pre-screen and Find Sites discovery (`pre-screen`), Topic generation (`topic-generation`), AI Mentions (`ai-mentions`), Site metrics (`site-metrics`), SERP search (`serp`), Backlink data (`backlink-data`);
Hunter: Contact discovery (`contact-discovery`);
MillionVerifier: Email verification (`email-verify`);
Instantly: Email verification (`email-verify`) — the optional second opinion on catch-alls, on your key only;
QuickEnrich: Contact discovery (`contact-discovery`).
The exchange page picker is `opportunity_osg_run`; site metrics is `dataforseo_site_metrics`; backlink data is `target_page_fetch_backlinks` and `client_excluded_domains_auto_import`; SERP search is `campaign_serp_search_run` (the probe grants it on the same four checks as the other DataForSEO features; the SERP endpoint itself is not tested, so a login without the SERP product fails its first search and the run stops or falls back as the key rule below says).
Keyword extraction (`client_llm_extract_keywords` and friends) and the search-query brief run on the AI Mentions and target page AI grants. When the work ran on your key the answer says `onOwnKey: true` and the provider cost is not charged; the sync citation routes (`citation_check_trigger`, the one-off check) stay on Linkio keys because their per-check reserve is priced for them — use `client_llm_check_async` to run checks on your own keys.
- Add a key of any of the six providers in Settings → AI & Data Keys (one card each), or `POST /api/account/provider-keys` with `{ "provider": "openai", "apiKey": "…" }` or `{ "provider": "dataforseo", "login": "…", "password": "…" }` (MCP `provider_key_save`; the answer is `{ credential, replaced }` and never echoes the key); `GET /api/account/provider-keys` (`provider_keys_list`) lists them with their checked features; `GET …/{id}/probe-estimate` (`provider_key_probe_estimate`) is the probe's own price before you run it (one small paid call for OpenAI and DataForSEO; free for the four contact providers); probe with `POST /api/account/provider-keys/{id}/probe`. The probe reports `features` (the ids above) and, for OpenAI, builds your private copy of the Semantic SEO store.
- Hunter, MillionVerifier, QuickEnrich and Instantly keys: their cards on that page, or `POST /api/account/provider-keys` with `{ "provider": "hunter" | "millionverifier" | "quickenrich" | "instantly", "apiKey": "…" }` (MCP: `provider_key_save`). Their probe is one call: Hunter `GET /v2/account`, MillionVerifier `GET /api/v3/credits` and Instantly `GET /api/v2/workspaces/current` read the account and are free; QuickEnrich is one `dataset-search` page for a domain that has no results (QuickEnrich bills only a page that has results). It grants the feature when the provider accepts the key and, where the answer carries a credit count (Hunter, MillionVerifier), credits are left. An Instantly key that Instantly knows but whose workspace it does not show (HTTP 402 or 403) is saved active with the feature, and its first verification decides.
- Remove a key with `DELETE /api/account/provider-keys/{id}` (`provider_key_remove`): from the next start on, that provider's calls run on Linkio's key at provider cost plus the margin again; nothing already run is re-priced. A key of another account answers `404`.
- When your key fails mid-run (invalid, quota, rate limit): a step falls back to Linkio's key and credits when the balance allows, else it stops with a plain message that says where to fix the key: the key page (Settings → AI & Data Keys); a prospecting message also names `provider_key_save` and `provider_key_probe`. Fix the key and resume.
- A transient provider error is retried 3 times, waiting 30, 120, 300 seconds between attempts; if the step still fails, the article pauses, you get an email with a Resume link, and one automatic resume runs an hour later.

## Price before the click

`GET /api/account/usage/estimate?feature=<article|topic_generation|target_page_ai|ai_find_sites|website_search|site_metrics|serp_search|prospect_contact_discovery|prospect_email_verify|outreach_polish|prospect_classify|prospect_enhance>&count=N`
(MCP: `usage_estimate`) answers what the meter would hold if you started `count` units now:
`{ feature, count, estimateCents, perUnitCents, onOwnKey }` (the two prospect features add `ownKeys`, per provider). It calls the same functions the start
routes call before they reserve, so the number is the hold. Read only. Pre-screen keeps its own
per-domain estimate (`POST .../prescreen-estimate`) and AI Mentions its `client_llm_check_cost_estimate`.

In the app the same line sits under every paid start button ("This will hold about $X; you have
$Y prepaid · card on file"); on an account that cannot fund the job it says so and links to add
money, a card or your own keys.

Every paid prospecting start asks before it holds: without `confirm: true` the answer is `400` `CONFIRM_REQUIRED`
carrying the quote twice — `error` (the API wording) and `confirmMessage` (the sentence a person is asked, ending
"Start?") — and nothing is held. The same body with `confirm: true` starts; `dryRun: true` answers the quote alone.

## What you paid for

`GET /api/account/usage/jobs?period=YYYY-MM&limit=N` (MCP: `usage_jobs`) lists the month's
charges one per row, newest first: `{ id, at, feature, label, keySource: 'yours' | 'linkio' | 'unknown',
providerCostCents, feeCents, chargedCents, adjustment, apiKeyId, apiKeyName, ref }`. `keySource: 'yours'` means
your own provider key ran the job: the provider billed you directly and Linkio charged only
`feeCents`. The rows are the ones `GET /api/account/usage` sums, so the totals match the report.
The billing page shows the same table under Usage.

## Website search price

A topic or filter search (`POST /api/internal/websites/search`, MCP `website_search`) costs **0.25c per
site returned after the first 100 sites of the UTC day**, rounded up to whole cents per search (a
20-site page costs at most 5c). Domain lookups are free. The answer carries
`billing: { sitesReturned, billableSites, freeUsed, chargedCents, shortfallCents, freeRemainingToday, perSiteCents }`
(`shortfallCents` is the part the balance could not cover);
`usage_jobs` lists each paid search ("Site search: …"); `GET /api/account/usage/estimate?feature=website_search&count=20`
(MCP `usage_estimate`) is the price before the click. A search the account cannot fund answers 402 like
any other paid start; the monthly budget and the per-feature cap (`prospect_data`) apply. The route is
self-serve only: a managed account's key answers `403` with `code` `ACCOUNT_TYPE_DENIED`.

## AI Find Sites price

A full AI Find Sites run (`ai_find_sites_start`, MCP `usage_estimate` feature `ai_find_sites`) on a self-serve
account pays a run fee of **5c per completed client loop**, whatever keys it runs on. Its discovery calls run
on your own OpenAI key when that key is live for pre-screen (then they cost nothing from Linkio; OpenAI bills
you directly), and otherwise on Linkio's OpenAI key at provider cost plus 20%; the pre-screen of each found
site runs on your own keys when both are live (then only the pre-screen fee applies) and on Linkio's keys
otherwise. If your key fails mid-run, the rest of the run continues on Linkio's key when the account has
credits (checked before each further search; the run result's `keyNotice` says so in the status API and MCP)
and stops otherwise. The estimate before the click carries all of it; the charge is exact and lands in
`usage_jobs`.

## Site metrics price

`GET /api/internal/dataforseo/site-metrics?domains=a.com,b.com` (MCP `dataforseo_site_metrics`) returns
domain rank, referring domains, spam score and organic traffic from DataForSEO for up to 200 domains
per call on an account key. It costs **15c per call plus 0.1c per domain**, or **1.5c per domain with the
organic lookup** (`includeOrganic`, on by default) — DataForSEO bills the link endpoints per call,
not per domain — rounded up to whole cents, charged after the call under `prospect_data`; the answer
carries `billing: { domains, requestCents, perDomainCents, chargedCents, shortfallCents, onOwnKey }`
and `usage_jobs` lists it ("Site metrics: N domains"). On an account whose own DataForSEO login is on
file for the `site-metrics` feature the calls run on that login and cost nothing.
`GET /api/account/usage/estimate?feature=site_metrics&count=50` is the price before the call.

## SERP search price

The campaign itself is free: `POST /api/prospectfinder/campaigns` with `campaignType: 'serp_prospecting'` and no
domains (MCP `prospect_campaign_create`; the brand's `clientId`, `clientName`, `clientDomain` come from
`client_search`) creates it and charges nothing; with `dryRun: true` it creates nothing and answers
`{ dryRun: true, estimatedCostCents: 0, wouldCreate: { name, campaignType, clientId } }`. The campaign needs the
`prospect-pipeline:*` scopes; everything inside it (the scan below, classification, types, page checks, contacts,
outreach, exports) needs `campaign:*`.

`POST /api/prospectfinder/campaigns/{id}/keyword-search/{searchId}/run` (MCP `campaign_serp_search_run`) scans
Google through DataForSEO for every query of a prepared search (`campaign_serp_search_create`), up to the
configured per-scan maximum (10,000 by default) queries per search, in the background. It is **held before the
scan starts and settled when it ends**: Linkio's current fee is **2.5c per completed query** (`serpSearchCents`),
the live value being in `GET /api/pricing/self-serve`, on your own DataForSEO login and on Linkio's alike; on Linkio's login DataForSEO's price is added at **0.2c per query plus 0.01c per result** (1.2c at 100
results) **with the 20% margin**, so a 100-query search at 100 results holds 394c. On an account whose own
DataForSEO login is on file for the `serp` feature the hold is the fee alone and the scan runs on that login.
The hold is a reservation (`operation_type` `serp_search`) under `prospect_data`; the settle is one ledger row
(`serp_search_fee`, "SERP search: N queries" in `usage_jobs`) for the fee of the completed queries plus the
cost with margin of the queries Linkio's login served. `dryRun: true` in the body (MCP `confirm: false`) answers
the quote and starts nothing; `GET /api/account/usage/estimate?feature=serp_search&count=N` is the same price
before the click; an account caller starts the scan with `confirm: true` in the body (MCP `confirm: true`),
else the answer is `400` `CONFIRM_REQUIRED` with the same quote and nothing is held. When your DataForSEO login
fails during the scan, the remaining queries continue on Linkio's login if the balance and the budget cover them
(the search then carries the notice in `error_message`), else the scan stops with a message and the completed
queries are kept and charged; a scan whose every query failed ends `failed` with the first error text.

## Contact discovery and email verification price

`POST /api/prospectfinder/campaigns` with `campaignType: 'contact_discovery'`, a `domains` list and
`settings.workflowName: 'workflow4-hunter-findymail-verification'` (the one recipe an account can run) finds and
verifies contacts for every domain with QuickEnrich and MillionVerifier, in the
background (with your own Hunter key on file, Hunter searches run on that key too, fee only — Linkio runs no Hunter
search of its own; a domain QuickEnrich has nothing for ends with no contact and pays no fee); `POST /api/prospectfinder/campaigns/{id}/verify-contacts` with `contactIds` verifies contacts a
campaign found. Both are **held before the run starts and settled when it ends**: Linkio's fee is **0.06c per domain
processed** (`prospectDomainCents`) and **0.2c per contact verified** (`emailVerifyCents`), the live values being
in `GET /api/pricing/self-serve`, plus the providers' price (QuickEnrich per page,
MillionVerifier per check) **with the 20% margin**. Own keys: a Hunter or QuickEnrich key
(`contact-discovery`) or a MillionVerifier key (`email-verify`) that is on file and passed its check
serves that provider's calls, and **on your own key only the fee applies for that provider**: the
quote leaves its price out (`ownKeys` in the quote names the provider; `onOwnKey` is true only when every
provider of the run is on your keys, and the hold is then the fee alone) and the settle records its cost as
`customerKeyCost` and does not charge it.
A provider you hold no checked key for runs on Linkio's key. The fee per domain or per contact applies whichever key served
the unit. When your key fails during a run (rejected, no credits left, rate limit), that provider continues on
Linkio's key if the balance and the budget cover every domain of the campaign at the price on Linkio's keys
(discovery) or the contacts not yet checked (verify). If they do not, each remaining domain of the campaign stops
with a message that names the key page, `provider_key_save` and `provider_key_probe`, or the verify run stops and keeps what it
checked. What your key already answered inside one step is kept: the QuickEnrich pages it returned are not asked
again on Linkio's key (only the pages after them are), and an Instantly check your key accepted is not sent again.
A catch-all answer stays risky (`catch_all`): Linkio has no Instantly account, so the address is not re-checked
and not retried on a later pass — unless your own Instantly key is on file and checked, in which case Instantly
re-checks it on your key for the fee alone, and if that key fails the catch-alls are kept, not re-checked on
Linkio's key.
A check the provider did not make (an error answer) leaves the contact
`verification_failed`, not `invalid`, and carries no fee. The hold is a reservation (`operation_type`
`prospect_contact_discovery` or `prospect_email_verify`) under `prospect_data`; the settle is one ledger row
(`prospect_contact_discovery_fee`, "Contact discovery: N domains", or `prospect_email_verify_fee`, "Email
verification: N contacts", in `usage_jobs`) for the fee of the units the run completed plus the cost with margin
of what the providers billed; a domain that carried no provider call pays nothing. `dryRun: true` in the body
answers the quote and starts nothing; `GET /api/account/usage/estimate?feature=prospect_contact_discovery&count=N`
(domains) and `?feature=prospect_email_verify&count=N` (contacts) are the same price before the click; an account
starts a contact-discovery campaign with `confirm: true` in the body, else the answer is `400` `CONFIRM_REQUIRED`
with the same quote and nothing is held. Both routes are open to every account, whatever its billing plan: the
account that starts the run is charged for it; any other discovery recipe answers `403` `RECIPE_NOT_AVAILABLE`.

Discovery from a campaign type's assigned domains (`campaign_contacts_discover`, `POST
/api/prospectfinder/campaigns/{id}/contacts` with `{ action: 'discover', campaignTypeId, mode: 'new' | 'empty' }`;
`new` = domains never tried, `empty` = domains tried before with nothing found) is the same contact-discovery
price on the one account recipe: `dryRun: true` answers the quote for exactly the domains the mode names, no
`confirm: true` answers `400` `CONFIRM_REQUIRED`, `402` when the hold cannot be funded, and a start creates a
child campaign of yours whose spend rolls into the parent's `account_spend_cents`. Deleting contacts
(`campaign_contacts_delete`, `DELETE …/contacts` with `contactIds`) is free.

Importing a contacts CSV into your own campaign (`campaign_contacts_import_csv`,
`POST /api/prospectfinder/campaigns/{id}/import-apollo-csv`) is free: it calls no provider, holds nothing and starts
nothing. Only rows whose company domain is one of the campaign's domains are stored, as unverified contacts; the
other rows are dropped and counted in the answer. One import takes at most 5,000 rows. The paid next steps are the
two above: verifying the imported contacts (`campaign_contacts_verify`, with the `contactIds` the import answers)
at the email-verification price, and a contact-discovery campaign for domains the file did not cover.

## Outreach polish price

The AI polish reviews the outreach drafts of a prospecting campaign round before launch (approve, edit, or mark
for a person). It runs OpenAI (on your own key when the key is checked for it, else Linkio's) and is **held before the polish starts and settled when it ends**:
Linkio's fee is **0.5c per draft reviewed** (`outreachPolishDraftCents`), the live value being in
`GET /api/pricing/self-serve`, plus OpenAI's price for the calls the run made **with the 20% margin**. A draft
that got a verdict carries the fee; a draft whose call failed carries the cost of that call and no fee.
With your own OpenAI key on file and checked for **Outreach polish** (the key's card lists it), the polish calls run on your key and only the fee is charged
(`onOwnKey: true` on the quote and the receipt; OpenAI bills you directly). If your key fails during a run (rejected, out of quota,
or it cannot use the model) and your credits cover the rest at the price on Linkio's key, the rest runs on Linkio's key at that
price — provider cost with margin plus the fee — and the run's notice says so; otherwise the run stops with the budget notice and what
already ran is charged. A run Linkio's team starts is never on your key.

Three starts are metered for an account signed in to the site, whatever its billing plan (these routes take a
sign-in, not an account API key):

- composing a round, `POST /api/prospectfinder/campaigns/{id}/outreach/generate-drafts`. Composing costs nothing;
  the quote is for the polish the compose starts when it finishes, on up to contacts x sequence steps drafts.
- re-polishing a round, `POST /api/prospectfinder/campaigns/{id}/outreach/rounds/{roundId}/polish`. With
  `force: true` every draft not yet sent is polished again and charged again.
- re-polishing one draft, `POST /api/prospectfinder/campaigns/{id}/outreach/drafts/{draftId}/re-polish`. Every
  call is a new paid polish of that draft.

Each answers the quote and starts nothing on `dryRun: true`; answers `400` `CONFIRM_REQUIRED` with the same quote
until the body carries `confirm: true`; and answers `402` when the hold is refused, with the round and its drafts
unchanged. A paid start while a polish of the same round is running answers `409` `POLISH_ALREADY_RUNNING`. The
hold is a reservation (`operation_type` `outreach_polish`) under `prospect_data`; the settle is one ledger row
(`outreach_polish_fee`, "Outreach polish: N drafts" in `usage_jobs`) for the fee of the drafts that got a verdict
plus the cost with margin of the OpenAI calls. The hold bounds the run: when the OpenAI cost of a polish reaches
the amount held, it starts no further draft (calls already running finish and are charged). The round is then not
marked polished and cannot launch; the same route without `force` polishes only the drafts that are left, with its
own quote. One paid run covers at most 1,500 drafts: a larger round or compose answers `400` `DRAFT_LIMIT` and is
done in parts, as smaller rounds. Cancelling a round while its polish runs stops the polish before its next draft,
and what it used is charged. A run that ends without its settle is settled within about an hour from the calls it
recorded. `GET /api/account/usage/estimate?feature=outreach_polish&count=N`
(drafts) is the same price before the click.

The paid polish is for every account that starts it: an account in managed mode composes, holds and pays per
draft exactly as above. A polish Linkio's team starts for you is not charged.

## Domain classification price

The three-layer classification of a prospecting campaign's domains (domain type, page type, relevance to your brand —
`campaign_classify_start`, `POST /api/prospectfinder/campaigns/{id}/classify`) runs gpt-4o-mini (on your own OpenAI key when the key is checked for it, else Linkio's) and is
**held before the run starts and settled when it ends**: Linkio's fee is **0.06c per domain classified**
(`prospectClassifyCents`), the live value being in `GET /api/pricing/self-serve`, plus OpenAI's price for the calls
the run made **with the 20% margin**. A domain that reached a relevance verdict carries the fee; a domain the model
omitted after the retry (`failed`) carries the cost of its calls and no fee.
With your own OpenAI key on file and checked for **Domain classification** (the key's card lists it), the classification calls run on your key and only the fee is charged
(`onOwnKey: true` on the quote and the receipt; OpenAI bills you directly). If your key fails during a run (rejected, out of quota,
or it cannot use the model) and your credits cover the rest at the price on Linkio's key, the rest runs on Linkio's key at that
price — provider cost with margin plus the fee — and the run's notice says so; otherwise the run stops with the budget notice and what
already ran is charged. A run Linkio's team starts is never on your key.

The quote counts the campaign's domains with passed metrics (or its distinct SERP domains when none were pulled yet)
at 0.035c of expected OpenAI cost per domain plus the fee, with margin; the domain-type and page-type layers are
cached across campaigns, so the real cost is often lower and the settle is exact. `dryRun: true` answers the quote
and starts nothing; without `confirm: true` the answer is `400` `CONFIRM_REQUIRED` with the same quote and nothing is
held; `402` when the hold cannot be funded, with the campaign untouched; `409` `CLASSIFY_ALREADY_RUNNING` while a
classification of the campaign is in flight. One paid run covers at most 20,000 domains: a larger campaign answers
`400` `DOMAIN_LIMIT`. The hold is a reservation (`operation_type` `prospect_classify`) under `prospect_data`; the
settle is one ledger row (`prospect_classify_fee`, "Domain classification: N domains" in `usage_jobs`). The hold
bounds the run: when the OpenAI cost reaches the amount held, no further batch is started and the run ends `partial`;
the domains left keep their pending status, and a second start classifies only the domains not yet classified, with
its own quote. A run that ends without its settle is settled within about an hour from the calls it recorded, at
their cost with margin and no fee. Your run does not start the Ahrefs metrics check that a staff-started
classification fires. Resetting a classification (`campaign_classify_reset`, `DELETE` on the same route) costs
nothing and is refused (`409`) while a classification is still running.
`GET /api/account/usage/estimate?feature=prospect_classify&count=N` (domains) is the same price before the click.

When a paid classification ends, the campaign's domains are assigned to Linkio's predefined campaign types for you
— the types are ranked on the campaign (the paid placement types first) and the assignment runs; nothing of this
is charged. `GET …/classify` (`campaign_classify_get`) then carries `assignments` (every ranked type with its live
count) and `autoAssign` (what the run recorded); `GET /api/prospectfinder/campaigns/{id}` (`prospect_campaign_get`)
carries `campaign.type_assignments`. `campaign_type_list` (`GET …/campaign-types`) shows the predefined types with
their match and assigned counts: a shared type's rules and description are Linkio's and are not in your answer, and
its `settings` carry `want` and `offer_type` only; a type of your own brand comes whole. `campaign_type_assign`
re-runs one type's assignment (free); the ranking order and the domain filters are Linkio's.

## Page checks (enhancements) price

The page checks of a campaign type (`campaign_enhancements_run`, `POST /api/prospectfinder/campaigns/{id}/enhancements`
with `action: "kickoff"`) are topical fit — an AI verdict per page, on your own OpenAI key when the key is checked for it, else Linkio's — and, when the type enables
it, content validation, which fetches the page through Scrappey on Linkio's key. A run is
**held before it starts and settled when it ends**: Linkio's fee is **0.3c per page checked**
(`prospectEnhanceUrlCents`), charged once per page per run for a page that got a topical-fit verdict or was
fetched, the live value being in `GET /api/pricing/self-serve`, plus the providers' price for the calls the run made
**with the 20% margin**: OpenAI's price for the topical-fit calls, and 0.1c for each Scrappey fetch that returned a
page (a fetch that returned no page costs nothing).
With your own OpenAI key on file and checked for **Page checks (topical fit)** (the key's card lists it), the topical-fit calls run on your key and only the fee is charged
(`onOwnKey: true` on the quote and the receipt; OpenAI bills you directly). If your key fails during a run (rejected, out of quota,
or it cannot use the model) and your credits cover the rest at the price on Linkio's key, the rest runs on Linkio's key at that
price — provider cost with margin plus the fee — and the run's notice says so; otherwise the run stops with the budget notice and what
already ran is charged. A run Linkio's team starts is never on your key. The Scrappey fetches of content validation stay on Linkio's key (no own-key slot), so the quote keeps their price.

The quote counts the pages the run would check at 0.01c of expected OpenAI cost per page when topical fit is on,
plus 0.1c for every page content validation could fetch (the pages it has no result for yet), plus the fee, with
margin. It is an upper bound — topical fit usually removes pages before the fetch — and the settle is exact.
`dryRun: true` answers the quote and starts nothing; without `confirm: true` the answer is `400` `CONFIRM_REQUIRED`
with the same quote and nothing is held; `402` when the hold cannot be funded, with nothing started; `409`
`ENHANCE_ALREADY_RUNNING` while a run of the same campaign type is in flight; `400` `NO_PAGES` when there is nothing
to check. One paid run covers at most 2,000 pages: a larger campaign type answers `400` `URL_LIMIT`. An account's run
always runs in the background, whatever `mode` the request names. The hold is a reservation (`operation_type`
`prospect_enhance`) under `prospect_data`; the settle is one ledger row (`prospect_enhance_fee`, "Page checks: N
pages" in `usage_jobs`). The hold bounds the run: when the providers' cost reaches the amount held, the run
stopped at the amount held; start again to check the remaining pages. Such a run shows `budget_stopped: true` with
that sentence in the run list; it is not a provider failure, and the pages it checked keep their verdicts. A second
start has its own quote: topical fit checks every page of the type again, content validation fetches only the pages
it has no result for. A run that ends without its settle is settled within about an hour from the calls it
recorded, at their cost with margin and no fee. Overriding a verdict (`campaign_enhancements_override`), the
preflight and the lists cost nothing. A run Linkio's team starts for you is not charged.
`GET /api/account/usage/estimate?feature=prospect_enhance&count=N` (pages) is the same price before the click.

The positioning text the checks judge against is yours, per campaign: `POST /api/prospectfinder/campaigns/{id}/enhancements/config`
with `{ typeId, positioning_intent }` (`campaign_enhancements_config_update`), free. Before a paid classification has
ranked the type it answers `409` `rankings_row_not_found` ("Run classification first — the type is ranked for you when
it finishes"); a body that also carries `enhancements` (the check rules) for a shared type answers `422`
`cannot_configure_shared_type` — the rules of Linkio's types are not yours to change, a type of your own brand takes
them. `campaign_enhancements_list` (`GET …/enhancements?list=1`, or `?typeId=`) reads the positioning text you set
and, for a shared type, no `rules`.

## Exports (free)

Six `GET` routes under `/api/prospectfinder/campaigns/{id}/export/` — `serp-results`, `classification`, `type-matches`,
`rejected-domains`, `outreach-drafts`, `outreach-template` (MCP `campaign_export_*`, `campaign:read`) — answer
`?format=csv` (the default, a file) or `?format=json` (`{ name, filename, headers, rowCount, truncated, csv }`), at
most 10,000 rows each (`truncated: true` past that). Read only; nothing is held or charged.
