URL: /capabilities/convert-visitors/whos-visiting · Id: `page:/capabilities/convert-visitors/whos-visiting`

# Who is visiting

> A visit to your pricing page in, the company behind it out, checked against your customer sentence and added to your accounts when it fits.

A visit to your pricing page in, the company behind it out, checked against your customer sentence and added to your accounts when it fits.

```text title="Run it · Ask"
Tell me which companies visited my pricing page twice this week and fit my ICP, spend up to $1, and show me the plan first.
```

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

const result = await $.try.Visitor.match({ goal: 'Tell me which companies visited my pricing page twice this week and fit my ICP, spend up to $1, and show me the plan first.', budget: { amount: 1, currency: 'USD' } })
```

```json title="Run it · Response"
{
  "mode": "try",
  "stored": false,
  "estimate": "About 1.2¢ a visit",
  "sample": {
    "visit": "/pricing, twice",
    "company": "Harbor Point Residential",
    "fit": "strong",
    "added": "account Audience",
    "why now": "visited /pricing twice this week"
  }
}
```

- **Kind:** Workflow
- **Price:** 1.2¢ per visit
- **Your time:** 4 minutes
- **Approval:** Runs on its own
- **Deepens:** [`process:inbound-sales`](https://startupbuilder.dev/workflows/processes/inbound-sales)

## What you get
- **1 visit** · A visit to your pricing page in, the company behind it out, checked against your customer sentence and added to your accounts when it fits. · [`noun:visitor`](https://startupbuilder.dev/business/nouns/visitor)
- `organization` · The company behind the visit: hostname, name and `fit` (`strong`, `possible` or `none`), or nothing when the visit came from a home or mobile network.
- `added` · The account Audience it joined, with `whyNow`, such as “visited /pricing twice this week”.
- `person` · Empty until the visitor signs up; `Visitor.identify` joins them then.

**Connect once:** [Google Analytics](/code/integrations/google-analytics). **How long:** Under 5 seconds.

## Trigger
It runs on each new session on a buying page, from a visitor who is not a customer and not a bot.

```ts
on.Visit.created((visit, $) => ['/pricing', '/compare', '/demo'].includes(visit.path) && $.Visitor.match({ visitor: visit.visitor }))
```

## 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 company behind the visit | [`Organization.resolve`](/code/data/organizations/resolve) | The company’s hostname, or no match; a miss is free | About 1¢ | Runs on its own |
| 2. Read the company’s profile | [`Organization.enrich`](/code/data/organizations/enrich) | Headcount, location and industry | About 0.2¢ | Runs on its own |
| 3. Check it against your customer sentence | `Collection.query` | `strong`, `possible` or `none`, by the declared rule | Free | Runs on its own |
| 4. Add a strong fit to your accounts | `Audience.update` | The company on the account Audience, with why now | Free | Runs on its own |

Calls: `Organization.resolve`, then `Organization.enrich`, then `Collection.query`, then `Audience.update`.

About 1.2¢ a visit, each time it runs, capped at 5¢.

## Example: give it a goal
> Tell me which companies visited my pricing page twice this week and fit my ICP, spend up to $1, 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 |
| --- | --- |
| The visit came from a home, mobile or VPN network | No company resolves, the visit keeps no row, and the miss costs nothing. |
| The company is already a customer | It is skipped before any read: the trigger ignores customers. |
| One visitor comes back the same day | The stored match is returned at $0: it is idempotent per visitor 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-visitor-match-1-<step>-<item>`.

## Change it
| Ask | Compiles to |
| --- | --- |
| Write to the operations lead at each strong fit within the hour. | [`Prospect.reach`](/capabilities/reach-out/personal-first-touch) |
| Show my ads to the companies that visited pricing. | `Integration.sync` |
| Resolve visits to my blog posts too. | [`Visitor.match`](/capabilities/convert-visitors/whos-visiting) |

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

export default workflow({
  name: 'visitor-match',
  verb: 'Visitor.match',
  input: { visitor: 'string' },
  trigger: { event: 'Visit.created' },
  maxCost: 0.05,
  execute: async (ctx, { visitor, icp = 'icps/icp_4' }) => {
    const org = await ctx.step('resolve', () => $.Organization.resolve({ visitor, maxCost: 0.01 }))
    if (!org.match) return { visitor }
    const profile = await ctx.step('profile', () => $.Organization.enrich({ hostname: org.match.hostname, fields: ['profile'], maxCost: 0.01 }))
    const fit = fitOf(profile, icp)                                                          // the declared ICP rule
    if (fit !== 'strong') return { visitor, organization: { ...org.match, fit } }
    return ctx.step('add', () => $.Audience.update({ id: 'audiences/audience_accounts', add: [org.match], whyNow: 'visited /pricing' }))
  },
})
```

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

## Works with
**Plays that order it:** [Account visits](/plays/account-visits), [Retargeting](/plays/retargeting).

It serves [Convert visitors](/capabilities/convert-visitors), and runs in [Paid](/strategy/paid). It deepens [`process:inbound-sales`](https://startupbuilder.dev/workflows/processes/inbound-sales).

## For agents

| Fact | Value |
| --- | --- |
| Guide | `Visitor.match` |
| Guide approval | `limits` |
| Goal | Tell me which companies visited my pricing page twice this week and fit my ICP, spend up to $1, 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.Visit.created((visit, $) => ['/pricing', '/compare', '/demo'].includes(visit.path) && $.Visitor.match({ visitor: visit.visitor }))` |
| Retry | Three times with backoff, then the next route; the same idempotency key never charges, sends or posts twice |
| Idempotency key | `leaseline-visitor-match-1` |
| Lookups | A `find:` input asks unless exactly one record fits |

| Step | Action class | Seat rule that would hold it |
| --- | --- | --- |
| 1. Find the company behind the visit | None | None. |
| 2. Read the company’s profile | None | None. |
| 3. Check it against your customer sentence | None | None. |
| 4. Add a strong fit to your accounts | None | None. |

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

| Step | Function | Doer | Checker | Assurance | Inputs |
| --- | --- | --- | --- | --- | --- |
| 1. Find the company behind the visit | `code` | Code | Code checks each result against its declared type | `measured` | `$0.visitor` |
| 2. Read the company’s profile | `code` | Code | Code checks each result against its declared type | `measured` | `$1.hostname` |
| 3. Check it against your customer sentence | `code` | Code | Code checks each result against its declared type | `measured` | `$2, $0.icp` |
| 4. Add a strong fit to your accounts | `code` | Code | Code reads the result back from where it landed | `measured` | `$3.strong` |

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

## Reference

| Name | Type | Default | Meaning |
| --- | --- | --- | --- |
| `visitor` | `Visitor` | Required | The visit to resolve. |
| `paths` | `string[]` | `['/pricing', '/compare', '/demo']` | Only visits to these pages are resolved: a buying page is the trigger, a blog read is not. |
| `icp` | `ICP` | `your saved ICP` | The declared rule a company must pass to be added. |
| `maxCost` | `number` | `0.05` | 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 `Visitor.matched`. Refuses with [`max-cost`](/code/refusals#max-cost) before it spends, and with the Data calls and Accounts groups ([refusals](/code/refusals)).