Docs menu
# 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