URL: /capabilities/find-customers/lookalikes · Id: `page:/capabilities/find-customers/lookalikes`

# Lookalikes

> Five companies you would love as customers in, 100 more like them out, each with the quote from its own site that shows it fits.

Five companies you would love as customers in, 100 more like them out, each with the quote from its own site that shows it fits.

```text title="Run it · Ask"
Find 100 companies like my five best customers, spend up to $2, and show me the plan first.
```

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

const result = await $.try.Audience.expand({ goal: 'Find 100 companies like my five best customers, spend up to $2, and show me the plan first.', budget: { amount: 2, currency: 'USD' } })
```

```json title="Run it · Response"
{
  "mode": "try",
  "stored": false,
  "estimate": "About 36¢ per 100 companies",
  "sample": {
    "company": "Maple Court Homes",
    "similar to": "northgate-rentals.example",
    "fit": "strong",
    "quote": "“Our team manages 180 rental homes across the Twin Cities”"
  }
}
```

- **Kind:** Workflow
- **Price:** 36¢ per 100 companies
- **Your time:** 10 minutes
- **Approval:** Runs on its own
- **Deepens:** [`process:outbound`](https://startupbuilder.dev/workflows/processes/outbound)

## What you get
- **100 companies** · Five companies you would love as customers in, 100 more like them out, each with the quote from its own site that shows it fits. · [`noun:audience`](https://startupbuilder.dev/business/nouns/audience)
- `audience` · The kept companies, saved as an account Audience.
- `companies[].similarTo` · The seed each company was found from, so you can see which seed draws the best fits.
- `companies[].fit` · `strong`, `possible` or `unknown`, with the quote and its page for a strong fit.
- `bySeed` · Strong fits per seed: drop a seed that draws under 10 in 100.

**How long:** About a minute.

## Trigger
It runs when you call it, and each time an order is placed: the new customer becomes a seed.

```ts
on.Order.placed((order, $) => $.Audience.expand({ seeds: [order.customer.domain], take: 25 }))
```

## 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. Read your seeds | `Collection.query` | The five seed companies: your top customers by revenue, or the ones you named | Free | Runs on its own |
| 2. Find companies like each seed | [`Organization.find`](/code/data/organizations/find) | One and a half rows for every company wanted, each tagged with its seed | About 6¢ | Runs on its own |
| 3. Ask each site the fit question | [`Organization.find`](/code/data/organizations/find) | `fit` with a quote code finds verbatim on the page, or `unknown` | About 30¢ | Runs on its own |
| 4. Save the strong fits as an Audience | [`Audience.create`](/code/apps/ads/create-audience) | An account Audience of `take` strong fits | Free | Runs on its own |

Calls: `Collection.query`, then `Organization.find`, then `Audience.create`.

About 36¢ per 100 companies, each time it runs, capped at 75¢.

## Example: give it a goal
> Find 100 companies like my five best customers, spend up to $2, and show me the plan first.

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

## When something fails

| When | Then |
| --- | --- |
| Fewer than 3 seeds are given | It refuses with `too-few-seeds` and the fix “Name three companies like your best customer”, before it spends. |
| A seed returns no lookalikes | It is skipped and named in `bySeed`; the others carry on. |
| A site cannot be read | The company reads `fit: unknown` and stays out; the question costs nothing when no page answers. |
| You call it again the same day with the same seeds | It returns the stored Audience at $0: it is idempotent per seeds 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-audience-expand-1-<step>-<item>`.

## Change it
| Ask | Compiles to |
| --- | --- |
| Find the operations lead at each, with a checked email. | [`Audience.build`](/capabilities/find-customers/build-a-verified-list) |
| Drop any seed that finds fewer than 10 strong fits. | [`Audience.expand`](/capabilities/find-customers/lookalikes) |
| Sync this list to my Meta ad account as an audience. | `Integration.sync` |

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

export default workflow({
  name: 'audience-expand',
  verb: 'Audience.expand',
  input: { seeds: "'top-customers' | string[]", take: 'integer', question: 'string?', people: 'boolean' },
  maxCost: 0.75,
  execute: async (ctx, { seeds = 'top-customers', take = 100, question, people = false }) => {
    const s = seeds === 'top-customers' ? (await ctx.step('seeds', () => $.Collection.query({ from: 'customer', sort: '-arr', limit: 5 }))).map((c) => c.domain) : seeds
    if (s.length < 3) return ctx.refuse('too-few-seeds', 'Name three companies like your best customer')
    const like = await ctx.step('similar', () => $.Organization.find({ similarTo: s, limit: Math.ceil(take * 1.5), maxCost: 0.1 }))
    const fit = await ctx.step('fit', () => $.Organization.find({ hostnames: like.organizations.map((o) => o.hostname), question, maxCost: 0.35 }))
    const strong = fit.organizations.filter((o) => o.fit === 'strong' && o.evidence.quoteFound).slice(0, take)   // a declared rule
    const audience = await ctx.step('save', () => $.Audience.create({ name: `Like my best · ${ctx.today}`, members: strong }))
    return people ? ctx.step('people', () => $.Audience.build({ from: audience.$id, titles: ['owner', 'head of operations'], size: take })) : audience
  },
})
```

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

## Works with
**Plays that order it:** [Portfolio cross-sell](/plays/cross-sell), [House ads](/plays/house-ads), [Sibling audiences](/plays/sibling-audiences).

It serves [Find your first customers](/capabilities/find-customers), and runs in [Hands-on](/strategy/hands-on). It deepens [`process:outbound`](https://startupbuilder.dev/workflows/processes/outbound).

## For agents

| Fact | Value |
| --- | --- |
| Guide | `Audience.expand` |
| Guide approval | `limits` |
| Goal | Find 100 companies like my five best customers, spend up to $2, 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 | `on.Order.placed((order, $) => $.Audience.expand({ seeds: [order.customer.domain], take: 25 }))` |
| Retry | Three times with backoff, then the next route; the same idempotency key never charges, sends or posts twice |
| Idempotency key | `leaseline-audience-expand-1` |
| Lookups | A `find:` input asks unless exactly one record fits |

| Step | Action class | Seat rule that would hold it |
| --- | --- | --- |
| 1. Read your seeds | None | None. |
| 2. Find companies like each seed | None | None. |
| 3. Ask each site the fit question | None | None. |
| 4. Save the strong fits as an Audience | None | None. |

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

| Step | Function | Doer | Checker | Assurance | Inputs |
| --- | --- | --- | --- | --- | --- |
| 1. Read your seeds | `code` | Code | Code checks each result against its declared type | `measured` | `$0.seeds` |
| 2. Find companies like each seed | `code` | Code | Code checks each result against its declared type | `measured` | `$1.seeds` |
| 3. Ask each site the fit question | `code` | Code | Code finds the quoted evidence on the page | `measured` | `$2.organizations, $0.question` |
| 4. Save the strong fits as an Audience | `code` | Code | Code reads the result back from where it landed | `measured` | `$3.strong, $0.take` |

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 | [Ads](/code/apps/ads) |
| Nouns | [`noun:collection`](https://startupbuilder.dev/business/nouns/collection), [`noun:organization`](https://startupbuilder.dev/business/nouns/organization), [`noun:audience`](https://startupbuilder.dev/business/nouns/audience) |
| Events | `Collection.queried`, `Audience.created` |

## Reference

| Name | Type | Default | Meaning |
| --- | --- | --- | --- |
| `seeds` | `'top-customers' \| Hostname[]` | `'top-customers'` | Three to five companies to match: your best customers by revenue, or the ones you would most like to sign. |
| `take` | `integer` | `100` | Strong-fit companies wanted. It stops at `take` strong fits, not at `take` rows. |
| `question` | `string` | `the question saved with your ICP` | Asked of each company’s own site; a fit needs the quote that answers it. |
| `people` | `boolean` | `false` | Hand the kept companies to Build a verified list for a checked email at each. |
| `maxCost` | `number` | `0.75` | 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 `Audience.expanded`. Refuses with [`max-cost`](/code/refusals#max-cost) before it spends, and with the Data calls and Accounts groups ([refusals](/code/refusals)).