Cheap Seats

CLI, API and MCP

Every command, JSON route and MCP tool, and how access works.

CLI

cheapseats init [-name X] [-competitors a,b] [-suggest [-n 10]] [-dry-run] <url> [dir]
                                       a new config repo from the brand's website (free unless -suggest)
cheapseats serve [-addr 127.0.0.1:3000]   web UI, JSON API, MCP and the daily schedule
cheapseats sets                           list prompt sets
cheapseats run <set-id>                   run one set and wait for it (costs money)
cheapseats report <run-id> [-json]        rates, share of voice and top pages for a run
cheapseats fixes <run-id> [-rebuild]      the run's fix list as markdown
cheapseats themes <run-id> [-rebuild]     net sentiment and the run's themes; -rebuild runs the theme pass again (one grader call)
cheapseats suggest-prompts <set-id> [-n 10]  new buyer questions for a set, as TOML to paste (one analyst call; writes nothing)
cheapseats regrade [-since 28d] [-dry-run=false] <set-id|run-id>
                                       grade stored answers again with today's grader, brand and competitors;
                                       a dry run with its cost unless -dry-run=false
cheapseats review [-days 30] [-json] [-suggest] <set-id>
                                       the prompts themselves: flat, noisy, off-target, duplicate and branded ones, and gaps (free; -suggest: one analyst call)
cheapseats audit [-pages 10] [-info] [-json] [site...]   what AI crawlers can read on the brand's sites (free)
cheapseats check-citations [-n 25] [-json] <run-id>      which pages the run cited name the brand, and which competitors (free)
cheapseats crawl import [-host example.com] <logfile>... AI crawler fetches and AI referrals from access logs; - reads stdin, .gz is read as gzip
cheapseats crawl report [-days 30] [-json]               those counts per page, beside how the latest runs cite it
cheapseats backup <file>                  a consistent copy of the database, safe while serve runs
cheapseats check                          validate the config and every prompt set
cheapseats plan <before> [<after>]        what a config change does to trends, for its pull request
cheapseats migrate-config [dir]           give each prompt an id, in sets written before ids
cheapseats version                        this build's version and database schema

From a checkout, run any of them with go run ./cmd/cheapseats <command>. audit, check-citations and crawl call no model and cost nothing; see Free site checks. init is in Getting started, regrade in Re-grading stored answers, review in Reviewing your prompts.

  • init: -name sets the brand’s name instead of what the site calls itself; -competitors adds [[competitors]] entries, alternatives prompts and head-to-heads; -suggest adds -n suggested prompts to the starter sets with one analyst call; -dry-run prints what it found and every file it would write, and writes nothing. It writes to ./<brand>-cheapseats-config unless you name a directory.
  • regrade: -since takes 28d, 72h or all (default 28 days) and applies to a set; -dry-run is true by default.

MCP server

/api/mcp is an MCP server (Streamable HTTP), so Claude Code, Claude Desktop or any MCP client can list prompt sets, start runs and read the results without the web UI.

claude mcp add --transport http cheapseats https://<host>/api/mcp --header "Authorization: Bearer <token>"
toolwhat it does
get_startedcall it first: the brand and its sites, each set’s tier, last run and status, today’s spend against the budget, which answerers and the grader can run here, the latest audit’s errors and warnings, open and urgent fixes, attempts awaiting an outcome, data_state (no_runs, running, stale or ready) and next_steps, the tool calls worth making now with their arguments. Free
list_prompt_setsthe checked-in prompt sets with ids, descriptions and counts
get_prompt_setthe prompts in one set, and the competitor names the grader folds onto
start_runstart a run over a set (costs money; returns the id); takes set and dry_run. With dry_run: true it starts nothing and returns the plan: answers per answerer and track, answerers skipped, estimated cost from each answerer’s recent answers and their grading (priced by [pricing]; re-grades count toward grading, so it errs high), today’s remaining budget, and whether it would be refused
list_runsrecent runs with status and headline rates
get_runrates per answerer and track (and unbranded, without branded prompts), share of voice with named rate, positions and top 3, head to head, objections, sources by kind and host, the outreach list, the brand’s pages found but not cited, net sentiment with top praise and objections, the run’s themes, the fix list, insights, one line per prompt
get_resultone answer in full: text, grade, searches and citations
get_quick_winsthe ranked fixes, filterable by gap, set, site, status and priority, each with its Claude prompt and the work submitted for it; brief leaves out the prompts and limit caps the list
mark_fixrecord the commit or pull request submitted for a fix, as proposed or accepted
set_attempt_statemove submitted work on: accepted when it merges, withdrawn when it’s closed
list_attemptsthe work submitted for fixes, with each one’s outcome (moved, worse, mixed, no detectable change…) tested against noise
get_citationsevery URL linked in an answer across runs, filterable by URL, set and window
search_probeone query through the answerers’ web search tool, returning the exact result
get_citation_stabilityhow much the hosts a set’s answers cite move from run to run: Bray–Curtis similarity and Jaccard overlap of consecutive runs, the headline score from 3 transitions, and the newest transition’s new and gone hosts (ten of each); takes set, track (search by default, or recall) and last_days (default 30). Free
get_rankingsthe brand’s daily share-of-voice rank and rates per set, with rolling averages and 95% bands, and day-over-day change with its margin (within_noise when it could be chance); takes set, last_days and window (7, 14 or 28)
get_search_queriesthe searches behind a run’s search-track answers: top queries, names the assistants added, words added and dropped, and queries worth adding as prompts; takes run, pool and limit. Free
suggest_promptsdrafts new buyer questions for a set as TOML to paste, with repeats dropped; takes set and n (default 10, max 50). One analyst call; writes nothing
audit_sitesaudits the brand’s sites for AI crawlers and returns the errors and warnings, each with a fix; takes site, cached and info. Free
check_citationsfetches a run’s most-cited third-party pages and says which name the brand and which competitors; takes run, limit and cached. Free
get_crawlersAI crawler fetches and AI referrals per page from imported logs, beside how often runs cite each; takes last_days, flag and limit. User agent only, unverified
review_promptsa set’s prompt review: flat, noisy, off-target, duplicate, rewritten and branded prompts, and gaps; takes set, days (30, 60 or 90) and suggest. Free unless suggest, which is one analyst call for proposed edits
regradegrade a set’s, or one run’s, stored answers again with today’s grader, brand and competitors; takes target (a set or run id), since_days (default 28, 0 for all) and dry_run (true unless set to false). The dry run says how many answers and the estimated cost; the real one runs in the background within the daily budget. See Re-grading
share_runa read-only share link to a run’s report; takes run, or set for its newest finished run, days (default 30, max 365) and include_answers. The URL is shown only this once
cancel_runstop an in-progress run

The server also offers three prompts, each telling the model which tools to call:

promptwhat it asks forarguments
weekly_pulsea short weekly read-out: what moved, what’s open, what to do this weekset (default: every set)
competitor_radarwho is winning which prompts, and from which sourcesset, competitor (default: the leaders)
why_not_recommendedwhy the assistants don’t recommend the brand for one prompt, and what would change thatprompt (its text or number; required), set

JSON API

routewhat it returns
GET /api/statusthe brand, set count and today’s spend against the budget
GET /api/versionthe Cheap Seats version, config commit and database schema
GET /api/progressruns in progress: answers done, errors, in flight and waiting, per answerer, with time left
GET /api/livea version stamp that changes whenever a run does, and whether one is in progress
GET /api/sets, /api/sets/{id}the checked-in prompt sets
GET /api/sets/{id}/citation-stabilitythe set’s citation stability, every transition in full; takes track (search or recall) and lastDays (default 30)
GET /api/runsruns with headline rates; takes set and limit
POST /api/runsstarts a run over {"set": "<id>"} (costs money); with "dry_run": true it returns the plan and estimated cost and starts nothing
GET /api/runs/{id}one run in full: every answer with its grade and sources, fixes, insights
GET/POST /api/runs/{id}/fixesthe fix list; POST rebuilds it and the insights (one paid Claude call)
POST /api/runs/{id}/cancel, /retrystop a run, or finish an unfinished one
GET/POST /api/runs/{id}/sharesthe run’s share links; POST makes one from {"days"?, "include_answers"?} and returns it as url, once
DELETE /api/shares/{id}revokes a share link
GET /api/quick-winsthe ranked fixes across sets; takes gap, set, site (a url, a kind like docs, or elsewhere), status (open, proposed, accepted) and priority
POST /api/runs/{id}/fixes/{rank}/attemptsrecords work submitted for a fix: {"url", "state"?, "note"?}, state proposed (default) or accepted
POST /api/attempts/{id}moves an attempt on: {"state": "accepted"} or "withdrawn"
GET /api/attemptsthe work submitted for fixes, newest first, with outcomes; takes set
GET /api/rankingsdaily rank and rates per set, with rolling averages, bands and day-over-day margins; takes lastDays, set and window (7, 14 or 28)
GET /api/runs/{id}/searcheswhat the assistants searched in a run; pool=1 adds the set’s earlier same-method runs
GET/POST /api/auditthe latest site audit; POST runs one over the brand’s sites and waits for it (free)
GET/POST /api/runs/{id}/citation-checkthe run’s latest citation check; POST runs one over its n most-cited third-party pages (default 25) and waits for it (free)
GET /api/crawlersAI crawler and referral counts per page, from imported logs; takes days (default 30)
GET /api/citationscited URLs across runs; takes urlContains, lastDays and set
POST /api/search-probeone search, verbatim; takes {"query", "allowed_domains"?, "blocked_domains"?}
curl -H "Authorization: Bearer <token>" \
  "https://<host>/api/citations?urlContains=docs.example.com&lastDays=30"

A run’s JSON (/api/runs/{id}, and /runs/{id}/export.json) carries everything on the run page under Summary: voice with named_rate, share_of_voice, avg_position, named_first, top3 and position_weighted per product (and share, the old name for named_rate), unbranded, head_to_head, objections, source_classes, hosts with bucket, outreach and found_not_cited; net sentiment and themes are under opinion; each source page has a page_type, rolled up in page_types. Each grade carries its evidence, and a re-graded run lists its regrades: when, how many answers, the cost, and the method and grader it had before. Metric definitions defines each field. An attempt’s outcome has status, summary, and named and recommended as {d, margin, prompts}.

/api/citations counts each answer that linked a URL once, and returns runs_scanned and answers_scanned alongside the pages, so a URL missing from the list reads as uncited across that many answers.

In the web UI, /sets/{id}/review is a set’s prompt review (/api/sets/{id}/review its JSON), /sets/{id}/regrade shows a set’s re-grade dry run and starts one (Re-grading), and /share/{token} is a run’s share page.

GET /healthz answers ok without a token, for a container’s liveness probe. GET /share/{token} is the only other page open without a sign-in besides /login and static files, and serves only that link’s run.

Access

With CHEAPSEATS_TOKENS unset, serve only listens on 127.0.0.1 and everything is open. To listen anywhere else, set CHEAPSEATS_TOKENS (comma-separated, so one can be rotated at a time): every /api/* request, MCP included, then needs Authorization: Bearer <token>, and the web UI asks for a token once and keeps it in a cookie.

On exe.dev, Cheap Seats instead trusts the proxy’s signed-in email header (CHEAPSEATS_AUTH_HEADER, narrowed by CHEAPSEATS_ALLOWED_EMAILS). API and MCP clients get through the proxy with an exe.dev token scoped to the VM; see connecting Claude Code.

Behind a proxy on the same machine, set CHEAPSEATS_HOSTS to the host names it serves Cheap Seats as (comma-separated). MCP refuses a request that reaches Cheap Seats on loopback under any other name, against DNS rebinding. Set CHEAPSEATS_PUBLIC_URL to the address people reach Cheap Seats at, so share links point there.

Data and privacy

Prompts and answers are sent to the model providers on every run, and brand.about and brand.facts go to the grader and analyst, so keep anything private out of them. API keys are masked out of provider error messages before they’re logged or stored.