URL: /code/data/domains/find-links · Id: `page:/code/data/domains/find-links`

# Domain.findLinks

> List who links to a domain, with anchors and follow status, and the sites that link to your rivals but not to you.

`POST https://api.sb/{your-domain}/domains/find-links`

List who links to a domain, with anchors and follow status, and the sites that link to your rivals but not to you.

- **Price:** 2¢ per 100 links
- **Success rate:** 100% of 3,967 calls
- **Freshness:** Fresh for 30 days
- **Emits:** `Domain.linksFound`
- **Capability:** [Market data](https://startupbuilder.dev/systems/marketing/market-data)

```text title="Run it · Ask"
Which sites link to rentrival.example and ledgerly.example but not to leaseline.app? Spend up to 15¢.
```

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

const result = await $.try.Domain.findLinks({ hostname: 'leaseline.app', group: 'sites', gap: ['rentrival.example', 'ledgerly.example'], where: { minAuthority: 20, maxSpam: 5 }, maxCost: 0.15 })
```

```json title="Run it · Response"
{
  "sites": [
    {
      "hostname": "landlordtalk.example",
      "authority": 38,
      "spamScore": 3,
      "links": 4,
      "follow": true,
      "linksTo": [
        "rentrival.example",
        "ledgerly.example"
      ]
    },
    {
      "hostname": "nwpropertymanagers.org",
      "authority": 29,
      "spamScore": 1,
      "links": 1,
      "follow": true,
      "linksTo": [
        "rentrival.example"
      ]
    }
  ],
  "after": "ls_77Pq",
  "observedAt": "2026-10-27T14:02:11Z",
  "method": "estimated",
  "cost": {
    "usd": 0,
    "maxUsd": 0.12,
    "units": 600,
    "misses": 0
  }
}
```

## Options and prices
| You get | Input that selects it | Price | Success rate |
| --- | --- | --- | --- |
| Every link, with its anchor and follow status | `{"group":"links"}` | About 2¢ per 100 links | 100% of 3,967 calls |
| The sites that link | `{"group":"sites"}` | About 2¢ per 100 sites | 100% of 3,967 calls |
| Sites linking to your rivals and not to you | `{"group":"sites","gap":["rentrival.example"]}` | About 2¢ per 100 site compareds | 100% of 3,967 calls |
| Anchor text, counted | `{"group":"anchors"}` | About 0.7¢ a report | 100% of 491 calls |
| Links gained and lost by month | `{"group":"changes"}` | About 2.4¢ a report | 100% of 298 calls |
| Is a link you earned still live | `{"check":["https://nwpropertymanagers.org/vendors"]}` | About 1.3¢ a source checked | 100% of 4 calls |

## Inputs
| Name | Type | Default | Meaning |
| --- | --- | --- | --- |
| `hostname` | [`Hostname`](/code/data/types#hostname) | Required | The domain to read links to: yours or a rival’s. |
| `scope` | `'domain' \| 'host' \| 'url'` | `'domain'` | Links to the whole domain, one host, or one URL in `hostname`. |
| `group` | `'links' \| 'sites' \| 'anchors' \| 'changes'` | `'links'` | What the rows are. Each is its own job with its own price. |
| `gap` | [`Hostname[]`](/code/data/types#hostname) | None. | Up to 5 rivals: sites linking to them and not to `hostname`. Use with `group: sites`. |
| `check` | `string[]` | None. | Up to 50 page URLs that should link to you: is each link still there? |
| `where` | `{ follow?: boolean; minAuthority?: number; maxSpam?: number; since?: string }` | None. | Filters run before rows are billed. Gap lists fill with hubs and directories, so set `minAuthority` and `maxSpam`. |
| `limit` | `number` | `100` | Rows a page, at most 10,000. You pay for rows returned, never rows asked for. |
| `after` | [`Cursor`](/code/data/types#cursor) | None. | The cursor from the last page. |
| `maxCost` | `number` | `0.50` | The most this call may spend, in USD. Over it, the call is refused before it spends, or stops early with what it has. |

## Outputs
| Field | Type | What `null` means |
| --- | --- | --- |
| `links` | `{ from: string; to: string; anchor: string \| null; follow: boolean; firstSeen: string \| null; lastSeen: string \| null; live?: boolean }[]` | Present with `group: links` or `check`; `anchor: null` is an image link. |
| `sites` | `{ hostname: Hostname; authority: number \| null; spamScore: number \| null; links: number; follow: boolean \| null; linksTo: Hostname[] }[]` | Present with `group: sites`. `linksTo` names the rivals each site links to with `gap`; `follow: null` means mixed. |
| `anchors` | `{ text: string; links: number }[]` | Present with `group: anchors`. |
| `changes` | `{ month: string; gained: number; lost: number }[]`, in referring domains a month | Present with `group: changes`; a month with no reading is left out. |
| `after` | [`Cursor`](/code/data/types#cursor) | The last page: there are no more rows. |
| `(report)` | [`Observed`](/code/data/types#observed) | The report carries `observedAt` and `method`. |
| `cost` | [`Cost`](/code/data/types#cost), in USD | Never null. In try mode `usd` is 0 and `maxUsd` is what the call would have cost. |

## Read the result
| Field | Value | What it means | What to do |
| --- | --- | --- | --- |
| `sites.linksTo` | 2 or more rivals | The site links to products like yours already, so it may link to one more. | Find the editor, then send one ask that names the page and why it helps their reader. (the link-gap playbook) |
| `sites` | A gap list of 120 domains | Planning: 120 filter to about 40 worth asking, 70% have an editor you can find, and 10% to 15% say yes. | Expect 3 to 4 links from 28 asks; replace these rates with your own after the first 30. (our planning numbers) |
| `follow` | `false` | The link sends visitors but passes no ranking weight. | Count it toward referral traffic, not toward your referring-domain target. (how search engines treat nofollow and ugc links) |
| `links.to` | Mostly your home page | Your target pages earn no weight of their own. | Point the next asks at the pages you want ranked, and link them from the home page. (the backlinks playbook) |
| `changes.lost` | Every week | Listings are lapsing or paid placements are expiring. | Check each source with `check`; renew only those that send visitors. (the backlinks playbook) |

## Effects
**Action class**

None: it reads, so no approval rule names it

**Modes**

- `try`: Runs the read and returns real rows; nothing is saved and nothing is billed.
- `send`: Returns a pending Action at once; its Event carries the rows when the read finishes.
- `do`: Waits for the read and returns the rows, billed per result.

**Idempotency**

The same `Idempotency-Key` with the same input replays the first answer at $0; with other input it returns 409.

**Saved or shared**

Nothing is written to your records. Link readings are shared across startups for 30 days.

**Emits**

`Domain.linksFound` with `{ hostname, group, gap, rows }`

**Performed by**

[Analyst](https://startupbuilder.dev/agents/marketing/analyst)

**Price**

About 2¢ per 100 links. Free: rows filtered out by `where` before they return. `maxCost` defaults to 50¢. 3 rivals × 200 linking sites, diffed against yours: 600 × 0.02¢ a site compared is about 12¢.

**Reliability**

The first route succeeds 100% of 3,967 calls. If it fails, the call refuses with `sources-busy` and nothing is spent.

**Freshness**

A reading younger than 30 days returns at $0. Readings are shared across startups for the same subject and locale.

**Batch and pages**

Up to 10,000 rows a page. Pages with an `after` cursor, 25 rows a page by default and 100 at most. It answers in one response.

**Rate**

[The shared rate rules](/code/data#rate): 10 calls a second per key, and an array input counts as one call.

## Refuses when
This call has no refusals of its own. Every data call can also meet [the shared data-call refusals](/code/refusals#data-calls): [`filter-unsupported`](/code/refusals#filter-unsupported), [`input-missing`](/code/refusals#input-missing), [`limit-exceeded`](/code/refusals#limit-exceeded), [`max-cost`](/code/refusals#max-cost), [`rate-limited`](/code/refusals#rate-limited), [`sources-busy`](/code/refusals#sources-busy), [`venue-unsupported`](/code/refusals#venue-unsupported).

## When it waits
Returns in one response; nothing waits.

## Example

**Ask**

```text
Which sites link to rentrival.example and ledgerly.example but not to
leaseline.app? Spend up to 15¢.
```

**API**

```bash
curl -X POST "https://api.sb/leaseline.app/domains/find-links?mode=try" \
  -H "Authorization: Bearer $SB_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"hostname":"leaseline.app","group":"sites","gap":["rentrival.example","ledgerly.example"],"where":{"minAuthority":20,"maxSpam":5},"maxCost":0.15}'
```

The response is the one in the Run it window above.

**CLI**

```bash
npx startup-builder do Domain.findLinks --try --data '{"hostname":"leaseline.app","group":"sites","gap":["rentrival.example","ledgerly.example"],"where":{"minAuthority":20,"maxSpam":5},"maxCost":0.15}'
```

**MCP**

```json
{
  "name": "do",
  "arguments": {
    "mode": "try",
    "code": "return await $.Domain.findLinks({ hostname: 'leaseline.app', group: 'sites', gap: ['rentrival.example', 'ledgerly.example'], where: { minAuthority: 20, maxSpam: 5 }, maxCost: 0.15 })"
  }
}
```

**SDK**

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

const result = await $.try.Domain.findLinks({
  hostname: 'leaseline.app',
  group: 'sites',
  gap: ['rentrival.example', 'ledgerly.example'],
  where: { minAuthority: 20, maxSpam: 5 },
  maxCost: 0.15,
})
```

To retry, send the same `Idempotency-Key`: the retry returns the first answer for $0. A new key reads again.

A `sk_test_` key answers these the same way every time, at $0 ([every test value](/code/data#test-values)).

| Test value | Always returns | What it is for |
| --- | --- | --- |
| `maxCost: 0.0001` | Refuses with [`max-cost`](/code/refusals#max-cost) | Any priced call with a maxCost of 0.0001 is refused before it spends: the price is above the cap. |
| `rate-limited.test` | Refuses with [`rate-limited`](/code/refusals#rate-limited) | Any call naming this hostname answers 429 with Retry-After, so your retry logic runs. |
| `sources-busy.test` | Refuses with [`sources-busy`](/code/refusals#sources-busy) | Any call naming this hostname answers 503 as if every route were busy, and nothing is spent. |

Watch it on a schedule: a Monitor runs this call and records an Event only when the reading changes.

```ts
await $.Monitor.create({
  verb: 'Domain.findLinks',
  input: { hostname: 'leaseline.app', group: 'changes' },
  change: 'link-lost',
  every: '1w',
})
```

## Used by
- **Workflows:** [Competitor gap](/capabilities/size-up-competitors/competitor-gap)
- **Jobs:** [Earn first links](/capabilities/earn-first-links) · [Get indexed](/capabilities/get-indexed) · [Size up competitors](/capabilities/size-up-competitors)

## Related
[`Domain.benchmark`](/code/data/domains/benchmark) · [`Domain.findCompetitors`](/code/data/domains/find-competitors) · [`Domain.verify`](/code/data/domains/verify)