There is a category of business that is enormously valuable, deeply local, and almost entirely invisible to the current wave of AI-native company-building: the local service business. Tree services, landscapers, HVAC contractors, plumbers, roofers, dog daycares, trailer dealers. They are the businesses that keep neighborhoods functioning, and they are sitting on more first-party customer signal per dollar of revenue than any tech company on earth.
This post is the second in a series about building self-learning operating models for those businesses. The first introduced the customer intelligence loop — the foundational layer of a self-learning local business. This one is more practical: I'm going to give you the complete playbook for building the first loop I'm running on my own site, localprosolutions.com, end to end.
This is a fully autonomous SEO system. It does its own keyword research, builds new pages, tracks rankings, manages the Google Business Profile, monitors competitors, and decides what to do next — without anyone prompting it. It is built once and runs continuously. The architecture is the same one I'll be cloning to client sites once the lighthouse is stable.
If you are a developer, an agency owner, or a local business operator with technical depth: you can build this. The code isn't the hard part. The hard part is the discipline of letting the system run, reading its action log every week, and tuning its planner prompts based on real outcomes.
What follows is the complete specification. Copy it, adapt it, ship it.
§ 01What this is
A fully autonomous SEO system that runs continuously on a single local service business website. It does its own keyword research, builds new pages, tracks rankings, manages the Google Business Profile, monitors competitors, and decides what to do next — without anyone prompting it.
It is built once for localprosolutions.com as the lighthouse implementation. Once it works there, the same architecture clones to client sites.
What "fully autonomous" means here
- The system publishes content without human review (gated by automatic safety checks, not human approval)
- It schedules its own work based on what's most valuable to do next
- It learns from outcomes — rankings, traffic, leads — and adjusts its own playbook
- A human only intervenes when a safety gate fires or a quarterly strategic review is due
What it is not
- A content mill that publishes 100 thin pages a week (those get deindexed within months)
- A black box (every decision is logged with reasoning in an append-only table)
- A replacement for actual local expertise — it amplifies that expertise, doesn't replace it
§ 02The core mental model
The system is one agent loop that runs forever, with four phases:
- Perceive — pull the current state of the SEO world: rankings, GSC data, GBP insights, competitor changes, site health
- Decide — given that state, what's the single highest-leverage action to take right now?
- Act — do the thing. Publish a page, post to GBP, refresh decaying content, add internal links, fix a tech issue
- Learn — log the action with reasoning, then measure what happened so the next decision is better
Everything else in this document is plumbing around that loop.
§ 03Prerequisites & accounts
Before any code is written, these accounts must exist. None of them are optional.
| Service | Purpose | Why it's required |
|---|---|---|
| GitHub | Code & content versioning | Every change is committed; full audit trail |
| Vercel | Deployment | Auto-deploys on every push; runs scheduled cron jobs |
| Supabase | Database | Source of truth — keywords, rankings, pages, actions |
| Claude API | Agent brain | Planning + execution calls |
| DataForSEO | Keyword + SERP data | Cheap, predictable, MCP available |
| Local Falcon | Local rank tracking | Geo-gridded local rankings — critical |
| Search Console | Query data | The source of truth for what's actually working |
| GBP API | Profile management | Posts, Q&A, photos, services |
| GA4 | Traffic data | Outcome measurement |
| CallRail | Call tracking | Outcome measurement — calls > rankings |
§ 04The tool stack
The principle: the AI is only as useful as the tools it can reach. Every tool here either has a native MCP server or a documented API that gets wrapped in one. The tools change. The protocol doesn't.
Build & operate environment
- Claude Code — the agent runtime. Reads SKILL files, calls MCP tools, executes work.
- GitHub — code, content, prompts, policies, all versioned together.
- Vercel — hosts the site, runs the cron jobs that wake the agent.
Data & state
- Supabase — single source of truth. Every keyword, page, rank, action, experiment lives here.
- Notion (optional) — human-readable mirror for when an owner wants to look at the system without SQL.
Reach surfaces (via MCP)
- DataForSEO MCP — keyword volume, difficulty, SERP, on-page audits
- Local Falcon MCP — geo-gridded local rank tracking
- Ahrefs MCP — backlink and competitor intelligence
- GSC MCP — owned query data
- GBP MCP — post, respond, manage services
- GitHub MCP — commit content, open PRs
- Vercel MCP — trigger deploys, check builds
- Supabase MCP — read/write state
- Gmail / Slack MCPs — alerts and digests
Wrong question: "Which tool should I use?"
Right question: "Can the AI reach it?"
Tools change quarterly. Anchoring the architecture on a specific vendor locks it to that vendor's lifespan. Anchoring on the protocol future-proofs it.
Get everything connected.
Every account is reachable. Every MCP is wired into Claude Code. Every credential lives in one secure place.
0.1 — Initialize the project repo
mkdir lps-seo-agent
cd lps-seo-agent
git init
gh repo create lps-seo-agent --private --source=. --remote=origin
0.2 — Folder structure
lps-seo-agent/
├── site/ # The Astro site
│ ├── src/
│ │ ├── content/ # Markdown — pages, blog, cities
│ │ ├── layouts/ # Astro layouts (schema baked in)
│ │ ├── components/
│ │ └── pages/
│ ├── astro.config.mjs
│ └── package.json
├── agent/ # The agent
│ ├── skills/ # Individual skill files
│ ├── lib/ # Shared utilities, MCP clients
│ ├── prompts/ # System prompts
│ └── CLAUDE.md # Master instruction file
├── infra/
│ ├── supabase/ # Schema migrations
│ └── vercel/ # Cron config
├── .env.example
└── README.md
0.3 — Connect MCPs to Claude Code
Create .claude/mcp.json with entries for every MCP server you need. Pattern looks like:
{
"mcpServers": {
"dataforseo": {
"command": "npx",
"args": ["-y", "dataforseo-mcp-server"],
"env": {
"DATAFORSEO_USERNAME": "${DATAFORSEO_USERNAME}",
"DATAFORSEO_PASSWORD": "${DATAFORSEO_PASSWORD}"
}
},
"supabase": {
"command": "npx",
"args": ["-y", "@supabase/mcp-server-supabase"],
"env": { /* ... */ }
},
// ... github, vercel, localfalcon, gsc, gbp, etc.
}
}
Verify each MCP connects by asking Claude Code to make a trivial call to each one before moving on.
Build the Supabase schema.
Every piece of state the agent needs lives in Supabase. Multi-tenant by client_id from day one, even for a single site — so the same schema works when you expand to clients.
The core tables
| Table | Purpose |
|---|---|
clients | Every site the agent operates on. Business info, service area, GBP ID, repo URL. |
client_facts | Verifiable signals — pricing, team, photos, real customer questions. Injected into every published page. |
keywords | Every term tracked. Volume, difficulty, intent, business fit score, priority score. |
rankings | Daily rank snapshots. Position, URL, SERP features, device, geo. |
pages | Every URL published. Target keyword, status, word count, performance. |
actions | Append-only log of every decision. Reasoning, inputs, outputs, gate results, outcome. |
experiments | A/B tests the agent is running. Hypothesis, variants, winner. |
competitors | Domains the agent watches. |
backlinks | Link profile + outreach pipeline. |
The actions table is the most important one. It's append-only by design. Every action has reasoning attached. That's how you debug, how you learn, and how you defend the work to anyone who asks "why did you publish this?"
create table actions (
id uuid primary key default uuid_generate_v4(),
client_id uuid references clients(id) on delete cascade,
action_type text not null, -- 'publish_page'|'gbp_post'|'refresh'|...
target_table text,
target_id uuid,
reasoning text not null, -- WHY the agent did this
inputs jsonb, -- what state it saw
outputs jsonb, -- what it produced
gate_results jsonb, -- which safety gates fired
outcome jsonb, -- measured after the fact
status text default 'completed',
created_at timestamptz default now()
);
An Astro site, deliberately static.
The site is intentionally a static Astro project. Four reasons:
- Astro outputs plain HTML — perfect for crawling, no SPA rendering blockers
- Markdown content with frontmatter is trivial for the agent to generate and edit
- Schema markup is baked into layouts, not bolted on per page
- Vercel auto-deploys on every commit — "publish" = "git commit"
Each piece of content is a markdown file in a typed collection. Services, cities, blog posts. The frontmatter is enforced by Astro's content collections, which means the agent can't publish a malformed page — it would fail the build.
Content collections (typed schemas)
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const services = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
targetKeyword: z.string(),
serviceAreas: z.array(z.string()).optional(),
pricing: z.object({ from: z.number() }).optional(),
faqs: z.array(z.object({ q: z.string(), a: z.string() })).optional(),
publishedAt: z.date()
})
});
// ... cities, blog collections follow same pattern
export const collections = { services, cities, blog };
Schema markup baked in
Every layout — ServiceLayout.astro, CityLayout.astro, BlogLayout.astro — emits structured data automatically based on the frontmatter. The agent doesn't need to remember to add schema. It can't not have schema.
Each skill is a self-contained capability.
This is the heart of the system. Each skill is a single SKILL.md file telling Claude Code exactly how to do one job. Skills live in agent/skills/. The agent never freelances. It runs skills.
The master instruction file — agent/CLAUDE.md — lists every available skill and the operating principles that apply to all of them.
1. Every action gets logged to the actions table with reasoning.
2. Never publish without running all safety gates.
3. Local trust signals are non-negotiable — every page needs verifiable facts.
4. Rank tracking is a means, not an end. Lead generation is the goal.
5. When uncertain, log a blocked action with reasoning and alert via Slack.
The full skill inventory
Fourteen skills, each ~100-300 lines of natural-language instruction. Together they cover the entire loop.
What it does
Pulls current SEO state from every source: Supabase, GSC, GA4, GBP, Local Falcon. Outputs a structured JSON state object that downstream skills consume.
Why it's first
You can't decide what to do until you know what's happening. This skill is the agent's eyes.
What it does
Takes the state JSON. Calls Claude Opus (the planner model) with a strict constraint: return ONE action. Outputs JSON: action type, target, reasoning, estimated impact, confidence score.
Decision priority order
- Critical issues (de-indexed, manual penalties) — fix immediately
- Decaying winners (>20% drop in 30 days) — refresh
- Easy wins (ranking 4-15, commercial intent) — expand + internal link
- New opportunities (high-priority keywords, no targeting page) — publish
- GBP work — minimum 2 per week
- Maintenance (linking, schema, tech health)
What it does
Calls DataForSEO MCP with seed terms. Pulls related terms, volume, difficulty, CPC, current SERP. Scores each candidate by business_fit, local_match, and priority_score. New keywords inserted with status='discovered'; top 10 by priority get auto-promoted to 'targeted'.
Guardrail
Hard cap of $5 in API spend per run. Skip terms where business fit < 0.4. Skip volume < 10 unless local_match is true.
What it does
For every targeted/ranking keyword, calls DataForSEO SERP. For local-match keywords, also calls Local Falcon for geo-gridded data. Writes to rankings table. Flags movers >5 positions.
What it does
- Reads target keyword, supporting keywords, action input
- Pulls verifiable facts from
client_facts - Pulls top 3 ranking competitor pages, summarizes structure
- Generates a brief: H1, H2 outline, internal link targets, FAQ source material
- Generates full page content as markdown with frontmatter
- Runs safety-gates.md against the draft
- If gates pass: commits to GitHub, Vercel auto-deploys
- Inserts row in
pages, logs the action - Schedules outcome measurement at +7d and +30d
What it does
Same flow as service page, with city-specific requirements: title pattern "{Service} in {City}, {State}"; 600-800 words; embedded GBP map; named neighborhoods; links to all sibling city pages (hub-and-spoke). Schema is Service + LocalBusiness + BreadcrumbList.
Guardrail
Never publish a city page outside the client's service area. Never duplicate primary keyword targeting.
What it does
Informational-intent content only. Pulls People Also Ask data from DataForSEO. Generates 1200-2000 words built around real questions. Always includes a related-services callout box, internal links to 2-3 service pages, and an FAQ section.
What it does
Pulls current page content via GitHub MCP. Pulls top 3 current rankers for the page's target keyword. Diffs the structure — what do they cover that we don't? Pulls recent GSC query data — what queries is the page already attracting but not optimizing for? Generates a refresh plan, applies it as an update, runs gates, commits.
What it does
Builds a graph of current internal links by parsing every markdown file. Identifies orphaned pages and hub candidates. For each orphaned page, finds 3 contextually relevant existing pages and adds links. Commits all changes as one PR, one action log entry.
What it does
Pulls a recent winning page or featured service. Generates a 100-300 word GBP post with a local hook (named city, season, recent project), a clear CTA, and an image reference pulled from client_facts. Posts via GBP MCP. Never repeats the same content within 60 days.
What it does
For each tracked competitor domain, pulls their current top-ranking pages for our targeted keywords. Diffs against last week's snapshot. Identifies new pages, gained rankings, lost rankings. Summarizes new competitor pages — what angles, what keywords? Feeds findings to the planner.
What it does
Runs DataForSEO OnPage against the site and Lighthouse on the 10 most important pages. Identifies broken links, duplicate metadata, slow pages, missing schema, mobile issues. Auto-fixes tier-1 issues (broken internal links, missing alt text, schema additions). Queues tier-2/3 for planner review.
Seven gates, all must pass
- Fact-check — Claude verifies draft against client_facts. Fail if score < 0.85
- YMYL check — fail if medical, legal, financial, safety topics
- Duplicate / cannibalization — fail if existing page targets same keyword
- Local signal — fail if missing 2+ cities, real pricing, or image
- Word count — fail if under minimum for page type
- Internal links — fail if under minimum count
- Brand voice — Claude scores against tone-of-voice JSON. Fail if < 0.75
If any gate fails
Do not publish. Log a 'blocked' action with the gate results. Alert Slack. Move on to the next planned action.
What it does
Pulls the week's actions, rank deltas, traffic deltas, GBP deltas, lead deltas. Generates a markdown digest covering: what I did this week, what moved, what I learned, what I'll prioritize next week. Emails via Gmail MCP. Posts summary to Slack.
The agent runs on triggers, not continuously.
Vercel Cron is the nervous system. A simple vercel.json defines four scheduled endpoints:
{
"crons": [
{ "path": "/api/agent/daily", "schedule": "0 13 * * *" },
{ "path": "/api/agent/weekly", "schedule": "0 14 * * 1" },
{ "path": "/api/agent/monthly", "schedule": "0 15 1 * *" },
{ "path": "/api/agent/digest", "schedule": "0 1 * * 1" }
]
}
- Daily — perceive-state, track-rankings, plan-next-action → execute one action
- Weekly — keyword-research, competitor-delta, plan a 3-5 action queue for the week
- Monthly — audit-site, internal-linking-pass, refresh-decaying-page for any decaying winners
- Digest — weekly-digest report goes out Sunday night
Beyond per-publish gates: global guardrails.
Even with safety gates on every publish, the system needs standing constraints. These are the protections that keep "fully autonomous" from becoming "fully unsupervised."
Cost caps
- DataForSEO — hard cap $50/month per site. Tracked in
actions.inputs.api_cost. Abort runs that would exceed. - Claude API — hard cap $30/month per site. Same pattern.
- Email alert at 80% of monthly cap.
Frequency caps
- Maximum 1 published page per day, 4 per week, 12 per month.
- Maximum 3 GBP posts per week.
- Maximum 1 site-wide internal linking pass per month.
Rollback triggers
- Site-wide traffic drop >15% within 7 days of a publish → auto-rollback (git revert + log).
- Site de-indexed → freeze all publishing, alert immediately.
- GSC manual action → freeze all publishing, alert immediately.
Google notices when a small site goes from publishing once a month to publishing every day. Frequency caps aren't an arbitrary restriction — they're a defensive posture. The whole point of "self-learning" is that the site gets better every week, not just bigger.
Outcomes feed back into the loop.
The system has to prove it's working — to you, to clients, and to itself. The feedback loop is the difference between automation and learning.
After every completed action, three outcome measurements get scheduled:
- +7 days — rank delta on the target keyword
- +30 days — traffic delta on the target page
- +90 days — lead delta (calls attributable to that page via CallRail)
Those outcomes write back to actions.outcome. The planner reads recent outcomes when deciding the next action. Actions that worked get amplified. Actions that didn't get parked. That is the learning mechanism, made concrete.
Rank tracking is a means, not an end. The system optimizes for what shows up in the bank account.
§ 12Build sequence, week by week
| Week | Milestone |
|---|---|
| 1 | Foundation. Accounts, MCPs, Supabase schema, Astro init, one test page live with schema verified. |
| 2 | Perception. perceive-state, track-rankings, keyword-research. Run daily for 7 days against empty queue. |
| 3 | First publish. publish-service-page + safety-gates. Manually trigger one publish. Verify indexed in <72h. |
| 4 | The planner. plan-next-action. Run daily. Watch the action log accumulate. Spot-check reasoning. |
| 5 | Full skill set. City pages, blog, refresh, internal linking, GBP, competitor delta, audit. |
| 6 | Guardrails. All global caps. Trigger a deliberate failure to verify the YMYL gate fires correctly. |
| 7 | Reporting. Dashboard or Notion mirror. Weekly digest. Outcome measurement back-references. |
| 8 | Watch and tune. Two weeks unattended (with Slack monitoring). Read every action. Tune planner prompts. |
After week 8, the lighthouse is live. Every subsequent week, the agent gets smarter because the actions table is growing.
§ 13Appendix — what to expect
Where things will break
I am building this in public, which means I expect it to fail in ways I can't predict yet. The known risks:
- Google will sniff out low-effort AI content fast. Mitigation: every page needs verifiable local signals — real pricing, named neighborhoods, real photos from GBP. The
client_factstable is what separates this from a content mill. - Fully autonomous publishing is a liability surface. Even with no human review, the safety gates are non-negotiable. YMYL topics get flagged. Pages contradicting existing pages get blocked. The "fully autonomous" promise is preserved because of the agent-internal gates, not despite them.
- Local SEO is fundamentally a GBP game. For local service businesses, GBP optimization moves the needle more than 100 blog posts. The system treats GBP work as first-class, not a side feature.
- Rank tracking is a vanity metric. Clients (and you) will churn if you show "ranked #3" while the phone isn't ringing. CallRail integration is what makes the whole thing measurable.
The honest read
If you handed this playbook to a competent full-stack developer with the prerequisite accounts, they could build the lighthouse implementation in 6 to 8 weeks. The code isn't the hard part. The hard part is the discipline of watching the action log every week and tuning the planner prompts based on real outcomes.
The system gets better because you get better at noticing what it's doing well and what it's doing poorly. That's the part that can't be outsourced. That's the part that compounds.