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
- Deepens
- Content marketing
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' } })
{
"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"
}
}
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, andthin:when it ranks for under 20 terms, so a weak rival never drives the plan.true
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.
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 | 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 | 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 | 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 | 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 | 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 | 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
Each failure, and what happens next
| 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 |
| 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 |
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: 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
| 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: |
| 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 |
|---|
| 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
| 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, |
| 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, |
| 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, |
| 6. Measure each domain’s authority | code | Code | Code checks each result against its declared type | measured | $0.hostname, |
| 7. Take the differences | code | Code | Code checks each result against its declared type | measured | $2, |
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
| Kind | What it touches |
|---|---|
| Systems | None. |
| Nouns | Domain, Keyword, Ad, Collection |
| Events | Domain.benchmarked, Collection.queried |
Reference
Inputs, outputs, its Event and its errors
| 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: | Rows read per domain for each list. |
venues | VenueId[] | ['facebook', | 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 before it spends, and with the Data calls and Accounts groups (refusals).