# Lookalike

Nearest accounts, deals, and themes by conversation similarity - centroid cosine over your own corpus, honest about coverage

`POST /lookalike` answers "more like this one" over your **own** conversation corpus. `lookalike.find` takes a seed company or deal and returns the entities most similar to it, ranked by centroid cosine similarity; `lookalike.similar_themes` takes any free-text description and returns the customer-conversation ML themes closest to it. Both are honest about coverage: when the similarity data is not materialized for your workspace yet, they say so (`available: false`) instead of guessing.

Every "lookalike audience" tool ranks companies by firmographics. Amdahl ranks by **conversation**: each company / deal / theme gets a centroid vector built from its utterances in your corpus, and similarity is distance between centroids. The account whose buying committee raises the same concerns, in the same words, as your last three wins is a better expansion bet than the one that merely shares an industry code.

| | Operation | REST | MCP (`lookalike` tool) | Scope |
| --- | --- | --- | --- | --- |
| Nearest entities to a seed | `lookalike.find` | `POST /lookalike` | action `find` | `data:read` |
| Nearest themes to a description | `lookalike.similar_themes` | `POST /lookalike/themes` | action `themes` | `data:read` |

## `find` — nearest entities to a seed

| Field | Type | Default | Notes |
| --- | --- | --- | --- |
| `entity_type` | `company` \| `deal` | — (required) | Which centroid family to search. |
| `entity_id` | string | — | The seed's id, as your pipeline emits it — preferred. |
| `domain` | string | — | Company seeds only: tried **as** the entity id (see below). |
| `limit` | int | `10` | Max neighbours, cap `50`. |

```bash
curl -X POST "https://app.amdahl.co/api/platform/v1/lookalike" \
  -H "X-API-Key: $AMDAHL_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "entity_type": "company", "entity_id": "1852…", "limit": 10 }'
```

Over MCP, the `lookalike` tool's `find` action:

```json
{ "action": "find", "entity_type": "company", "entity_id": "1852…", "limit": 10 }
```

What comes back — the seed's centroid facts plus its ranked neighbours (the seed itself is excluded):

```json
{
  "success": true,
  "available": true,
  "seed": { "entity_type": "company", "entity_id": "1852…", "member_count": 214, "computed_at": "2026-07-19T03:40:11Z" },
  "matches": [
    { "entity_id": "9b03…", "score": 0.91, "member_count": 88 },
    { "entity_id": "77aa…", "score": 0.87, "member_count": 45 }
  ]
}
```

`member_count` is how many corpus utterances back each centroid — treat a match built on 6 utterances more skeptically than one built on 200. `score` is cosine similarity: use it to rank within a response, not as a calibrated probability.

**On `domain`:** company seeds also accept a `domain`, which is tried *as* the entity id (company centroid ids are the pipeline's company ids). If the domain is not that id for your workspace, you get the honest `available: false` below rather than a guessed match — when you have a company id (from a [routed-search](search.md) row, for example), prefer it.

## Handling `available: false` — coverage, not failure

Centroids are materialized by a background pipeline sweep on its own cadence. A new workspace, a just-connected data source, or a seed with no corpus presence yet all land here:

```json
{ "success": true, "available": false, "reason": "centroids_not_materialized", "matches": [] }
```

Two `reason` values, two different reactions:

- **`centroids_not_materialized`** — the similarity data is not there (yet) for this seed or workspace. Not retryable-right-now, not an error. Fall back to the `themes` action (which degrades gracefully on its own), ask the question as [semantic search](search.md#the-semantic-lane), or simply re-check after the pipeline's next materialization pass.
- **`error`** — the read itself failed. Transient; retry later.

The HTTP status is 200 either way — branch on `available` and `reason`, not on status codes.

## `themes` — nearest themes to a description

The theme-level sibling: describe an idea, an angle, or a complaint in free text and get the ML conversation themes it lands on.

```bash
curl -X POST "https://app.amdahl.co/api/platform/v1/lookalike/themes" \
  -H "X-API-Key: $AMDAHL_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "query": "frustration with slow security reviews blocking procurement", "limit": 5 }'
```

```json
{ "action": "themes", "query": "frustration with slow security reviews blocking procurement", "limit": 5 }
```

```json
{
  "success": true,
  "results": [
    { "cluster_id": "c_0412", "label": "Security review as deal blocker", "score": 0.88, "member_count": 96 },
    { "cluster_id": "c_0197", "label": "Procurement timeline friction", "score": 0.81, "member_count": 143 }
  ],
  "freshness": { "source": "pgvector", "synced_at": "2026-07-21T04:12:09Z" }
}
```

Unlike `find`, `themes` never returns `available: false` — when the fast centroid mirror is off or unsynced it transparently falls back to the warehouse theme index, and `freshness.source` tells you which one answered (`pgvector` with a `synced_at` stamp, or `bigquery` for the fallback).

## A prompt to hand an agent

```
Similarity read over our own corpus — use the lookalike tool.

Seed: {company id or domain of a top closed-won account | deal id}.
Find the {10} most similar {companies | deals}, with scores and member counts.

If it comes back available: false with centroids_not_materialized, don't
improvise a list — tell me, then run the themes action instead with a
description of what made the seed a great account, and give me the nearest
themes with their labels and member counts.

For every match, note the member_count so I know how much conversation
evidence backs the similarity.
```

## Variations

- **Deal-shaped seeds**: `entity_type: "deal"` + the deal id — "which past deals did this one talk like?" for triage and postmortems.
- **Campaign-to-theme mapping**: run `themes` on a draft campaign angle before you write it — if it maps onto a big, well-labeled theme you have grounding; if nothing scores well, your corpus does not talk about it yet.
- **Chain it**: seed from your best closed-won, then enrich and interrogate each match — the full play is the [expansion motion](../endpoints.md#how-the-endpoints-chain).

## Tips

- **Prefer `entity_id` over `domain`.** Centroid ids are pipeline company ids; a routed-search row gives you the real id for free, and an unmapped domain resolves to `available: false` by design.
- **Weight matches by `member_count`.** Similarity over a thin corpus slice is noisy; the score ranks, the member count tells you how much to trust the rank.
- **`available: false` is a state, not a bug.** Centroids materialize on the pipeline's cadence — build the fallback (themes / semantic search / re-check) into any automation instead of treating it as an error path.
- **Lookalike expansion mines your corpus, not the open market.** Matches are entities you have already talked to — dormant accounts, stalled deals, early conversations that resemble your winners.

## See also

- [Search](search.md) — `similar_themes` is the theme-level sibling of the semantic lane; the filter lane is where seeds come from.
- [Enrich](enrich.md) — what to run on each match once you have the list.
- [Endpoints overview](../endpoints.md) — the whole surface, prerequisites, and the expansion motion.
