# SEO FACTORY — AGENT SPECIFICATION
# Version: 1.0.0
# Format: YAML-like structured text (machine-readable)
# URL: /docs/agent-spec
---
PRODUCT: SuperDuperSEO
DESCRIPTION: Subscription-based automated SEO content engine that generates
and publishes programmatic pages to Ghost and Directus CMSes daily.
BASE_URL: https://app.seofactory.io
API_BASE: /api/v1
---
OBJECT_MODEL:
User:
id: cuid
email: string (unique)
name: string|null
created_at: datetime
Workspace:
id: cuid
name: string
slug: string (unique)
automation_status: ACTIVE | PAUSED | STOPPED
ga4_property_id: string|null
Domain:
id: cuid
workspace_id: ref:Workspace
hostname: string (e.g. example.com)
language: string (ISO 639-1)
country: string (ISO 3166-1 alpha-2)
tone_guidance: string|null
verification_status: PENDING | VERIFIED | FAILED
verification_token: string
CmsConnection:
id: cuid
workspace_id: ref:Workspace
domain_id: ref:Domain|null
type: GHOST | DIRECTUS
encrypted_credentials: json (AES-256-GCM encrypted at rest)
is_active: boolean
Subscription:
id: cuid
workspace_id: ref:Workspace (unique)
stripe_customer_id: string
stripe_subscription_id: string|null
status: ACTIVE | PAST_DUE | CANCELED | PAUSED | TRIALING
daily_budget: float (USD, minimum 3.00)
pages_per_day: int (= floor(daily_budget / 0.19))
next_charge_at: datetime|null
current_period_start: datetime|null
canceled_at: datetime|null
ContentItem:
id: cuid
workspace_id: ref:Workspace
domain_id: ref:Domain
keyword: string
is_preview: boolean
status: DRAFT | PREVIEW | PUBLISHED | FAILED
seo_title: string (max 60 chars)
meta_description: string (max 155 chars)
slug: string (unique)
content_html: string (HTML)
faq_json_ld: json|null (schema.org/FAQPage)
tokens: int|null
cost_estimate: float|null (USD)
cms_id: string|null (ID in CMS)
url: string|null
run_id: ref:Run
published_at: datetime|null
Run:
id: cuid
workspace_id: ref:Workspace
domain_id: ref:Domain
type: PREVIEW | DAILY
status: PENDING | RUNNING | COMPLETED | FAILED
pages_generated: int
pages_published: int
error_message: string|null
started_at: datetime|null
completed_at: datetime|null
---
STATE_MACHINES:
AUTOMATION_STATUS:
states: [ACTIVE, PAUSED, STOPPED]
transitions:
PAUSED → ACTIVE:
trigger: preview.approved AND subscription.status == ACTIVE
ACTIVE → PAUSED:
triggers:
- invoice.payment_failed (webhook)
- subscription.canceled (webhook)
- manual pause via API
PAUSED → STOPPED:
trigger: subscription.canceled
note: Automation NEVER starts before preview approval.
CONTENT_STATUS:
states: [DRAFT, PREVIEW, PUBLISHED, FAILED]
transitions:
DRAFT → PREVIEW: preview generation completes
DRAFT → PUBLISHED: daily automation + publishing worker completes
DRAFT → FAILED: generation or publishing error
PREVIEW → FAILED: publishing worker error
SUBSCRIPTION_STATUS:
states: [TRIALING, ACTIVE, PAST_DUE, PAUSED, CANCELED]
source_of_truth: Stripe webhooks
transitions:
PAUSED → ACTIVE:
trigger: checkout.session.completed OR invoice.paid
ACTIVE → PAST_DUE:
trigger: invoice.payment_failed
ACTIVE → CANCELED:
trigger: customer.subscription.deleted
PAST_DUE → ACTIVE:
trigger: invoice.paid (retry succeeds)
---
BILLING_RULES:
MODEL: Subscription (NOT usage-based)
PRICING: $0.19 per page capacity
FORMULA: pages_per_day = floor(daily_budget / 0.19)
MINIMUM_DAILY_BUDGET: $3.00 USD
CHARGE_TIMING: Immediately at checkout (Stripe subscription start)
REBILL_INTERVAL: Every 24 hours from subscription start timestamp
REBILL_LOGIC: Rolling 24h cadence (NOT calendar-based)
STRIPE_INTERVAL: interval=day, interval_count=1
VALIDATION_ERROR: "Daily budget must be at least $3.00"
EXAMPLE:
daily_budget: 10.00
pages_per_day: 100
first_charge: at checkout completion
next_charge: checkout_timestamp + 24 hours
PAYMENT_FAILURE:
action_1: automation.status → PAUSED (immediate)
action_2: send payment_failed email to workspace owner
action_3: show red banner in dashboard
SUBSCRIPTION_CANCELED:
action_1: automation.status → PAUSED (immediate)
action_2: send subscription_canceled email
action_3: account remains accessible (no deletion)
---
EMAIL_SCHEDULE:
PREVIEW_ABANDONMENT:
trigger: preview generation completes AND user exits without approving
note: Subscription is already active. Emails are informational.
sequence:
- delay: +15 minutes → PREVIEW_ABANDON_15M
- delay: +1 hour → PREVIEW_ABANDON_1H
- delay: +24 hours → PREVIEW_ABANDON_24H
- delay: +48 hours → PREVIEW_ABANDON_48H
cancel_condition: automation.status == ACTIVE (preview approved)
content: link to /onboarding/step/5
PAYMENT_FAILED:
trigger: invoice.payment_failed webhook
template: payment_failed
timing: immediate
SUBSCRIPTION_CANCELED:
trigger: customer.subscription.deleted webhook
template: subscription_canceled
timing: immediate
---
API_ENDPOINTS:
AUTH: Header X-API-Key: sf_<32-byte-hex>
RATE_LIMITS:
default: 100 requests/minute per workspace
generate: 5 requests/hour per workspace
ERROR_CODES:
401: Unauthorized (missing or invalid API key)
402: Payment required (no active subscription)
403: Forbidden (automation not approved)
429: Rate limit exceeded
400: Bad request (validation error)
500: Internal server error
GET /api/v1/stats:
description: Workspace page generation statistics
response:
pages:
today: int
last_7_days: int
last_30_days: int
lifetime: int
subscription:
status: string
daily_budget: float
pages_per_day: int
example:
curl -H "X-API-Key: sf_abc123" /api/v1/stats
GET /api/v1/content:
description: List generated content items
params:
status: DRAFT|PREVIEW|PUBLISHED|FAILED (optional)
from: ISO date string (optional)
to: ISO date string (optional)
page: int (default 1)
limit: int (default 20, max 100)
response:
data: ContentItem[]
pagination: { total, page, limit, pages }
POST /api/v1/generate:
description: Manually trigger automation run
requires: subscription.status == ACTIVE, automation.status == ACTIVE
rate_limit: 5/hour
response:
success: boolean
job_id: string
GET /api/v1/analytics:
description: GA4 sessions and pageviews
params:
from: ISO date string (optional)
to: ISO date string (optional)
response:
data: [{ date, sessions, pageviews }]
total_sessions: int
total_pageviews: int
POST /api/v1/keywords:
description: Bulk import keywords
body:
keywords: string[] (max 500)
response:
imported: int
keyword_set_id: string
GET /api/v1/subscription:
description: Subscription status
response:
subscription:
status: string
daily_budget: float
pages_per_day: int
next_charge_at: ISO datetime|null
current_period_start: ISO datetime|null
canceled_at: ISO datetime|null
---
CONTENT_GENERATION:
MODEL: claude-sonnet-4-6
PREVIEW_PAGES: 5 (generated during steps 4-5, before approval)
DAILY_PAGES: up to pages_per_day per domain
PREVIEW_CONTENT: truncated to ~500 words
FULL_CONTENT: 1200+ words (informational), 800+ words (commercial)
ANTI_SPAM_SAFEGUARDS:
- slug_uniqueness: enforced (fallback: slug + timestamp)
- semantic_variance: each page takes unique angle
- no_doorway_pages: content satisfies search intent
- cluster_based_authority: related pages link to each other
- max_daily_velocity: controlled by pages_per_day limit
PAGE_OUTPUT:
seo_title: string (max 60 chars)
meta_description: string (max 155 chars)
slug: string (url-safe)
content_html: string (full HTML)
faq_json_ld: string (schema.org/FAQPage)
---
WORKER_QUEUES:
automation_generation: runs daily content generation
publishing: publishes content to Ghost or Directus CMS
analytics_sync: syncs GA4 data for dashboard
email_nudges: sends scheduled email notifications
---
SECURITY:
cms_credentials: AES-256-GCM encrypted at rest
ghost_admin_key: server-side only, NEVER sent to browser
api_keys: SHA-256 hashed in DB, shown once on creation
prefix_format: sf_<32-hex> → prefix stored as first 11 chars
rate_limiting: Redis-based sliding window
audit_log: all publish, billing, subscription events logged