URL: /code/data/keywords/find · Id: `page:/code/data/keywords/find`

# Keyword.find

> Find the terms worth a page: ideas from your seed words, buyers’ questions, and the terms a rival ranks or pays for.

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

Find the terms worth a page: ideas from your seed words, buyers’ questions, and the terms a rival ranks or pays for.

- **Price:** 5¢ per 100 ideas
- **Success rate:** 100% of 16,219 calls
- **Freshness:** Fresh for 30 days
- **Emits:** `Keyword.found`
- **Capability:** [Market data](https://startupbuilder.dev/systems/marketing/market-data)

```text title="Run it · Ask"
Which rent-tracking search terms are worth a page, at difficulty 30 or under? Spend up to 20¢.
```

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

const result = await $.try.Keyword.find({ seeds: ['rent tracking software', 'rent ledger'], where: { difficulty: { max: 30 } }, limit: 3, maxCost: 0.2 })
```

```json title="Run it · Response"
{
  "terms": [
    {
      "term": "rent ledger template",
      "searches": 1900,
      "cpc": 2.4,
      "difficulty": 18,
      "intent": "learn",
      "trend": "rising",
      "venue": "google-search",
      "locale": "en-US",
      "observedAt": "2026-10-27T14:02:11Z",
      "method": "estimated"
    },
    {
      "term": "rent tracking app for landlords",
      "searches": 590,
      "cpc": 9.1,
      "difficulty": 22,
      "intent": "compare",
      "trend": "rising",
      "venue": "google-search",
      "locale": "en-US",
      "observedAt": "2026-10-27T14:02:11Z",
      "method": "estimated"
    },
    {
      "term": "rent ledger for 50 units",
      "searches": null,
      "cpc": null,
      "difficulty": 6,
      "intent": "buy",
      "trend": null,
      "venue": "google-search",
      "locale": "en-US",
      "observedAt": "2026-10-27T14:02:11Z",
      "method": "estimated"
    }
  ],
  "found": 312,
  "kept": 40,
  "peopleAlsoAsk": [
    "How do landlords keep track of rent payments?",
    "Is there a free rent ledger?"
  ],
  "ignored": [],
  "cost": {
    "usd": 0,
    "maxUsd": 0.156,
    "units": 312,
    "misses": 0
  }
}
```

## Options and prices
| You get | Input that selects it | Price | Success rate |
| --- | --- | --- | --- |
| Ideas around your seed words | `{"seeds":["rent tracking software"]}` | About 5¢ per 100 ideas | 100% of 16,219 calls |
| How people finish your words in the search box | `{"mode":"suggestions"}` | About 10¢ per 100 seeds | 100% of 37,928 calls |
| The questions buyers type | `{"mode":"questions"}` | About 18¢ per 100 seeds | 66% of 3,022 calls |
| The terms a rival ranks for | `{"rival":"rentrival.example"}` | About 5¢ per 100 terms | 100% of 19,441 calls |
| The terms a rival pays for | `{"rival":"rentrival.example","mode":"rival-paid"}` | About 20¢ per 100 terms | 99.9% of 2,200 calls |
| What is rising on a site now | `{"mode":"trending","venue":"tiktok"}` | About 0.1¢ a board | 99.9% of 1,350 calls |

## Inputs
| Name | Type | Default | Meaning |
| --- | --- | --- | --- |
| `seeds` | `string[]` | None. | Up to 20 phrases. Seed from the words buyers used in problem posts and calls, not from your product’s own words. Pass `seeds` or `rival`. |
| `rival` | [`Hostname`](/code/data/types#hostname) | None. | The terms this domain ranks for, or pays for with `mode: rival-paid`. |
| `exclude` | [`Hostname`](/code/data/types#hostname) | None. | With `rival`: drop the terms this domain already ranks for. Pass your own to get the content gap. |
| `mode` | `'ideas' \| 'suggestions' \| 'questions' \| 'rival' \| 'rival-paid' \| 'trending'` | `'ideas' with seeds, 'rival' with rival` | Which job runs. Each one returns the same row. |
| `venue` | [`VenueId`](/code/data/types#venueid) | `'google-search'` | `youtube` and `tiktok` take `suggestions`; `tiktok`, `x` and `reddit` take `trending`. |
| `locale` | `string` | `your home Market` | The market and language, such as `en-US`. |
| `where` | `{ searches?: Range; difficulty?: Range; intents?: Intent[] }` | None. | Filters run before the list returns, so you never pay for a term you would drop. |
| `limit` | `number` | `100` | Terms to return, at most 1,000. You pay for terms returned. |
| `save` | `number` | `0` | Save the first n terms as Keywords; free, and it follows the call’s Mode. |
| `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 | Unit | What `null` means |
| --- | --- | --- | --- |
| `terms` | `Keyword[]` | None. | Never null: empty when nothing passed `where`, at $0. |
| `terms[].term` | `string` | None. | Never null. |
| `terms[].searches` | `number \| null` | searches a month | Unknown: no reading for a long-tail term. Never zero demand. |
| `terms[].cpc` | `number \| null` | USD a click | No advertiser bids on it, or no reading. |
| `terms[].difficulty` | `number \| null` | 0 to 100 | Unknown. |
| `terms[].intent` | `'buy' \| 'compare' \| 'learn' \| 'navigate' \| null` | None. | Unknown. |
| `terms[].trend` | `'rising' \| 'flat' \| 'falling' \| null` | 12 months | Under 12 months of readings. |
| `terms[].position` | `number \| null` | None. | Absent without `rival`; `null` past position 100. |
| `terms[].rivalUrl` | `string \| null` | None. | Absent without `rival`. |
| `terms[].monthlyAdCost` | `number \| null` | USD a month | Absent unless `mode: rival-paid`. |
| `terms[].posts` | `number \| null` | posts this week | Absent unless `mode: trending`. |
| `terms[]` | [`Observed`](/code/data/types#observed) | None. | Each row also carries `observedAt`, `method`, `venue` and `locale`. |
| `found` | `number` | terms | Never null: before `where` ran. |
| `kept` | `number` | terms | Never null: after `where` ran; you pay for these. |
| `peopleAlsoAsk` | `string[]` | None. | Never null: empty when the results page shows none. |
| `ignored` | `string[]` | None. | Never null: empty when every filter you sent was applied. |
| `cost` | [`Cost`](/code/data/types#cost) | 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 |
| --- | --- | --- | --- |
| `searches` | `null` | Unknown, not zero: most long-tail B2B terms have no reading. | Keep it if a problem post uses the phrase; drop it only when it reads `null` and no buyer writes it. (the drop rule in the search playbook) |
| `difficulty` | Above your ceiling | Your ceiling is the difficulty your referring domains reach; above it, page one needs links you do not have yet. | Park it for week 12; target the terms at or under the ceiling now. (the referring-domain scale difficulty is defined on) |
| `trend` | `falling` | Demand is leaving the term. | Drop it if it fell more than 30% over 12 months. (our planning number) |
| `intent` | `navigate` to a rival’s brand | The searcher wants that site, not a choice. | Drop it; a page cannot win a search for someone else’s login page. (the intent rule in the search playbook) |
| `kept` | Under 60 of 300 | The seeds are your words, not the buyer’s. | Reseed from the phrases in problem posts and People also ask. (our planning number: 200 to 500 ideas leave about 60) |
| `monthlyAdCost` | Over $0 on a term you want | A rival pays for it, so the term converts for someone. | Earn it if its cost per click is over your break-even; buy it only if under. (break-even cost per click, the strategy numbers) |

## 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**

Terms are returned, not saved, unless you pass `save`. Ideas are shared across startups for the same seeds and locale for 30 days.

**Emits**

`Keyword.found` with `{ terms, found, kept, seeds, rival }`

**Performed by**

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

**Price**

About 5¢ per 100 ideas. Free: ideas from your own Google Ads account, saving terms as Keywords, ideas filtered out by `where` before they return. `maxCost` defaults to 50¢. 300 ideas from 5 seeds: 300 × 0.05¢ an idea is about 15¢.

**Reliability**

The first route succeeds 100% of 16,219 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 1,000 terms a call. 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
| Check | Error | Fix | Then call |
| --- | --- | --- | --- |
| A search engine, answer engine or network the call does not read, such as ChatGPT sent to `Keyword.measure` | [`venue-unsupported`](/code/refusals#venue-unsupported) | Pick one the call’s `venue` input lists; AI answers are `Keyword.ask`, posts are `Keyword.search`. | [`Keyword.ask`](/code/data/keywords/ask) |

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

```json
{
  "type": "https://startupbuilder.dev/errors/venue-unsupported",
  "title": "The call does not read that search engine or network",
  "status": 422,
  "detail": "A search engine, answer engine or network the call does not read, such as ChatGPT sent to `Keyword.measure`.",
  "fix": "Pick one the call’s `venue` input lists; AI answers are `Keyword.ask`, posts are `Keyword.search`.",
  "next": {
    "verb": "Keyword.ask",
    "input": {
      "keyword": "best rent tracking software for a 40-unit property manager",
      "venues": [
        "chatgpt"
      ]
    }
  },
  "instance": "https://api.sb/leaseline.app/actions/action_1"
}
```

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

## Example

**Ask**

```text
Which rent-tracking search terms are worth a page, at difficulty 30 or
under? Spend up to 20¢.
```

**API**

```bash
curl -X POST "https://api.sb/leaseline.app/keywords/find?mode=try" \
  -H "Authorization: Bearer $SB_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"seeds":["rent tracking software","rent ledger"],"where":{"difficulty":{"max":30}},"limit":3,"maxCost":0.2}'
```

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

**CLI**

```bash
npx startup-builder do Keyword.find --try --data '{"seeds":["rent tracking software","rent ledger"],"where":{"difficulty":{"max":30}},"limit":3,"maxCost":0.2}'
```

**MCP**

```json
{
  "name": "do",
  "arguments": {
    "mode": "try",
    "code": "return await $.Keyword.find({ seeds: ['rent tracking software', 'rent ledger'], where: { difficulty: { max: 30 } }, limit: 3, maxCost: 0.2 })"
  }
}
```

**SDK**

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

const result = await $.try.Keyword.find({
  seeds: ['rent tracking software', 'rent ledger'],
  where: { difficulty: { max: 30 } },
  limit: 3,
  maxCost: 0.2,
})
```

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: 'Keyword.find',
  input: { mode: 'trending', venue: 'tiktok' },
  change: 'new-term',
  every: '1w',
})
```

## Used by
- **Workflows:** [Competitor gap](/capabilities/size-up-competitors/competitor-gap) · [Keyword opportunity map](/capabilities/research-demand/keyword-opportunity-map)
- **Agent tasks:** [Monthly search content](/capabilities/make-content/monthly-search-content)
- **Jobs:** [Research demand](/capabilities/research-demand) · [Size up competitors](/capabilities/size-up-competitors)

## Related
[`Keyword.measure`](/code/data/keywords/measure) · [`Keyword.forecast`](/code/data/keywords/forecast) · [`Keyword.search`](/code/data/keywords/search) · [`Keyword.ask`](/code/data/keywords/ask)