> ## Documentation Index
> Fetch the complete documentation index at: https://docs.theaitracker.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Data model

> The core Eloquent models and how they relate.

## Campaign is the tenancy boundary

A **campaign** represents one tracked brand. Nearly every other record hangs off it, and the
dashboard is always scoped to the campaign currently selected in the session.

```
User
 └── Campaign (uuid route key)
      ├── Topic
      ├── Prompt ──── PromptRun
      ├── TrafficRecord
      ├── CompetitorAppearance / DiscoveredCompetitor / CompetitorSubdomain
      ├── SentimentRecord ── SentimentMention
      ├── SiteInfo / PagespeedInsight / AiTrainingPresence
      └── PageEngagement / UserEngagementRecord
```

Campaigns also reference up to two `GoogleAccount` records — `ga4_google_account_id` and
`gsc_google_account_id` — so Analytics and Search Console can be connected under different
Google identities. `isDomainFullyVerified()` is true only when both `ga4_domain_verified`
and `gsc_domain_verified` are set.

## Prompts

`Prompt` is the unit of tracking. Its lifecycle is driven by three status constants:

| Status      | Constant                   | Meaning                                     |
| ----------- | -------------------------- | ------------------------------------------- |
| `suggested` | `Prompt::STATUS_SUGGESTED` | Generated by the app, awaiting a decision   |
| `active`    | `Prompt::STATUS_ACTIVE`    | Included in scheduled batches — the default |
| `rejected`  | `Prompt::STATUS_REJECTED`  | Dismissed; kept for history                 |

Useful scopes: `active()`, `suggested()`, `approved()`, `status($status)` and
`forCampaign($campaignId)`. `updateLastRun()` stamps the prompt after a batch.

<Note>
  `PROMPTS_ACTIVE_LIMIT` (default `10`) caps how many prompts a campaign may have active at
  once, which is what bounds the per-campaign provider spend. `PROMPTS_AUTO_SEED` controls
  whether a new campaign gets a starter set automatically.
</Note>

## Prompt runs

`PromptRun` is one execution of one prompt against one provider. Runs are grouped by
`batch_id` — `scopeByBatchId()` is how a whole scheduled batch is retrieved.

Visibility lives on the run:

* `hasVisibility()` — whether the brand was detected in the answer
* `getVisibilityType()` — how it appeared
* `scopeWithVisibility()` / `scopeWithoutVisibility()` — filtering for analytics
* `screenshot_url` accessor — resolves the stored SERP screenshot, captured by
  `TakeSearchScreenshotJob` via ScreenshotOne

## Sentiment

`SentimentRecord` holds a scored topic result, with `SentimentMention` rows for the
individual brand mentions behind the score. The `BrandSentiment` model namespace holds the
supporting value objects used by the scoring pipeline.

## Competitors

Three models cover the competitor flow:

* `DiscoveredCompetitor` — domains found in answers, before classification
* `CompetitorAppearance` — per-run appearances used for benchmarking
* `CompetitorSubdomain` — subdomain grouping so `blog.example.com` and `example.com` count
  as one competitor

`CompetitorClassifier` decides which discovered domains are *direct* competitors. That
verdict feeds `non_competitor_penalty` in the scoring formula, so misclassification directly
moves visibility scores.

## Supporting models

| Model              | Purpose                                  |
| ------------------ | ---------------------------------------- |
| `Plan` / `Product` | Billing catalogue, mirrored from Polar   |
| `TeamMember`       | Seat management under settings           |
| `LlmUsage`         | Per-call token and cost tracking         |
| `Notification`     | In-app notification feed                 |
| `PromptTemplate`   | Reusable prompt scaffolds for generation |
| `Setting`          | Key/value application settings           |
| `LoginToken`       | Magic-link authentication tokens         |
| `Faq`              | Marketing content                        |
