Skip to content

Build a verified list

One sentence in, 100 checked work emails out, each at the right company, with the quote that shows the fit.

Kind
Workflow
Price
$1.38 per 100 emails
Your time
8 minutes
Approval
Runs on its own
Deepens
Outbound
Audience.build
You, to your agent

Find 50 verified heads of operations (or COO, VP Operations, Director of Property Operations) at US residential property management companies managing roughly 100 to 1,000 units, spend up to $4, and show me the plan first.

curl -X POST "https://api.sb/leaseline.app/audiences/build?mode=try" \
  -H "Authorization: Bearer $SB_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"goal":"Find 50 verified heads of operations (or COO, VP Operations, Director of Property Operations) at US residential property management companies managing roughly 100 to 1,000 units, spend up to $4, and show me the plan first.","budget":{"amount":4,"currency":"USD"}}'
npx startup-builder do Audience.build --try --data '{"goal":"Find 50 verified heads of operations (or COO, VP Operations, Director of Property Operations) at US residential property management companies managing roughly 100 to 1,000 units, spend up to $4, and show me the plan first.","budget":{"amount":4,"currency":"USD"}}'
{
  "name": "do",
  "arguments": {
    "mode": "try",
    "code": "return await $.Audience.build({ goal: 'Find 50 verified heads of operations (or COO, VP Operations, Director of Property Operations) at US residential property management companies managing roughly 100 to 1,000 units, spend up to $4, and show me the plan first.', budget: { amount: 4, currency: 'USD' } })"
  }
}
import { $ } from 'startup-builder'

const result = await $.try.Audience.build({ goal: 'Find 50 verified heads of operations (or COO, VP Operations, Director of Property Operations) at US residential property management companies managing roughly 100 to 1,000 units, spend up to $4, and show me the plan first.', budget: { amount: 4, currency: 'USD' } })
  1. Prospect.count with the company filters, free, to size each query before a row is bought.
  2. Prospect.find from the query that sizes best, then screen each company against your sentence.
  3. ContactPoint.findEmail, cheapest route first, at most $0.03 a person.
  4. ContactPoint.verifyEmail, free routes first.
  5. Repeat until 50 pass.
Estimate$0.45 of $4.00

Planning is free. You see the plan and its price before anything runs.

Try mode reads, stores nothing and spends nothingAPI referenceIts Nouns

What you get

  • 100 checked emails · One sentence in, 100 checked work emails out, each at the right company, with the quote that shows the fit. · Audience
  • audience · The saved list, as an Audience you can enroll, sync to an ad account or refresh.
  • rows · One row per person: company, domain, person, title, email and its check result, with the quote that shows fit.
  • funnel · How many matched, fit, had a person, had an email at the right company, and checked valid: the counts that tell you which filter to loosen.
  • skipped · Every row it dropped, by reason and count: no email found, wrong company, catch-all, invalid, already a contact.
  • cost · What it spent each time it ran, and its cap.

How long: About 3 to 6 minutes; the email finds set the pace.

Trigger

It runs when you call it. List refresh re-reads the Audience each month and on every bounce.

TypeScript
every.month(($) => $.Audience.refresh({ audience: 'audiences/audience_31' }))

Steps

Your agent does these steps, one row per step; each step’s check is part of its row and its price.

StepVerbReturnsPriceApproval
1. Count the marketOrganization.countHow many companies match, free, before a row is boughtFreeRuns on its own
2. Find the companies and ask each site the fit questionOrganization.findThree companies for every email wanted, each with fit and a quote code finds verbatim on the pageAbout 60¢Runs on its own
3. Find the peopleProspect.findOne person per strong-fit company with a matching title; names and titles are freeFreeRuns on its own
4. Find each work emailContactPoint.findEmailAn address for about 85 in 100, in fit order; a miss is freeAbout 54¢Runs on its own
5. Drop wrong-company addresses and find againContactPoint.findEmailAny address whose domain is not the account’s domain dropped, and found again from the person’s current profile URL; about 1 in 12About 4.5¢Runs on its own
6. Check each emailContactPoint.verifyEmailvalid, catch-all, invalid or unknown; catch-all and unknown cost nothingAbout 20¢Runs on its own
7. Drop who you already knowCollection.queryThe rows not already a contact, a customer, suppressed or on an open dealFreeRuns on its own
8. Save the AudienceAudience.createOne Audience of size rows, linked to this callFreeRuns on its own

Calls: Organization.count, then Organization.find, then Prospect.find, then ContactPoint.findEmail, then ContactPoint.verifyEmail, then Collection.query, then Audience.create.

About $1.38 per 100 checked emails, each time it runs, capped at $2.00.

Example: give it a goal

Find 50 verified heads of operations (or COO, VP Operations, Director of Property Operations) at US residential property management companies managing roughly 100 to 1,000 units, spend up to $4, and show me the plan first.

Run it above shows the plan it showed before it spent anything, and the receipt of every call.

When something fails

Each failure, and what happens next
WhenThen
A site cannot be readThe company reads fit: unknown and stays out of the list; it is counted under skipped as site-unreadable.
No email is found for a personThe row is skipped as no-email-found, and the miss costs nothing.
A find returns an address at another companyCode drops it and finds again from the person’s current profile URL on the account’s own domain. In the 50-lead goal, 4 first finds pointed at other companies (one at a county government domain), so the run reads 50 verified, 46 at the right company on the first find, 4 replaced.
The check says catch-all or unknownIt settles at $0 and stays out unless accept includes catch-all; a deep check (ContactPoint.verifyEmail with depth: deep) resolves it for about 0.4¢.
size cannot be reached under maxCostIt returns what it found, with a max-cost skip count and a resume link that carries on from the last company read.

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-build-1-<step>-<item>.

Change it

AskCompiles to
Count catch-all addresses too, in their own batch.Audience.build
Only companies hiring a leasing coordinator this month.Organization.find
Keep this list fresh every month and drop anyone who bounces.Audience.refresh
TypeScript
import { $, workflow } from 'startup-builder'

/** An address is at the right company only on the account's own domain. */
const sameDomain = (address: string, hostname: string) => address.split('@')[1]?.toLowerCase() === hostname.replace(/^www\./, '').toLowerCase()

export default workflow({
  name: 'audience-build',
  verb: 'Audience.build',
  input: { icp: 'string', question: 'string?', titles: 'string[]', size: 'integer', perCompany: 'integer', accept: "('valid' | 'catch-all')[]" },
  maxCost: 2,
  execute: async (ctx, { icp, question, titles, size = 100, perCompany = 1, accept = ['valid'] }) => {
    const market = await ctx.step('count', () => $.Organization.count({ where: { description: icp } }))           // free
    if (market.count < size * 3) return ctx.refuse('market-too-small', 'Widen the customer sentence, or lower size')
    const orgs = await ctx.step('companies', () => $.Organization.find({ where: { description: icp }, question, limit: size * 3, maxCost: 0.7 }))
    const fit = orgs.organizations.filter((o) => o.fit === 'strong' && o.evidence.quoteFound)                  // a declared rule
    const people = await ctx.step('people', () => $.Prospect.find({ organizations: fit.map((o) => o.hostname), titles, perCompany }))
    const checked = []
    for (const p of people.prospects) {                                                                        // fit order
      if (checked.filter((c) => accept.includes(c.status)).length >= size) break                               // stop at size
      let found = await ctx.step(`find:${p.$id}`, () => $.ContactPoint.findEmail({ prospect: p.$id, maxCost: 0.02 }))
      if (found.address && !sameDomain(found.address, p.organization.hostname))                                // wrong company
        found = await ctx.step(`refind:${p.$id}`, () => $.ContactPoint.findEmail({ prospect: p.$id, from: p.profileUrl, domain: p.organization.hostname, maxCost: 0.02 }))
      if (!found.address || !sameDomain(found.address, p.organization.hostname)) continue                      // skipped: wrong-company
      checked.push(await ctx.step(`verify:${p.$id}`, () => $.ContactPoint.verifyEmail({ address: found.address, maxCost: 0.005 })))
    }
    const reachable = checked.filter((c) => accept.includes(c.status))
    const fresh = await ctx.step('dedupe', () => $.Collection.query({ minus: ['contacts', 'customers', 'suppressions', 'deals'], rows: reachable, key: 'email' }))
    return ctx.step('save', () => $.Audience.create({ name: icp, members: fresh.rows.slice(0, size), from: ctx.action }))
  },
})

Extend it with extends: 'Audience.build' and base(), or compose your own from these steps.

Works with

Plays that order it: Account ads, Benchmark rows, Buying committee, Cold email, Show up in person, LinkedIn outbound, Proof-of-work outreach.

It serves Find your first customers, and runs in Hands-on. It deepens Outbound.

For agents

For agents: the guide, its approvals, retries and lookups
FactValue
GuideAudience.build
Guide approvallimits
GoalFind 50 verified heads of operations (or COO, VP Operations, Director of Property Operations) at US residential property management companies managing roughly 100 to 1,000 units, spend up to $4, 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.
Triggerevery.month(($) => $.Audience.refresh({ audience: 'audiences/audience_31' }))
RetryThree times with backoff, then the next route; the same idempotency key never charges, sends or posts twice
Idempotency keyleaseline-audience-build-1
LookupsA find: input asks unless exactly one record fits
Step
1. Count the market
2. Find the companies and ask each site the fit question
3. Find the people
4. Find each work email
5. Drop wrong-company addresses and find again
6. Check each email
7. Drop who you already know
8. Save the Audience

The amounts live once, on the hub (approvals).

Who does each step, and how it is checked
StepFunctionDoerCheckerAssuranceInputs
1. Count the marketcodeCodeCode checks each result against its declared typemeasured$0.icp
2. Find the companies and ask each site the fit questioncodeCodeCode finds the quoted evidence on the pagemeasured$0.icp, $0.question
3. Find the peoplecodeCodeCode checks each result against its declared typemeasured$2.organizations, $0.titles
4. Find each work emailcodeCodeCode checks each result against its declared typemeasured$3.prospects
5. Drop wrong-company addresses and find againcodeCodeCode compares each address’s domain with the account’s own domain, after redirects, and keeps only a matchmeasured$4.addresses, $2.organizations
6. Check each emailcodeCodeCode checks each result against its declared typemeasured$5.addresses
7. Drop who you already knowcodeCodeCode checks each result against its declared typemeasured$6.valid
8. Save the AudiencecodeCodeCode reads the result back from where it landedmeasured$7.rows, $0.size

The doer is never the checker, and the weakest assurance on the path is the whole call’s.

Under the hood

Under the hood: the Systems, Nouns and Events it touches
KindWhat it touches
SystemsAds
NounsOrganization, Prospect, Contact point, Collection, Audience
EventsCollection.queried, Audience.created

Reference

Inputs, outputs, its Event and its errors
NameTypeDefaultMeaning
icpstringRequiredYour customer in one sentence: who they are, where, how big. Each phrase is a hard filter.
questionstringRequiredAsked of each company’s own site; returns fit and the quote that answers it. Leave it out to keep every match.
titlesstring[]RequiredThe titles to reach at each company, in the buyer’s own words.
sizeinteger100Checked emails wanted. It stops when it has them.
perCompanyinteger1People per company. Two reaches a second buyer when the first does not answer.
accept('valid' | 'catch-all')[]['valid']Which check results count. A catch-all address goes in its own batch only when you add it here.
maxCostnumber2.00The 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.built. Refuses with max-cost before it spends, and with the Data calls and Accounts groups (refusals).

Contents