SEO/GEO Agent
ActiveA technical auditing engine for search and generative-engine visibility, now live as a public self-serve web app at audit.corycopeland.dev — free scoring, accounts, delta tracking, and a tested (not-yet-enabled) Stripe billing layer — plus its own CLI-driven lead-generation and outreach engine.
- Automation
- SEO
- GEO
- Crawling
- Structured data
- SaaS
Updated 2026-07-03

Overview
The SEO/GEO Agent started as a free-by-default audit toolkit for classic SEO
and Generative Engine Optimization — visibility in ChatGPT, Perplexity, and
Google AI Overviews. A pnpm monorepo with a polite, robots-aware crawler, a
JS-render fallback through Puppeteer, seven collectors across SEO and GEO
signals, deterministic scoring, and three Claude Code skills
(/seo-audit, /geo-audit, /full-audit).
That core engine is still there and still the primary interface for batch audits, agency-branded reports, and cold outreach. But it's since grown two more surfaces on top: a live self-serve web app at audit.corycopeland.dev — built in four staged sub-projects (SP1–SP4) — and its own independent lead-generation and outreach engine, complete with a leads ledger and an email drafter. The web app now carries the "Cory Copeland — AI Business Systems" brand lockup: it's deployed and positioned as ABS's free top-of-funnel audit tool, even though the two codebases share no code.
The problem
Two adjacent disciplines, no good single tool:
- Classic SEO — title/description quality, headings, schema, robots, performance, content depth, keyword coverage. A dozen paid SaaS products cover this for marketing teams, but each is rented, opinionated, and hostile to scripting.
- GEO (Generative Engine Optimization) — citability in AI search: whether the page is rendered without JavaScript, whether it has structured data the model can cite, whether the brand has external authority a model can ground against. Practically no off-the-shelf tooling exists yet.
The opportunity was a single toolkit that runs both passes against any URL, writes deterministic reports an LLM can summarize cleanly, and stays free for the operator by default. Paid providers (DataForSEO, real AI-platform citation checking) are stubbed unless free-tier limits actually bite.
A second problem showed up once the toolkit worked well: a CLI is a great
primitive and a terrible pitch. Local-service businesses — the exact
audience the audit is most useful for — don't run pnpm audit-cli. They
need a URL to paste their site into and a reason to hand over an email
address.
Audience
- Operators running multiple brands who need a repeatable audit workflow that doesn't depend on a paid dashboard.
- Developers who want the audit primitives composable, not locked behind a UI — the CLI emits structured JSON; Claude writes the prose.
- Small local-service businesses — roofers, med spas, chiropractors, cleaning services — who'll never touch the CLI but will paste a URL into a free web form, and who are the actual outreach targets of the toolkit's own lead-generation engine.
What I built
The core engine — a pnpm monorepo, unchanged in shape since Phase 1:
@seo-agent/core— collectors (SEO technical, SEO content, SEO keywords; GEO citability, GEO schema, GEO technical, GEO authority), a polite fetcher (robots-aware, disk-cached, retry), a BFS crawler, a Puppeteer JS-render fallback, scoring, and the orchestrator.@seo-agent/render— markdown, PDF, and one-page cold-outreach teaser renderers, plus white-label agency branding on client-facing PDFs.@seo-agent/cli— theaudit-clibinary: audit,compare(before/after delta reports),recheck(scheduled re-audits with auto-delta), and — newer —leadsandoutreach, a self-sourced lead ledger and cold-email drafter for the exact local-service niches the tool is built to audit.- Three skills:
/seo-audit,/geo-audit,/full-audit— still the fastest way to run this from inside a Claude Code session.
A self-serve web app, built in four staged sub-projects (SP1–SP4), all live at audit.corycopeland.dev:
- SP1 — audit service + public page. Paste a URL, get an SSRF/rate-limit/dedupe-guarded audit, watch a results page poll to completion, and trade an email for the full PDF. No signup required for the free score.
- SP2 — accounts + history. Magic-link auth, owner-scoped saved sites, per-site audit history, and a before/after delta compare view.
- SP3 — billing + plans. A full Stripe Checkout/Customer Portal integration, Free (3 saved sites, on-demand) vs. Pro (~$29/mo, 5 monitored sites with scheduled recheck + delta email alerts) — built and tested end to end, but not switched on in production yet (the Stripe keys are intentionally unset).
- SP4 — ops integration, merged today: an admin view of job/lead metrics, and a token-authed export/ack protocol that a nightly cron uses to pull web-app leads into the same ledger the CLI's outreach drafter reads from.
The web app carries the "Cory Copeland — AI Business Systems" brand lockup —
it's deployed and positioned as ABS's free top-of-funnel audit tool, even
though it shares no code with the separate ai-business-systems-demos /
ai-systems-builder repos.
Invocation, still, is either through Claude Code directly:
/full-audit https://spiceshelf.app
/seo-audit https://kingrove.corycopeland.dev --pdf
/geo-audit https://example.com --explain
…or through the CLI binary, or now through the public web form.
Product decisions
- Deterministic data, LLM-written prose. The CLI's job is to be boring
and correct — emit the same
report.jsonfor the same URL every run. The skill's job is to be readable. Mixing those produces unreproducible outputs and brittle scoring. - Free by default; paid providers are stubs. DataForSEO and real
AI-platform citation checking are stubbed in
packages/core/src/providers/and wire in only if the free path proves insufficient. - The web app is a second surface, not a replacement. The CLI/skills remain the primary interface for batch audits, agency branding, and outreach; the web app is a self-serve funnel built on the same engine for people who'll never touch a terminal.
- Build the paywall before you need it, but don't flip it on before you
do. SP3 shipped a fully tested, provider-abstracted billing layer
(
BillingProviderinterface, Stripe implementation plus a fake for tests) months before the first Pro subscriber — but the production Stripe keys stay unset until there's an actual reason to charge someone. - SSRF guarding is a first-class module, not an afterthought. A
dedicated
net-guardpackage does default-deny DNS resolution, per-hop redirect re-validation on every fetch, explicit 6to4/Teredo/fe80::/10blocking, and a browser subresource-interception policy — because this tool's whole job is fetching arbitrary URLs strangers paste into a public form. - The web app has its own lead-generation twin, deliberately kept
separate. The CLI's
leads/outreachcommands source and pitch local service businesses directly (flat-fee fix + retainer), independent of whoever finds the tool through the web form. SP4's nightly sync converges both into one ledger without merging the systems that produce them. - Self-healing sync over perfect sync. SP4's export→import→ack cursor only advances after a successful import, so a failed cron run automatically re-fetches the same batch the next night instead of losing leads or needing manual recovery.
Technical architecture
- Language / runtime: Node 20+, TypeScript, pnpm monorepo, SQLite
(
better-sqlite3, WAL mode) as the datastore across both the CLI ledger and the web app. - Crawler / collectors: unchanged from Phase 1 — BFS, robots-aware, disk-cached, Puppeteer JS-render fallback, seven independently testable collectors.
- Web app deployment: two Docker containers (
web— never touches Chromium;worker— runs the audit with system Chromium) sharing a/datavolume, on a dedicated LXC (deployment-pve, VMID 221) behind a Cloudflare Tunnel,webbound to127.0.0.1:3000only. - Queue: rate-limit → canonicalize + SSRF-validate → dedupe-within-window → queue-cap busy path (email-gated) → enqueue — one composed pipeline, five independently tested outcomes.
- Billing: idempotent, signature-verified Stripe webhook state machine
with a
stripe_eventsledger for exactly-once processing andclient_reference_idbinding so a completed checkout can't grant Pro without a confirmed subscription. - Scheduler: claim + enqueue + cursor-advance wrapped in a single SQLite transaction, after a Codex review caught that a naive scheduler tick could crash mid-cycle and double-enqueue a recheck.
- Providers:
MailProvider(Resend) andBillingProvider(Stripe) are both interface-abstracted with fake implementations for tests, so the entire notification/billing surface is unit-testable without live keys.
Real-world calibration
Phase 1's scoring thresholds were originally calibrated against the plan's
own test fixtures. After dogfooding against three brands — Spiceshelf 63,
SetOff 55, KinGrove 49 — geo-citability was restructured into
geo.no-static-render (empty raw HTML, JS render saves it) and
geo.thin-content-rendered (sparse even after JS render). The trailing-slash
duplicate-title bug from the original TODO list is fixed (canonicalize
before dedupe). The geo-schema per-type weight is still a flat, uncalibrated
guess — genuinely still open.
Current status
- Core engine (CLI + 3 skills): live, unchanged in shape, still the primary interface for batch/agency work and the tool behind the CLI's own lead-generation and outreach engine.
- Web app (SP1, SP2, SP4): live at audit.corycopeland.dev — the public free-audit funnel works end to end.
- SP3 billing: built, tested, dark. Accounts, the dashboard, and billing are all currently blocked in production because the Stripe environment variables are intentionally unset — login 500s until that flips on (tracked as a repo issue).
- SP4 admin view: shipped but not yet reachable for the same reason (needs a logged-in admin); the export/ack sync endpoints it also shipped are token-authed and run regardless.
What I would do next
- Flip on the Stripe keys and take the SP3 billing layer live — it's already built and tested, just not turned on.
- Calibrate
geo-schemaweights against more real brand data. - Put
/dataon a backup cron — currently the one real infra gap on the new deployment. - Decide whether the web app's self-service leads and the CLI's cold-outreach leads should ever share a single outreach cadence, or stay deliberately separate funnels feeding the same ledger.
Proof
- Live web app: audit.corycopeland.dev
- Repo:
spirix/seo-agenton self-hosted Forgejo. - Live skills:
/seo-audit,/geo-audit,/full-auditavailable in Claude Code on the dev environment. - The hero image on this page is a live capture of the audit web app.