Skip to content

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.

Kind
Workflow
Price
$1.44 per 3 rivals
Your time
10 minutes
Approval
Runs on its own
Domain.compare
You, to your agent

Show me where my three nearest competitors get traffic I do not, spend up to $3, and show me the plan first.

curl -X POST "https://api.sb/leaseline.app/domains/compare?mode=try" \
  -H "Authorization: Bearer $SB_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"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"}}'
npx startup-builder do Domain.compare --try --data '{"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"}}'
{
  "name": "do",
  "arguments": {
    "mode": "try",
    "code": "return await $.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' } })"
  }
}
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' } })
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"
  }
}
Try mode reads, stores nothing and spends nothingAPI referenceIts Nouns

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. · 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.

TypeScript
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.

StepVerbReturnsPriceApproval
1. Find the rivals when none are namedDomain.findCompetitorsThe 10 domains that share the most ranked terms with you; the top 3 are keptAbout 0.5¢Runs on its own
2. Read the terms you and each rival rank forKeyword.find200 ranked terms per domain, with position and searchesAbout 40¢Runs on its own
3. Read the terms each rival pays forKeyword.find100 paid terms per rival, with cost per click and first month seenAbout 60¢Runs on its own
4. Search the ad libraries for each rivalAd.findEach rival’s running ads on Meta, Google and LinkedIn, with first and last seenAbout 1.8¢Runs on its own
5. Read the sites that link to you and to each rivalDomain.findLinks500 linking sites per domain, each with its authority and follow statusAbout 40¢Runs on its own
6. Measure each domain’s authorityDomain.benchmarkOne authority row per hostnameAbout 1.2¢Runs on its own
7. Take the differencesCollection.queryTheir terms minus yours, their links minus yours, ranked by difficulty and authorityFreeRuns 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

Each failure, and what happens next
WhenThen
A rival ranks for under 20 termsIt 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 rivalThe rival reads ads: [] with observedAt, never “no ads ever”; a rival can run ads under another advertiser name.
Link reads hit maxCost firstIt 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 rivalsIt 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

AskCompiles to
Only show me what they pay for and the ads they run.Domain.compare
Pitch the sites that link to two of my rivals but not to me.Domain.pitch
Write a comparison page for my nearest rival.Page.answer
TypeScript
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 from these steps.

Works with

Plays that order it: Benchmark rows, Editorial SEO, Integration partners, Switch kit.

It serves Size up competitors, and runs in Paid, Borrowed. It deepens Content marketing.

For agents

For agents: the guide, its approvals, retries and lookups
FactValue
GuideDomain.compare
Guide approvallimits
GoalShow 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.
Triggerevery.month(($) => $.Domain.compare({ hostname: 'leaseline.app', competitors: ['keyrent.example', 'unitbook.example', 'rentrival.example'] }))
RetryThree times with backoff, then the next route; the same idempotency key never charges, sends or posts twice
Idempotency keyleaseline-domain-compare-1
LookupsA find: input asks unless exactly one record fits
Step
1. Find the rivals when none are named
2. Read the terms you and each rival rank for
3. Read the terms each rival pays for
4. Search the ad libraries for each rival
5. Read the sites that link to you and to each rival
6. Measure each domain’s authority
7. Take the differences

The amounts live once, on the hub (approvals).

Who does each step, and how it is checked
StepFunctionDoerCheckerAssuranceInputs
1. Find the rivals when none are namedcodeCodeCode checks each result against its declared typemeasured$0.hostname
2. Read the terms you and each rival rank forcodeCodeCode checks each result against its declared typemeasured$0.hostname, $1.rivals
3. Read the terms each rival pays forcodeCodeCode checks each result against its declared typemeasured$1.rivals
4. Search the ad libraries for each rivalcodeCodeCode checks each result against its declared typemeasured$1.rivals, $0.venues
5. Read the sites that link to you and to each rivalcodeCodeCode checks each result against its declared typemeasured$0.hostname, $1.rivals
6. Measure each domain’s authoritycodeCodeCode checks each result against its declared typemeasured$0.hostname, $1.rivals
7. Take the differencescodeCodeCode checks each result against its declared typemeasured$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

Under the hood: the Systems, Nouns and Events it touches
KindWhat it touches
SystemsNone.
NounsDomain, Keyword, Ad, Collection
EventsDomain.benchmarked, Collection.queried

Reference

Inputs, outputs, its Event and its errors
NameTypeDefaultMeaning
hostnameHostnameRequiredYour site.
competitorsHostname[]RequiredUp to three rivals. Leave it out and it picks the three that share the most ranked terms with you.
rowsobject{ ranked: 200, paid: 100, links: 500 }Rows read per domain for each list.
venuesVenueId[]['facebook', 'instagram', 'google-search', 'linkedin']The ad libraries searched for each rival’s running ads.
maxCostnumber2.00The 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 before it spends, and with the Data calls and Accounts groups (refusals).

Contents