Skip to the content.

SEO automation: connect once, budget once, run unattended

The user gives credentials once, in Claude Code’s own configuration, and sets an autonomy level and budgets once. After that, pipelines run on a schedule and spend paid credits only inside those budgets. The only things that reach the user are business decisions, spend above budget and always-gated actions (${CLAUDE_PLUGIN_ROOT}/references/autonomy.md). The vendor details behind every number below, with their sources, are in ${CLAUDE_PLUGIN_ROOT}/references/paid-tools-automation.md. Read it before any paid call.

When to use / not

Inputs: decide, don’t ask

Decision How to decide it
Which vendors are connected Run claude mcp list and read the mcp__… tool names in the session. Never ask “do you have Semrush?”
Tool-name form per vendor Vendor plugin mcp__plugin_seo-suite-<vendor>_<server>__*, e.g. mcp__plugin_seo-suite-semrush_semrush__*. User-added mcp__<name>__*, e.g. mcp__semrush__*, mcp__semrush-key__*, mcp__dfs-mcp__* or mcp__analytics-mcp__*. claude.ai connector mcp__claude_ai_<Server>__*. If the user already added the same URL, the plugin copy is dropped as a duplicate (mcp)
Plan and remaining balance Free checks only: Ahrefs subscription-info (0 units), DataForSEO /v3/appendix/user_data (free), Semrush countapiunits (free, but only on the API-key route)
Site, repo, locale, competitors, money pages As in autonomy.md: from the working directory, <html lang>, the SERPs and GSC
Pipelines to schedule and their cadence The defaults in the table under B, cut to the vendors that are connected
Proposed budget numbers Each scheduled pipeline’s expected spend per run (B) × runs per month, plus 50% headroom
Where outputs go seo-work/<pipeline>/<YYYY-MM-DD>/, SEO-WORKLOG.md, and PRs at level ≥ 2. Research results go into the store, seo-research/
What a run refreshes research_store.py status: only its stale and missing datasets per locale, and the stale records in its gaps.<locale> (router rule 7)

Ask only for these, batched at the end of setup, each with a recommendation:

  1. Sign-ins and keys. The user completes them in /mcp or /plugin configure, never in chat.
  2. Budget numbers above 0. Spend is a business decision. Propose figures from the credit plan.
  3. An autonomy level above 1.
  4. One-time OKs for persistent changes: writing the permission rules to user settings, and loading a schedule (launchd job, Desktop task or cloud routine).

Workflow

Read the research store first (${CLAUDE_PLUGIN_ROOT}/references/research-store.md):

python3 "${CLAUDE_PLUGIN_ROOT}/scripts/research_store.py" status

A. One-time setup

  1. Detect what’s connected.
    • Run claude mcp list and list the vendor mcp__* tools in the session.
    • Write a table to seo-work/automation/setup.md: vendor, tool-name form, signed in yes/no, plan and balance from the free checks.
    • A server that needs re-auth shows as failed. Note it and continue.
  2. Install one connector plugin per vendor the user has. Each is optional, so nobody sees a failed server for a vendor they don’t pay for:
    • /plugin install seo-suite-semrush@seo-suite: server semrush, OAuth;
    • /plugin install seo-suite-ahrefs@seo-suite: server ahrefs, OAuth (Lite plan or higher);
    • /plugin install seo-suite-dataforseo@seo-suite: server dataforseo, login and password as plugin options.

    • The credit guard is part of the core seo-suite plugin, so it covers every vendor however it was added. Servers the user already added keep working under their own names, and the guard matches every name form.
    • GA4 is not bundled (step 3).
    • There is deliberately no GSC server: Google publishes no Search Console MCP (google-mcp-list).
  3. Credentials. The user enters them; Claude never sees or stores them.

    Vendor How the user signs in
    Semrush /mcp → semrush → Authenticate (OAuth 2.1). API key instead: skip the plugin. The user puts the key in an environment variable and runs, in their own terminal, claude mcp add --transport http semrush-key https://mcp.semrush.com/v2/mcp --header "Authorization: Apikey $SEMRUSH_API_KEY". The key never goes into chat. It also enables the free countapiunits balance check
    Ahrefs (Lite plan or higher) /mcp → ahrefs → Authenticate → log in, pick the workspace, Allow. Then in app.ahrefs.com → Account → API keys, lower the MCP key’s default 200,000-unit monthly cap to the budget, and switch off pay-as-you-go units under Limits & usage
    DataForSEO dataforseo_login and dataforseo_password in /plugin configure seo-suite-dataforseo@seo-suite. Input is masked and stored in the macOS Keychain (plugin-ref). In the DataForSEO dashboard (API Settings), set a daily spending limit; calls over it fail with error 40203
    PageSpeed Insights Optional psi_api_key in /plugin configure seo-suite@seo-suite. The core plugin’s SessionStart hook exports it as PSI_API_KEY for psi.py
    OpenAI, Perplexity, Anthropic (the AI-answer probe) openai_api_key (main engine), perplexity_api_key (cross-check) and the optional anthropic_api_key in /plugin configure seo-suite@seo-suite, masked and kept in the Keychain. SessionStart exports them as SEO_SUITE_OPENAI_API_KEY, SEO_SUITE_PERPLEXITY_API_KEY and SEO_SUITE_ANTHROPIC_API_KEY for ai_probe.py, so other tools in Bash keep their own keys. At each vendor, the user turns off auto-recharge or sets a monthly limit as the backstop
    DataForSEO for rank_snapshot.py run By default snapshots go through the connector: rank_snapshot.py plan, the connector’s api_request, then rank_snapshot.py parse. For one scripted call per batch instead, the user gives DATAFORSEO_LOGIN and DATAFORSEO_PASSWORD to the process that starts Claude, read from the Keychain at launch (for example security find-generic-password -s dataforseo -w in the launchd command), never written in a file
    GA4 (optional, not bundled) The user signs in with Application Default Credentials: gcloud auth application-default login --scopes https://www.googleapis.com/auth/analytics.readonly,https://www.googleapis.com/auth/cloud-platform --client-id-file=<client.json>, or --impersonate-service-account=<email>. Then they add Google’s official server: claude mcp add analytics-mcp --scope user -e "GOOGLE_APPLICATION_CREDENTIALS=<path gcloud printed>" -e "GOOGLE_PROJECT_ID=<id>" -- pipx run analytics-mcp. It is read-only unless started with --allow-write; never add that flag (ga4-mcp)
    Search Console No official MCP. Default: the user drops a monthly Performance export into seo-work/gsc/ for gsc_analyze.py. Option: a community GSC MCP the user audits and pins, authenticated as a service account added as a property user. That is the user’s trust decision
    Bing Webmaster The user generates the API key under Settings → API Access in BWT
    • Unattended runs need finished sign-ins. A claude -p run cannot perform MCP OAuth. Sign in first with /mcp or claude mcp login <name> (mcp-login).
    • If a key or password appears in chat: don’t use it and don’t write it anywhere. Tell the user to revoke it and re-enter it through /plugin configure.
  4. Autonomy and budgets. The user runs /plugin configure seo-suite@seo-suite and sets the options below. Hooks read them as CLAUDE_PLUGIN_OPTION_<KEY>.

    Option Default Suggested start (one site)
    autonomy_level (1–4) 1 1. Raise to 2 once the first PRs look right
    semrush_monthly_units 0 (paid calls denied) ≤ 20,000 of the 50,000 units Starter, Pro+, Pro and Guru include
    ahrefs_monthly_units 0 ≤ 20,000 (Lite includes 200,000)
    dataforseo_monthly_usd 0 10 (the scheduled pipelines below cost about $5–6 a month for one site)
    other_monthly_usd 0 12 once the AI-probe keys are set: a monthly run of 40 prompts × 2 engines × 3 runs is about $2.60 [estimate], under the per-run cap of $3. Otherwise 0 until another vendor is connected
    per_run_cap_pct 25 25: one run may spend a quarter of each vendor’s monthly budget (5,000 of 20,000 units; $2.50 of $10)
    ask_above_pct 5 5: one call estimated above 5% of the monthly budget is deferred to the end-of-run questions (1,000 of 20,000 units; $0.50 of $10)
    max_rows 50 50
    cache_ttl_days per data type (paid-tools-automation.md § 8) leave unset
    psi_api_key (sensitive) unset set it if PageSpeed runs hit the keyless quota
    openai_api_key, perplexity_api_key, anthropic_api_key (sensitive) unset the first two for B8; the third only when Claude answers matter
  5. Permission rules. Propose them, then apply them only after the user’s one-time OK.
    • A plugin cannot ship permission rules: a plugin’s settings.json honours only agent and subagentStatusLine (plugin-components). These rules therefore go into the user’s ~/.claude/settings.json.
    • Allow the free and read-only tools of the servers that are connected.
    • Leave paid tools unlisted. PreToolUse hooks run before every tool call (permissions).
      • The credit guard returns allow when a call fits the budget. A call above ask_above_pct (or a Semrush report with no known price) is deferred: denied, recorded in credit-guard/requests.jsonl, and raised with the end-of-run questions. The guard never asks mid-run.
      • It returns deny when the call is over budget or the per-run cap, when a cached copy exists, or for a Semrush report sent without a row limit.
      • For free tools it gives no opinion, so the allow rules below decide.
      • If the guard isn’t running, Claude Code prompts as usual, so a missing guard fails safe in interactive sessions.
    • Rules are checked deny, then ask, then allow, and the first match wins. A hook’s allow never overrides a deny or ask rule. mcp__ rules containing parentheses are skipped (permissions).
    • Starting point, using the vendor plugins’ names. Keep only the vendors that are connected. When /mcp shows a user-added copy instead, swap the prefix (mcp__semrush__, mcp__semrush-key__, mcp__ahrefs__, mcp__dfs-mcp__). The GA4 lines assume the server name analytics-mcp from step 3.
    {
      "permissions": {
        "allow": [
          "mcp__plugin_seo-suite-semrush_semrush__get_report_schema",
          "mcp__plugin_seo-suite-semrush_semrush__keyword_research",
          "mcp__plugin_seo-suite-semrush_semrush__organic_research",
          "mcp__plugin_seo-suite-semrush_semrush__competitors_research",
          "mcp__plugin_seo-suite-semrush_semrush__backlinks_research",
          "mcp__plugin_seo-suite-semrush_semrush__domain_overview",
          "mcp__plugin_seo-suite-semrush_semrush__projects",
          "mcp__plugin_seo-suite-semrush_semrush__site_audit",
          "mcp__plugin_seo-suite-semrush_semrush__position_tracking",
          "mcp__plugin_seo-suite-dataforseo_dataforseo__docs_index",
          "mcp__plugin_seo-suite-dataforseo_dataforseo__docs_list_sections",
          "mcp__plugin_seo-suite-dataforseo_dataforseo__docs_search",
          "mcp__plugin_seo-suite-ahrefs_ahrefs__subscription-info-*",
          "mcp__plugin_seo-suite-ahrefs_ahrefs__rank-tracker-*",
          "mcp__plugin_seo-suite-ahrefs_ahrefs__management-*",
          "mcp__plugin_seo-suite-ahrefs_ahrefs__public-*",
          "mcp__plugin_seo-suite-ahrefs_ahrefs__gsc-*",
          "mcp__analytics-mcp__*"
        ],
        "ask": [
          "mcp__plugin_seo-suite-ahrefs_ahrefs__gsc-anonymous-queries"
        ],
        "deny": [
          "mcp__analytics-mcp__create_property_annotation"
        ]
      }
    }
    
    • Not in the list on purpose: Semrush execute_report, DataForSEO api_request and every other Ahrefs tool. The guard decides those.
    • Semrush discovery and schema calls are allowed because they are expected to cost nothing, but that is [UNVERIFIED]. Step 6 settles it.
    • Ahrefs tool names follow <group>-<endpoint>, and only two names are confirmed. Before writing the Ahrefs lines, check them against the names /mcp shows.
    • Applying the rules: show the merged diff, wait for the OK, then edit the file. Merge into existing arrays and never replace them.
  6. Calibrate once, inside the budget. Read the Semrush balance, run one discovery call plus get_report_schema, and read the balance again. Record whether those calls cost units in setup.md. Without an API key, the user checks My Profile → Subscription info before and after.
  7. Choose the pipelines and their schedule (B and C). Write the plan to setup.md, then log the work.

B. Pipelines (the cheapest source per step)

Rules for every pipeline:

# Pipeline Default cadence Steps, cheapest source first Expected paid use per run
B1 Full audit (seo-audit) Quarterly, and on demand sitemap_check.py, robots_check.py, crawl.py (≤ 2,000 URLs, sections above that), psi.py on top templates, GSC export → triage.py. seo-audit’s audit profile (audit_profile.py, references/profiles.md) is full-free by default at every size. Paid crawlers follow the profile’s lists: Screaming Frog CLI is Main on paid-big if licensed (£199/yr, $0 per crawl) and Reserve elsewhere; DataForSEO On-Page is a Cross-check on paid-small and paid-big, within budgets $0. On-Page: $0.00015/page (5,000 pages ≈ $0.75; JS $0.0015/page). Semrush Site Audit reads 100 units each; never “snapshot info” (10,000)
B2 Weekly technical monitor Weekly, Tuesday (CrUX History refreshes Monday ~04:00 UTC) Crawl diff vs last week, CrUX origin + key templates, robots/sitemap changes, GSC URL Inspection only for changed URLs (≤ 2,000/day/site). On Ahrefs: site-audit/projects (free), issues with date_compared only when “new” > 0 $0. Ahrefs ≤ 50 units/week (interactive only, see C)
B3 Keyword research (seo-keywords) On demand, refresh quarterly Free expansion (keyword_ideas.py, GSC queries, PAA) → cluster and shortlist locally → paid volume and KD for the shortlist only → SERP check of the top 20 300 shortlisted terms: DataForSEO ≈ $0.11 (search_volume $0.06 + bulk KD $0.048), SERPs $0.012. Same 300: Semrush 3,000 units (phrase_these with the Kd column; no Kd surcharge is documented, not metered) or Ahrefs ≈ 6,300 units
B4 Competitor gap (seo-competitors) Monthly Competitors from the SERPs → per competitor, one gap call with a 100-row limit and a volume filter → diff locally against GSC DataForSEO domain_intersection $0.024/competitor. Ahrefs organic-keywords (4 fields, 100 rows) ≈ 2,200 units. Semrush domain_organic 100 lines = 1,000 units (not domain_domains, 8,000 per 100 lines)
B5 Backlink deltas (seo-links) Weekly Change detector first; pull new and lost rows only when it shows change, never the full profile DataForSEO ≈ $0.024 (summary) + 2 × $0.0276 ≈ $0.08/site. Ahrefs ≤ 600 units/100 rows (no traffic/refdomains fields). Semrush 40 (overview) + 4,000 per 100 delta lines
B6 Rank tracking (seo-analytics) Daily money tier, weekly mid tier Search Console positions for everything (free, 2–3 day lag): rank_snapshot.py gsc. Ahrefs Rank Tracker reads are free (updates weekly). Paid: rank_snapshot.py run per keyword set in the DataForSEO Standard queue, ~50 money keywords daily (depth 10) and ~450 weekly (depth 20), AI Overview references on the money tier. Then research_store.py ingest serps, rank_compare.py snapshots for alerts, and rank_compare.py gsc at +14 and +28 days for each logged change ≈ $0.03/day + $0.54/week ≈ $3.22/month per market and language (+$0.90 with AI Overview on the money tier). Semrush Position Tracking positions cost 100 units per keyword: avoid
B7 GSC-driven content refresh (seo-content) Monthly Page-level GSC last 3 months vs a year earlier → flag pages −20% clicks → classify (decay, lost position, SERP/AI Overview change) → SERP check of the flagged pages’ top queries only ≈ $0.01–0.02 for 10 pages
B8 AI-visibility tracking (seo-ai-search) Monthly run of the full frozen set; weekly only when the budget covers four runs Frozen prompt set → ai_probe.py run through the OpenAI (main) and Perplexity (cross-check) APIs, Anthropic when asked → ai_visibility.py --compare against the last run. GSC Generative AI report via UI export (no API). Never the consumer apps, and never DataForSEO’s LLM Scraper, which drives them 40 prompts × 2 engines × 3 runs = 240 probes ≈ $2.60 a run [estimate], from other_monthly_usd; Claude adds ≈ $1.40, so run it separately or raise the budget
B9 Multilingual blog QA (seo-blog-review) On each new or changed post (PR trigger), or weekly Fetch changed posts → content_score.py, page_audit.py, the blog-review checks per language. Paid only to confirm a target query’s SERP per locale $0 by default. SERP check $0.0006 per keyword per locale
B10 Blog batch (seo-blog-builder) Weekly Store status → refresh only stale or missing data (the site’s post list included, ingest posts) → next-topics → briefs and posts → the blog-review checks per language → ship per level (PR, merge, CMS publish) → IndexNow and sitemap submission → store status → indexing and ranking checks of earlier posts $0 by default. Paid only for stale SERP refreshes ($0.0006 per keyword per locale) and the volume shortlist (B3 prices)

Where results go, by autonomy level:

C. Scheduling unattended runs

  Local: macOS launchd (or cron) running claude -p Claude Desktop scheduled task (local) Cloud routine (/schedule)
Runs on Your machine, which must be awake Your machine, with the app open and the machine awake Anthropic cloud. Your machine can be off
Plugin, skills and credit guard Loaded from your install Your local config Not carried over: cloud sessions install no plugins (not even ones a repo’s settings enable). ~/.claude/skills and MCP servers added with claude mcp add at local or user scope stay on your machine (cloud-env)
Locally stored credentials (Keychain plugin secrets, MCP OAuth tokens) Yes. The login keychain must be unlocked (errors) Yes No. Use claude.ai connectors and the environment’s network secrets
Permissions You set them per run: --permission-mode dontAsk plus --allowedTools Permission mode chosen per task Starts in auto mode; ask rules still prompt and wait (routines)
Minimum interval Any 1 minute (set by Claude), 1 hour in the forms 1 hour

Recommendation:

Local launchd recipe:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
  <key>Label</key><string>com.seo-suite.example-com.weekly-monitor</string>
  <key>WorkingDirectory</key><string>/Users/you/sites/example.com</string>
  <key>ProgramArguments</key><array>
    <string>/bin/zsh</string><string>-lc</string>
    <string>claude -p "$(cat seo-work/automation/prompts/weekly-monitor.md)" --permission-mode dontAsk --allowedTools "Read" "Edit(./seo-work/**)" "Edit(./SEO-WORKLOG.md)" "Bash(python3 *)" "WebSearch" "WebFetch" "Skill(seo-suite:*)" "mcp__plugin_seo-suite-dataforseo_dataforseo__api_request" --max-turns 80 --max-budget-usd 3 --output-format json &gt; seo-work/automation/logs/weekly-monitor-$(date +%F).json 2&gt;&amp;1</string>
  </array>
  <key>StartCalendarInterval</key><dict>
    <key>Weekday</key><integer>2</integer><key>Hour</key><integer>7</integer><key>Minute</key><integer>15</integer>
  </dict>
</dict></plist>

Flags in the recipe (cli):

The prompt file (seo-work/automation/prompts/<pipeline>.md) is self-contained. It says:

A scheduled prompt is not live user input and never counts as approval for a gated action (routines).

After loading:

  1. Run it once with launchctl start <label>.
  2. Read the JSON log and the ledger.
  3. Confirm the guard saw the paid calls and that outputs landed. Only then tell the user it’s scheduled.

D. Browser automation (only where vendors allow it)

Checks

Data: free vs paid

The free path covers:

All ten pipelines run at $0 with paid steps skipped. Those steps are marked [needs data].

Paid, only for what free sources can’t give:

Cheapest source per data type, call shapes and traps: paid-tools-automation.md. Plans and prices: tools-paid.md.

Output

Pitfalls & myths

Log the work

Append an entry to SEO-WORKLOG.md per ${CLAUDE_PLUGIN_ROOT}/references/worklog.md.

For setup, include:

For each scheduled run, include: