URL: /capabilities/size-up-competitors/competitor-gap · Id: `page:/capabilities/size-up-competitors/competitor-gap`

# Competitor gap

> Your site and three rivals in, three lists out: searches they win and you do not, terms they pay for, and sites linking to them.

Your site and three rivals in, three lists out: searches they win and you do not, terms they pay for, and sites linking to them.

```text title="Run it · Ask"
Show me where my three nearest competitors get traffic I do not, spend up to $3, and show me the plan first.
```

```ts title="Run it · SDK"
import { $ } from 'startup-builder'

const result = await $.try.Domain.compare({ goal: 'Show me where my three nearest competitors get traffic I do not, spend up to $3, and show me the plan first.', budget: { amount: 3, currency: 'USD' } })
```

```json title="Run it · Response"
{
  "mode": "try",
  "stored": false,
  "estimate": "About $1.44 per 3 rivals",
  "sample": {
    "term": "rent ledger template",
    "searches": "1,900 a month",
    "difficulty": "18",
    "ranks for it": "keyrent.example, unitbook.example",
    "you": "not in the top 100"
  }
}
```

- **Kind:** Workflow
- **Price:** $1.44 per 3 rivals
- **Your time:** 10 minutes
- **Approval:** Runs on its own
- **Deepens:** [`process:content-marketing`](https://startupbuilder.dev/workflows/processes/content-marketing)

## What you get
- **3 rivals** · Your site and three rivals in, three lists out: searches they win and you do not, terms they pay for, and sites linking to them. · [`noun:domain`](https://startupbuilder.dev/business/nouns/domain)
- `gap.terms` · Terms at least one rival ranks in the top 20 for and you do not, with searches and difficulty. Sort by difficulty: under 20 is your first page.
- `gap.paid` · Terms rivals pay for, with cost per click and the month they started. A term paid for 6 months or more converts for them.
- `gap.ads` · Each rival’s running ads, first seen and last seen. The longest-running ad is the message that pays.
- `gap.links` · sites that link to two or more rivals and not to you, with their authority: your first link pitches.
- `rivals` · Each rival’s authority, and `thin: true` when it ranks for under 20 terms, so a weak rival never drives the plan.

**How long:** About 1 to 2 minutes.

## Trigger
It runs when you call it, and each month as a Monitor that re-reads rivals whose pages changed.

```ts
every.month(($) => $.Domain.compare({ hostname: 'leaseline.app', competitors: ['keyrent.example', 'unitbook.example', 'rentrival.example'] }))
```

## Steps
Your agent does these steps, one row per step; each step’s check is part of its row and its price.

| Step | Verb | Returns | Price | Approval |
| --- | --- | --- | --- | --- |
| 1. Find the rivals when none are named | [`Domain.findCompetitors`](/code/data/domains/find-competitors) | The 10 domains that share the most ranked terms with you; the top 3 are kept | About 0.5¢ | Runs on its own |
| 2. Read the terms you and each rival rank for | [`Keyword.find`](/code/data/keywords/find) | 200 ranked terms per domain, with position and searches | About 40¢ | Runs on its own |
| 3. Read the terms each rival pays for | [`Keyword.find`](/code/data/keywords/find) | 100 paid terms per rival, with cost per click and first month seen | About 60¢ | Runs on its own |
| 4. Search the ad libraries for each rival | [`Ad.find`](/code/data/ads/find) | Each rival’s running ads on Meta, Google and LinkedIn, with first and last seen | About 1.8¢ | Runs on its own |
| 5. Read the sites that link to you and to each rival | [`Domain.findLinks`](/code/data/domains/find-links) | 500 linking sites per domain, each with its authority and follow status | About 40¢ | Runs on its own |
| 6. Measure each domain’s authority | [`Domain.benchmark`](/code/data/domains/benchmark) | One authority row per hostname | About 1.2¢ | Runs on its own |
| 7. Take the differences | `Collection.query` | Their terms minus yours, their links minus yours, ranked by difficulty and authority | Free | Runs on its own |

Calls: `Domain.findCompetitors`, then `Keyword.find`, then `Ad.find`, then `Domain.findLinks`, then `Domain.benchmark`, then `Collection.query`.

About $1.44 per 3 rivals, each time it runs, capped at $2.00.

## Example: give it a goal
> Show me where my three nearest competitors get traffic I do not, spend up to $3, and show me the plan first.

The plan it shows runs 7 steps, priced before it spends anything; Run it above shows the result it returns.

## When something fails

| When | Then |
| --- | --- |
| A rival ranks for under 20 terms | It is kept and marked `thin`; in goal mode, a rival sharing under 5% of your terms is dropped as a Decision and the next one is read. |
| An ad library returns nothing for a rival | The rival reads `ads: []` with `observedAt`, never “no ads ever”; a rival can run ads under another advertiser name. |
| Link reads hit `maxCost` first | It reads links for you and the two strongest rivals, skips the third with `max-cost`, and the gap still computes. |
| You call it again the same day with the same rivals | It returns the stored gap at $0, keyed on hostname, rivals and day. |

Each step retries three times, then falls through to the next route; one item's failure lands in `skipped` with its reason, never failing the call. Before a step would cross `maxCost` it stops and returns what it has, with a `resume` link. Each step's idempotency key is the call's key plus the step and the item: `leaseline-domain-compare-1-<step>-<item>`.

## Change it
| Ask | Compiles to |
| --- | --- |
| Only show me what they pay for and the ads they run. | [`Domain.compare`](/capabilities/size-up-competitors/competitor-gap) |
| Pitch the sites that link to two of my rivals but not to me. | [`Domain.pitch`](/capabilities/earn-first-links/link-pitches) |
| Write a comparison page for my nearest rival. | [`Page.answer`](/capabilities/make-content/answer-pages) |

```ts
import { $, workflow } from 'startup-builder'

export default workflow({
  name: 'domain-compare',
  verb: 'Domain.compare',
  input: { hostname: 'string', competitors: 'string[]?' },
  maxCost: 2,
  execute: async (ctx, { hostname, competitors }) => {
    const rivals = competitors ?? (await ctx.step('rivals', () => $.Domain.findCompetitors({ hostname, limit: 10 }))).domains.slice(0, 3).map((d) => d.hostname)
    const all = [hostname, ...rivals]
    const ranked = await ctx.step('ranked', () => $.Keyword.find({ rival: all, limit: 200, maxCost: 0.45 }))
    const paid = await ctx.step('paid', () => $.Keyword.find({ rival: rivals, mode: 'paid', limit: 100, maxCost: 0.65 }))
    const ads = await ctx.step('ads', () => $.Ad.find({ advertiser: rivals, venues: ['facebook', 'instagram', 'google-search', 'linkedin'], maxCost: 0.05 }))
    const links = await ctx.step('links', () => $.Domain.findLinks({ hostname, gap: rivals, group: 'sites', limit: 500, maxCost: 0.45 }))
    const auth = await ctx.step('authority', () => $.Domain.benchmark({ hostnames: all, measures: ['authority'], maxCost: 0.02 }))
    return gapOf(hostname, { ranked, paid, ads, links, auth })   // set differences: a declared rule
  },
})
```

Extend it with `extends: 'Domain.compare'` and `base()`, or [compose your own](/code/compose) from these steps.

## Works with
**Plays that order it:** [Benchmark rows](/plays/benchmark-rows), [Editorial SEO](/plays/editorial-seo), [Integration partners](/plays/integration-partners), [Switch kit](/plays/switch-kit).

It serves [Size up competitors](/capabilities/size-up-competitors), and runs in [Paid](/strategy/paid), [Borrowed](/strategy/borrowed). It deepens [`process:content-marketing`](https://startupbuilder.dev/workflows/processes/content-marketing).

## For agents

| Fact | Value |
| --- | --- |
| Guide | `Domain.compare` |
| Guide approval | `limits` |
| Goal | Show me where my three nearest competitors get traffic I do not, spend up to $3, and show me the plan first. It may change counts, the order of steps, soft filters and the route a read takes, and it stops early when the goal is met. It never relaxes your words, raises its own budget, answers its own approval, calls a Play or acts outside your standing approval. |
| Trigger | `every.month(($) => $.Domain.compare({ hostname: 'leaseline.app', competitors: ['keyrent.example', 'unitbook.example', 'rentrival.example'] }))` |
| Retry | Three times with backoff, then the next route; the same idempotency key never charges, sends or posts twice |
| Idempotency key | `leaseline-domain-compare-1` |
| Lookups | A `find:` input asks unless exactly one record fits |

| Step | Action class | Seat rule that would hold it |
| --- | --- | --- |
| 1. Find the rivals when none are named | None | None. |
| 2. Read the terms you and each rival rank for | None | None. |
| 3. Read the terms each rival pays for | None | None. |
| 4. Search the ad libraries for each rival | None | None. |
| 5. Read the sites that link to you and to each rival | None | None. |
| 6. Measure each domain’s authority | None | None. |
| 7. Take the differences | None | None. |

The amounts live once, on the hub ([approvals](https://startupbuilder.dev/agents/approvals)).

| Step | Function | Doer | Checker | Assurance | Inputs |
| --- | --- | --- | --- | --- | --- |
| 1. Find the rivals when none are named | `code` | Code | Code checks each result against its declared type | `measured` | `$0.hostname` |
| 2. Read the terms you and each rival rank for | `code` | Code | Code checks each result against its declared type | `measured` | `$0.hostname, $1.rivals` |
| 3. Read the terms each rival pays for | `code` | Code | Code checks each result against its declared type | `measured` | `$1.rivals` |
| 4. Search the ad libraries for each rival | `code` | Code | Code checks each result against its declared type | `measured` | `$1.rivals, $0.venues` |
| 5. Read the sites that link to you and to each rival | `code` | Code | Code checks each result against its declared type | `measured` | `$0.hostname, $1.rivals` |
| 6. Measure each domain’s authority | `code` | Code | Code checks each result against its declared type | `measured` | `$0.hostname, $1.rivals` |
| 7. Take the differences | `code` | Code | Code checks each result against its declared type | `measured` | `$2, $3, $4, $5, $6` |

The doer is never the checker, and the weakest assurance on the path is the whole call’s.

## Under the hood

| Kind | What it touches |
| --- | --- |
| Systems | None. |
| Nouns | [`noun:domain`](https://startupbuilder.dev/business/nouns/domain), [`noun:keyword`](https://startupbuilder.dev/business/nouns/keyword), [`noun:ad`](https://startupbuilder.dev/business/nouns/ad), [`noun:collection`](https://startupbuilder.dev/business/nouns/collection) |
| Events | `Domain.benchmarked`, `Collection.queried` |

## Reference

| Name | Type | Default | Meaning |
| --- | --- | --- | --- |
| `hostname` | `Hostname` | Required | Your site. |
| `competitors` | `Hostname[]` | Required | Up to three rivals. Leave it out and it picks the three that share the most ranked terms with you. |
| `rows` | `object` | `{ ranked: 200, paid: 100, links: 500 }` | Rows read per domain for each list. |
| `venues` | `VenueId[]` | `['facebook', 'instagram', 'google-search', 'linkedin']` | The ad libraries searched for each rival’s running ads. |
| `maxCost` | `number` | `2.00` | The most this call spends each time it runs, in USD. It stops before a step would cross it and returns what it has, with a `resume` link. |

Emits `Domain.compared`. Refuses with [`max-cost`](/code/refusals#max-cost) before it spends, and with the Data calls and Accounts groups ([refusals](/code/refusals)).