Cory Copeland
Back to work

SEO/GEO Agent

Active

A 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

SEO/GEO Agent

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 — the audit-cli binary: audit, compare (before/after delta reports), recheck (scheduled re-audits with auto-delta), and — newer — leads and outreach, 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.json for 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 (BillingProvider interface, 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-guard package does default-deny DNS resolution, per-hop redirect re-validation on every fetch, explicit 6to4/Teredo/fe80::/10 blocking, 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/outreach commands 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 /data volume, on a dedicated LXC (deployment-pve, VMID 221) behind a Cloudflare Tunnel, web bound to 127.0.0.1:3000 only.
  • 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_events ledger for exactly-once processing and client_reference_id binding 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) and BillingProvider (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-schema weights against more real brand data.
  • Put /data on 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-agent on self-hosted Forgejo.
  • Live skills: /seo-audit, /geo-audit, /full-audit available in Claude Code on the dev environment.
  • The hero image on this page is a live capture of the audit web app.