Skip to content

Paid search

Bids on searches that show buying intent, and runs only while a first sale earns more than it costs to win.

Channel
Ads and sponsorships
Run oneMode try
You, to your agent

Hand yesterday’s search terms report to the Marketing Lead agent and show me what it chose and why. Don’t change anything.

curl "https://api.sb/leaseline.app/events?where=name%20in%20(%22Integration.synced%22%2C%20%22Keyword.measured%22%2C%20%22Invoice.collected%22%2C%20%22Organization.announced%22%2C%20%22Offer.repriced%22%2C%20%22Experiment.matured%22)&limit=1" \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"

# Not live yet (gaps #7)
curl -X POST "https://api.sb/leaseline.app/triggers/fire?mode=try" \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"like":"event_1402"}'

# Not live yet (gaps #7)
curl "https://api.sb/leaseline.app/decisions?where=on%20%3D%20%22batch_44%22" \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"

# Not live yet (gaps #7)
curl "https://api.sb/leaseline.app/actions?where=on%20%3D%20%22batch_44%22" \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"
import { $ } from 'startup-builder'

const { events: [last] } = await $.Event.list({ where: 'name in ("Integration.synced", "Keyword.measured", "Invoice.collected", "Organization.announced", "Offer.repriced", "Experiment.matured")', limit: 1 })   // { play } compiled to this Play’s Trigger Events
const { subject } = await $.Trigger.fire({ like: last.$id }, { mode: 'try' })   // the same Event again, in try mode · Not live yet, gaps #7
const { decisions: [decision] } = await $.Decision.list({ where: `on = "${subject}"` })   // what it chose, and why · Not live yet, gaps #7
const { actions } = await $.Action.list({ where: `on = "${subject}"` })   // what it did, with its checks · Not live yet, gaps #7
return { decision, actions }
npx startup-builder do Event.list --json '{"where":"name in (\"Integration.synced\", \"Keyword.measured\", \"Invoice.collected\", \"Organization.announced\", \"Offer.repriced\", \"Experiment.matured\")","limit":1}'
npx startup-builder do Trigger.fire --json '{"like":"event_1402"}' --try   # Not live yet (gaps #7)
npx startup-builder do Decision.list --json '{"where":"on = \"batch_44\""}'   # Not live yet (gaps #7)
npx startup-builder do Action.list --json '{"where":"on = \"batch_44\""}'   # Not live yet (gaps #7)
{
  "name": "do",
  "arguments": {
    "code": "const { events: [last] } = await $.Event.list({ where: 'name in (\"Integration.synced\", \"Keyword.measured\", \"Invoice.collected\", \"Organization.announced\", \"Offer.repriced\", \"Experiment.matured\")', limit: 1 })   // { play } compiled to this Play’s Trigger Events\nconst { subject } = await $.Trigger.fire({ like: last.$id }, { mode: 'try' })   // the same Event again, in try mode · Not live yet, gaps #7\nconst { decisions: [decision] } = await $.Decision.list({ where: `on = \"${subject}\"` })   // what it chose, and why · Not live yet, gaps #7\nconst { actions } = await $.Action.list({ where: `on = \"${subject}\"` })   // what it did, with its checks · Not live yet, gaps #7\nreturn { decision, actions }",
    "mode": "try"
  }
}
Response200application/json
{
  "decision": {
    "$id": "https://api.sb/leaseline.app/decisions/decision_581",
    "$type": "Decision",
    "choice": "block-wasted-terms",
    "reason": "31 of 214 clicked terms match the negative rules in your file (jobs, free, template, login), and they cost $86 yesterday",
    "stakes": "low",
    "options": [
      "block-wasted-terms",
      "queue-new-terms",
      "reset-bids",
      "take-the-next-step",
      "defend-your-name",
      "not-now"
  …
sample · 4 Not live yetPreview

On PATCH /{namespace}/campaigns/{id}Off PATCH /{namespace}/campaigns/{id}Take this one POST /{namespace}/tasks/{task}/claim

action_2 adds the 31 negatives through the write proxy at once, because your rules cover them. action_3 queues 6 terms no rule covers for your OK in today’s batch; a batch waiting 48 hours halves the fenced arm’s budget.

+13.6 per $100 · ■■□□ Early sign · n 56 days · path → arr

Try another: the terms reportChoiceReason
Oct 18 terms report: 31 of 214 clicked terms match your negative rulesblock-wasted-termsyour rules cover them: blocked at once
“renewal notice software for landlords”: 9 clicks, 2 trials, no Keyword yetqueue-new-termsa new term no rule covers: for your OK
The Bing probe’s 28-day switchback: +15.1 ICP Visits per $100, breakeven at 8take-the-next-stepabove breakeven: open Google Search at $1,000, with your OK
A rival started bidding on “leaseline”defend-your-namea rival on your name: one brand ad group, for your OK
Google Search, day 14 of its holdout: +9.2 [−6.0, +24.4]not-nowthe holdout has not read: spend holds where it is

Where it sits: Stranger → Lead → Trialing → Activated → At risk → Churned → Won back · Search ICP Visits per $100 → People reached → New Customers ↑ → ARR added each week → ARR

Worth’s formula: about +13.6 incremental ICP Visits per $100 (your switchback, ■■□□ Early sign) × $1,100 a month = 150 Visits × 0.02 Visits to paid × $347 Pro ARR ≈ +$1,040. Under First signal: A day switchback at $1,100 a month tells a +10 lift per $100 in about 8 weeks; each scale step waits for a 28-day holdout read.

sample · api.sb/leaseline.app · 41 Customers at 2026-10-19 · on Integration.synced · negatives applied 6 min after the terms report · 9 keys Not live yet

Start it this week

Terminal
npx startup-builder add play paid-search   # 1 · writes plays/paid-search.mdx (Not live yet, gaps #5)

# 2 · dry run; below, its 422 on 2026-08-24
curl -X POST "https://api.sb/leaseline.app/campaigns?mode=try" \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"play":"paid-search"}'

# 3 · keep it: every change waits for your OK
curl -X POST https://api.sb/leaseline.app/campaigns \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"play":"paid-search"}'

# the probe’s $100 monthly Limit
curl -X POST https://api.sb/leaseline.app/limits \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"paid-search-month","amount":100,"period":"month"}'

# off: every Campaign pauses through the proxy, and write scope is revoked
curl -X PATCH https://api.sb/leaseline.app/campaigns/paid-search \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"endsAt":"now"}'
422Refusedpaid-search can’t start yet: 2 checks fail and 1 warns.
CheckFixThen
use-when pass · Not live yet, gaps #8Nothing to do: it counts searches for your exact-intent terms.POST /{namespace}/keywords/{keyword}/measure
baseline warn · Not live yet, gaps #8n 0: the probe’s switchback becomes the baseline in 28 days.POST /{namespace}/metrics/query
billing-connected fail · Not live yet, gaps #8Connect Stripe, so every gate reads first sales from your own records.POST /{namespace}/integrations/connect
fact-check fail · Not live yet, gaps #1Approve the claims ads may state; every ad line must cite one.POST /{namespace}/content/draft
role-unstaffed pass · Not live yet, gaps #8Nothing to do: a founder-only Startup approves every change.POST /{namespace}/roles/{role}/staff
JSON
{
  "type": "https://startupbuilder.dev/errors/refused",
  "title": "Refused",
  "status": 422,
  "detail": "paid-search can’t start yet: 2 checks fail and 1 warns.",
  "checks": [
    {
      "check": "use-when",
      "status": "pass",
      "observed": "1,900 exact-intent searches a month across 14 terms",
      "doc": "/plays/paid-search#check-use-when",
      "fix": "Nothing to do: it counts searches for your exact-intent terms.",
      "then": "Keyword.measure"
    },
    {
      "check": "baseline",
      "status": "warn",
      "observed": "0 days of paid spend: no switchback yet",
      "doc": "/plays/paid-search#check-baseline",
      "fix": "n 0: the probe’s switchback becomes the baseline in 28 days.",
      "then": "Metric.query"
    },
    {
      "check": "billing-connected",
      "status": "fail",
      "doc": "/plays/paid-search#check-billing-connected",
      "fix": "Connect Stripe, so every gate reads first sales from your own records.",
      "then": "Integration.connect"
    },
    {
      "check": "fact-check",
      "status": "fail",
      "observed": "no claims source for ad text",
      "doc": "/plays/paid-search#check-fact-check",
      "fix": "Approve the claims ads may state; every ad line must cite one.",
      "then": "Content.draft"
    },
    {
      "check": "role-unstaffed",
      "status": "pass",
      "observed": "every approval comes to you",
      "doc": "/plays/paid-search#check-role-unstaffed",
      "fix": "Nothing to do: a founder-only Startup approves every change.",
      "then": "Role.staff"
    }
  ]
}

It says no, says why, and names the fix.

CheckFix (one call)Your timeWait
use-when · baseline · pass: 1,900 searches a month; warn: no switchback yet (gaps #8)POST /{namespace}/keywords/{keyword}/measure the 14 terms; POST /{namespace}/metrics/query fills Before as the probe runs2 min28 days for the first read
billing-connected · failPOST /{namespace}/integrations/connect with system recurring-billing (Stripe)5 minthe first sync, about 10 min
fact-check · fail (Not live yet)POST /{namespace}/content/draft the claims source from your Pricing Page and approved Pages20 minnone
role-unstaffed · pass: 0 staffing fails on a founder-only Startupnothing; every change comes to you0 minnone

Your number · Before 0 · Now 13.6 per $100 · Target 12 by Dec 31 · ■■□□ Early sign · n 56 days · Other Startups +9.0, ■■■□ Likely

The probe spends $100 a month on Bing, and Google Search opens at $1,000 a month only after a winning read; the Play itself costs about $0.40 a day to read reports and write through the proxy (Worker cost). That comes to $112 a month on the probe and about $1,112 after the step.

When it starts

WhenTrigger idStarts onWindowHow it gets in
The Venue reports the terms people clicked since the last readsearch-term-batchevent on Integration.synced where report = 'search-terms'read each morning; a batch for you within 48 hPOST /{namespace}/integrations/connect with venue bing or google-search
A Customer’s first sale follows a paid Visitpaid-first-transactionevent on Invoice.collected where first = true and visit.paid = true30dStripe, through POST /{namespace}/integrations/connect; never the Venue’s own count
A switchback or geo holdout is ready to readholdout-closedevent on Experiment.matured where method in ('geo', 'switchback')7d to decide the next stepPOST /{namespace}/experiments/{experiment}/start set the dates
+ 4 more: a new intent phrase, a rival on your name, a rival’s moment, and a change to your margin (?lanes=all)
One signed delivery · StartupBuilder-Signature
{
  "$id": "https://api.sb/leaseline.app/events/event_1402",
  "$type": "Event",
  "name": "Integration.synced",
  "at": "2026-10-19T06:04:30Z",
  "seq": 1402,
  "data": {
    "integration": "google-search",
    "report": "search-terms",
    "batch": {
      "$id": "https://api.sb/leaseline.app/batches/batch_44",
      "day": "2026-10-18",
      "clicked": 214,
      "spend": 52.4
    },
    "negativeMatches": 31,
    "wasted": 86
  }
}
Reproduce it in test
npx startup-builder trigger Integration.synced --like event_1402   # replays the report in test; the proxy writes nothing live
npx startup-builder clock advance --by 48h   # halves the fenced arm’s budget if the batch still waits
npx startup-builder listen Invoice.collected --forward-to http://localhost:3000/hook

Who runs it

Marketing Lead · your agent (role:marketing-lead, worker_6) holds the Key Result and writes to the ads Venues only through the fenced proxy, which holds the token outside the model and creates every ad paused. You (Dana Okafor, person, worker_1) approve new terms, every spend step and every brand ad. Your number links to Did it work.

OptionRung todayEvidence (passed of n)Moves up atDrops back whenYou can
Block the wasted terms · Reset bids to breakevenIt does it, you can stop it412 negatives · 3 resetsmet on day 21 (95 % of 30 change sets passed unedited)a drift or fence-breach → You approve each onePOST /{namespace}/actions/{action}/cancel before the proxy writes
Queue new terms for your OK · Bid on your own nameYou approve each one44 of 52 approved · 1 of 1capped at It does it, you can stop it, after 30 batches—POST /{namespace}/approvals/{approval}/approve · POST /{namespace}/approvals/{approval}/refuse
Take the next spend stepYou approve each one1 of 1never: a spend step is yours—POST /{namespace}/approvals/{approval}/approve, then POST /{namespace}/limits/{limit}/raise
Not nowIt does it4 of 4——POST /{namespace}/actions/{action}/redirect
Change a negative ruleYou approve each one1 merged of 1never—POST /{namespace}/changes/{change}/merge · POST /{namespace}/changes/{change}/revert
Never: a rival’s name in ad text, a review quote you didn’t verify, the Venue’s auto-made assetsRefused, every time1 drafted, 1 refusednever—none: the Check holds it

What it reads · What it may call. It reads the terms report, spend, your Offers’ margin and first sales from Stripe, never clicks or the Venue’s own credit. A search term is data: it never enters ad text and never instructs the agent. Its tools are POST /{namespace}/keywords, POST /{namespace}/approvals, PATCH /{namespace}/ads/{id}, POST /{namespace}/limits/{limit}/raise, POST /{namespace}/experiments/{experiment}/start, POST /{namespace}/ads, and any other call becomes an Approval under the Role’s Mandate.

The brief (folded): compiled Agent-register Markdown · play:paid-search@1 · each line names its record key
You hold the Marketing Lead Key Result: 12 incremental ICP Visits per $100 by 2026-12-31. ← worker, metric
On each report, sale, margin change or holdout, choose one option and say why.   ← triggers, chooses
Every bid stays at or under breakeven for a first sale.                          ← Payback gate, your file line 5
A Price in ad text equals the Offer’s live Price; no urgency the Offer lacks.    ← copyRules 2, 4
A rival’s name is a Keyword, never ad text.                                      ← copyRules 3
No spend step without a winning holdout read and Dana’s OK.                      ← Scale gate, chooses.take-the-next-step

The slow loop, as a Change. decision_396 (2026-09-21, weekly) read $142 on terms with “template” and “free” over 3 weeks, with 0 first sales. change_28 added both words to the negative rules (your file, line 6), and Dana merged it with POST /{namespace}/changes/{change}/merge. Replayed on past reports, POST /{namespace}/changes/{change}/review cut wasted spend from 18 % to 7 %; read back over the next 14 days it is 8 %, n 2,960 clicks, ■■□□ Early sign (Example, day 42).

Bring your own agent (folded) starts at It does it, you can stop it when it matches or beats the seated agent on the eval suite, and at You approve each one when it does not; it still writes only through the proxy.

Terminal
npx startup-builder eval --scenario paid-search-56 --agent "claude -p" --seat marketing-lead   # seated: 8 % wasted, 0 Checks failed sent · yours: 11 % wasted → You approve each one

What it can do next

OptionPicked whenDecided byRunsHands toIf it’s just you
Block the wasted terms block-wasted-termsclicked terms match a negative ruleYour rule (line 6)POST /{namespace}/keywords as negatives, through the proxynobodythe same
Queue new terms for your OK queue-new-termsa term or negative no rule coversAgent’s call, with its reasonPOST /{namespace}/keywords, paused → POST /{namespace}/approvals in today’s batchyou approve the batch · Take this oneyou
Reset bids to breakeven reset-bidsan Offer’s Price or cost changedYour rule (line 5)PATCH /{namespace}/ads/{id} to the new breakeven bidsnobodythe same
Take the next spend step take-the-next-stepa holdout read beats breakevenAgent’s call, with its reasonPOST /{namespace}/approvals → on yes, POST /{namespace}/limits/{limit}/raise → POST /{namespace}/experiments/{experiment}/start for the next 28-day readyou approve it · Take this oneyou
Bid on your own name defend-your-namea rival bids on your nameAgent’s call, with its reasonPOST /{namespace}/ads for one brand ad group, paused → POST /{namespace}/approvalsyou approve it · Take this oneyou
Not now not-nowthe holdout has not read, or the payback gate is near its lineAgent’s call, with its reasonnothing; names what holds it: the open Experiment or the payback gatenobodythe same
When they write backFixture replyWhat happensWho answersBy whenCall
Interesteda demo form from an ad click: “340 units, can we see it?”the form starts instant-response, which answers and books; this Play only counts the paid VisitSDR · your agent, then AE · Priya Shah5 minPOST /{namespace}/leads
Send me pricinga click on the pricing ad, then the Pricing Pagethe ad’s Price equals the live Price; nothing follows themnobody—GET /{namespace}/decisions
Not nowa click, then no form and no trialnothing follows; retargeting runs only in its own Playnobody—GET /{namespace}/decisions
Wrong personclicks on “leaseline login” from Customersa rule negative blocks the term the next morning; Customers see no ad for itMarketing Lead · your agentthe next reportPOST /{namespace}/keywords
Out of officeclicks on days the switchback holds ads offthose days are the comparison; no ad showsnobody—GET /{namespace}/experiments/{id}
Question it can’t answera chat from an ad click: “Do you integrate with our accounting system?”the chat starts instant-response, which hands it to a personAE · Priya Shah (person)60 minPOST /{namespace}/conversations/{conversation}/escalate
Stopan ad-preferences block on the Venuethe Venue stops showing the ad to them; no list of them is keptnobodyat onceGET /{namespace}/decisions

When it hands to you

Every new term batch, spend step and brand ad comes to you: the Marketing Lead Role has no person, and a spend step is never the agent’s call. One Brief, as captured, then the Approval (brief, draft and checks are Not live yet, gaps #7).

What lands in each queue · sample
[
  {
    "$id": "https://api.sb/leaseline.app/tasks/task_427",
    "$type": "Task",
    "assignee": "worker_1",
    "due": "2026-09-30T17:00:00Z",
    "on": "experiment_6",
    "brief": {
      "what": "the Bing probe’s 28-day switchback",
      "read": "+15.1 incremental ICP Visits per $100 [+4.2, +26.0], breakeven 8",
      "chose": "take-the-next-step",
      "why": "above breakeven on a winning read",
      "step": "open Google Search at $1,000 a month",
      "never": [
        "a raise without your OK",
        "the fenced arm before 30 first sales"
      ]
    }
  },
  {
    "$id": "https://api.sb/leaseline.app/approvals/approval_96",
    "$type": "Approval",
    "verdict": null,
    "deadline": "2026-10-21T06:00:00Z",
    "draft": "6 new terms, paused: “renewal notice software for landlords”, “owner statement software”…",
    "route": "queue-new-terms",
    "checks": [
      {
        "check": "fact-check",
        "status": "pass"
      },
      {
        "check": "scale-gate",
        "status": "pass"
      }
    ]
  }
]

You answer with POST /{namespace}/approvals/{approval}/approve or POST /{namespace}/approvals/{approval}/refuse, or POST /{namespace}/tasks/{task}/claim (Take this one) and POST /{namespace}/tasks/{task}/return with a reason, each a one-call window. A term batch unanswered for 48 hours halves the fenced arm’s budget, and at 96 hours the arm pauses (negative-backlog); an unanswered spend step simply never happens.

On time: you, 7 of 8 batches answered within 48 h, median 9 h, n 8, and 1 of 1 spend steps within its day, ■■□□ Early sign beside Other Startups’ 88 % on time. Your queue: counted by kind: in week 1, the account plan and 3 batches; now about 2 batches a week, and a spend step at each holdout read.

The fixed line: POST /{namespace}/deals/{deal}/assign sets owner to the receiving person, the agent is never a Deal’s owner, and Meetings land on the owner’s calendar under the owner’s name.

When it won’t send

Check (refuses one send)RefusesFixThen
fact-check (Not live yet)ad text with a claim the claims source lacks, or a Price that differs from the Offer’s live PriceCut the claim, or name the record that backs it.POST /{namespace}/content/draft
scale-gate (the record’s Scale gate)a Limit raise, a new arm or a new Venue with no winning holdout readRun the 28-day holdout first; only the probe’s step reads on the probe.POST /{namespace}/experiments/{experiment}/start
Stop (pauses the Play)Pauses whenThen
paybacka 14-day cost per first sale above first-sale margin, at 5 or more first salesthe Play pauses
zero-transaction14-day spend over 3 times the margin with no first salethe Play pauses
ad-groupan ad group spends 3 times breakeven with no first salethat ad group pauses
spend-limitmonth-to-date spend reaches the monthly Limitthe Play pauses until the month turns; POST /{namespace}/limits/{limit}/raise needs a winning read
conversion-feeda failed conversion send, or none for 3 times the expected gap, 72 h at leastbid changes freeze; at 7 times the gap the Play pauses
feed-mismatchthe Venue’s first sales differ from Stripe’s by over 30 % for 7 daysbid changes freeze
drifta live ad or setting differs from the approved stateit pauses or resets at once; a second drift in 7 days pauses the Campaign
landinga landing Page fails its status or Price checkits ad groups pause
fence-breach3 refused writes through the proxy in one daythe Play drops to read-only
kill-switchyou press itevery Campaign pauses through the proxy, and write scope is revoked
negative-backloga term batch waits 48 h for youthe fenced arm’s budget halves; at 96 h the arm pauses
fenced-armthe fenced arm is marked the loserit pauses at once and stays off until a new Decision

The captured refusal, then the Stop that fired as a Campaign.paused reason (Not live yet, gaps #2); the rewrite that went live is the Sep 8 ad in Example.

422RefusedThe new ad for “lease renewal software” was not created: one headline states a result the claims source lacks.
CheckFixThen
fact-check fail · Not live yet, gaps #1Cut the claim, or name the record that backs it.POST /{namespace}/content/draft
scale-gate pass—POST /{namespace}/metrics/query
JSON
{
  "type": "https://startupbuilder.dev/errors/refused",
  "title": "Refused",
  "status": 422,
  "detail": "The new ad for “lease renewal software” was not created: one headline states a result the claims source lacks.",
  "checks": [
    {
      "check": "fact-check",
      "status": "fail",
      "doc": "/plays/paid-search#check-fact-check",
      "fix": "Cut the claim, or name the record that backs it.",
      "then": "Content.draft"
    },
    {
      "check": "scale-gate",
      "status": "pass"
    }
  ]
}

It says no, says why, and names the fix.

The Stops that fired, as Campaign.paused reasons · sample
[
  {
    "reason": {
      "limit": "conversion-feed",
      "observed": "0 conversions sent in 81 h",
      "bound": "72 h (3 × the 24 h gap)",
      "window": "Oct 2 to Oct 5"
    },
    "doc": "/plays/paid-search#stop-conversion-feed",
    "task": "task_433"
  }
]

Did it work

Days were assigned at random, ads on or off (a switchback, rule o: 28 days each half); both halves are read from that assignment. Each figure’s call is POST /{namespace}/metrics/query by node and by arm; the parent field and every node above the leaf are Not live yet (gaps #4).

+13.6 per $100 · ■■□□ Early sign · n 56 days · path → arr

Chose
block-wasted-terms
Why
31 of 214 clicked terms match the negative rules in your file (jobs, free, template, login), and they cost $86 yesterday
From
block-wasted-terms · queue-new-terms · reset-bids · take-the-next-step · defend-your-name · not-now
Expects
wasted spend under 10 % of the next 7 days
Reopens when
wasted share over 10 % in the next Integration.synced terms report
By
worker_6
JSON
{
  "$id": "decisions/decision_581",
  "$type": "Decision",
  "choice": "block-wasted-terms",
  "reason": "31 of 214 clicked terms match the negative rules in your file (jobs, free, template, login), and they cost $86 yesterday",
  "stakes": "low",
  "options": [
    "block-wasted-terms",
    "queue-new-terms",
    "reset-bids",
    "take-the-next-step",
    "defend-your-name",
    "not-now"
  ],
  "by": "worker_6",
  "on": "batch_44",
  "expected": "wasted spend under 10 % of the next 7 days",
  "reopensWhen": "wasted share over 10 % in the next Integration.synced terms report"
}
NodeAds-on daysAds-off daysAddedConfidenceBasis
search-icp-visits-per-1001,140 ICP Visits on $1,180 spend980 ICP Visits, $0 spend+13.6 incremental ICP Visits per $100 [+4.1, +23.1]■■□□ Early signdays switched at random, 28 a half · n 56 days · Other Startups +9.0, ■■■□ Likely, n 47 Startups
icp-reached1,140 a half (285 a week)980 (245 a week)+160 [+48, +272]■■□□ Early signthe same assignment
new-arr ↑$1,388, 4 Customers from paid Visits · Booked: credited, not all caused by this Play$347, 1 Customer+$1,041 [−$700, +$2,800] · Not carrying up yet · Worth about +$1,000 a month■□□□ Too early to tell (4 vs 1 Customers)the same · 6 trials still deciding
net-new-arr · arr+$1,388+$347+$1,041, carried: it moves no other bridge term■□□□ Too early to tellcarried from new-arr

The leaf counts ICP Visits on ads-on days minus ads-off days, for each $100 spent, over the switchback’s 56 days; the Google Search step on day 35 keeps the same day assignment, and its own 28-day holdout reads on Nov 22.

How it moves it: ads showed to people searching your exact-intent terms at bids at or under breakeven, and 412 wasted terms were blocked the next morning (search-icp-visits-per-100 by term).

Guardrails: treated against held back: cost per first sale, $295 against a $347 margin, and Visits to paid, 0.35 % against 0.10 %.

The Campaign card · sample
{
  "$id": "https://api.sb/leaseline.app/campaigns/paid-search",
  "$type": "Campaign",
  "play": "play:paid-search@1",
  "path": [
    "search-icp-visits-per-100",
    "icp-reached",
    "new-arr",
    "net-new-arr",
    "arr"
  ],
  "experiment": {
    "metric": "search-icp-visits-per-100",
    "unit": "per-100-usd",
    "heldBack": 28,
    "decision": null
  },
  "readout": {
    "value": 13.6,
    "n": 56,
    "maturing": 0,
    "interval": [
      4.1,
      23.1
    ],
    "readability": "B",
    "basis": "alternating-weeks",
    "pooled": false
  },
  "ledger": {
    "spend": 1180,
    "workerCost": 22,
    "hours": 5.4,
    "pipelineArr": 2400,
    "wonArr": 1388,
    "projectedArr": 1040,
    "paybackDays": 310
  }
}

Outside benchmark: the catalog holds none for this Play yet, so Other Startups’ results stand in: +9.0 incremental ICP Visits per $100, ■■■□ Likely, n 47 Startups.

Before it · After it. Before it: editorial-seo and aeo find the exact-intent terms worth a bid. After it: every form, chat or email from a paid Visit starts instant-response, and retargeting hears the Visitors who left without one.

Example

Leaseline ran it from 2026-08-24 to 2026-10-19 (56 days): the Bing probe from day 0, Google Search from day 35 at $1,000 a month. $1,180 spent on 28 ads-on days; 412 negatives blocked and 44 new terms approved. Read every captured thread at ?threads=all; three follow the tables.

What happened, by optionDecisionsDoneWon (credited, not all caused by this Play)
Block the wasted terms · Reset bids to breakeven38 · 3412 negatives · 3 resets—
Queue new terms for your OK8 batches44 of 52 approved3 on Pro from approved terms (+$1,041 ARR)
Take the next spend step · Bid on your own name · Not now1 · 1 · 4Google Search opened · 1 brand ad group · spend held 4 times1 on Pro (+$347 ARR) · 0 · —
At 500 searches a month (the Use it when volume)about $300 a month of spendswitchback: Too early to tell at 90 daysOther Startups’ results: +9.0 per $100, ■■■□ Likely, n 47 Startups
DayEventWhat happenedYour time
0event_118 Campaign.createdthe three steps; the dry run fails billing-connected and fact-check35 min
0event_121 Integration.connectedStripe connected; the claims source approved with 9 claims25 min
9event_230 Invoice.collectedthe first sale from a paid Visit: a probe click on “lease renewal software”—
21event_420 Role.updatedpromoted: rule negatives and bid resets apply without waiting—
28 · 42event_560 Change.merged · event_744 Readout.created“template” and “free” added to the negative rules: replayed at 7 % wasted, read back at 8 %5 min
35event_640 Limit.raisedthe probe’s switchback read +15.1 per $100; you approved Google Search at $1,000 a month10 min
39event_702 Campaign.pausedconversion-feed: no conversion sent for 81 h; the send was fixed and bids unfroze20 min
1–56event_140 Approval.created, first of 9you approved 8 term batches and 1 spend step; weekly reviews, from the POST /{namespace}/tasks/next log3 h 40 min · total 5.4 h measured, 5 h asked

Search: “lease renewal software”, Sep 2, 10:14 · the ad: Lease renewals, sent 60 days out · Per-owner notices and reports · Pro from $29 a month.¹

The click · landed on the renewals Page; a trial started at 10:22 and paid on Pro on Sep 16. Stripe recorded the first sale, and the conversion send reached the Venue at 10:24 the next day.²

The “lease renewal software” ad, 2026-09-08 · fact-check refused, then the version that went live
- Cut late renewals by 40%
+ Renewal notices sent 60 days out

The auction report, 2026-10-06 · a rival began bidding on “leaseline” · the agent drafted one brand ad group, paused, and the Approval went to Dana with a deadline of Oct 7 at 06:00 (approval_91).³ Dana approved it at 21:40 on Oct 6, inside the deadline, and the ad group went live through the proxy at 21:41.

¹ The ad’s Price equals Pro’s live Price; no rival name in ad text. ² Every gate counts the sale from Stripe, never the Venue’s own credit. ³ A brand ad group is yours to approve, on every rung.

Your settings

plays/paid-search.mdx
---
limit: 1000       # dollars a month; it was 100 for the probe
holdout: switchback-days
---
Bids stay at or under breakeven for a first sale.
Block terms with jobs, free, template or login the next morning.
No spend step without a holdout read and my OK.
Text
$ npx startup-builder show plays/paid-search
limit $1,000 a month · day switchback · breakeven bids · 4 negative rules · spend steps to you     your file  plays/paid-search.mdx:2,3,5,6,7 (Ask 2026-08-24; line 2 by your OK on day 35, line 6 by change_28, 2026-09-21)
family ads · worker role:marketing-lead, owns never · chooses 6 (2 rule, 4 read) · approval every-send · checks fact-check, scale-gate · stops 12 · placements google-search, bing, through the write proxy    catalog  play:paid-search@1 (Not live yet, gaps #5)

Every key from the catalog

KeyIdWhat it is
Tacticsearch-account-buildSearch account build researches intent phrases and queues a complete, read-only account plan of ad groups, ads, negatives and breakeven bids. Starts at You approve each one.
Tacticad-write-proxyAd write proxy is the only write path to an ads Venue, holding the token outside the model and forcing every create to paused. Starts at You approve each one.
Tacticsearch-term-triageSearch term triage reads every clicked search term daily, applies rule negatives at once, and batches model-proposed negatives and Keywords for a human. Starts at You approve each one.
Tacticfenced-match-armFenced match arm runs the Venue’s automated matching in its own Campaign, capped at a fifth of the budget and walled off from core Keywords. Starts at You approve each one.
Tacticincrementality-holdoutIncrementality holdout measures the first Transactions a paid arm adds, by a geo holdout or a day switchback, before any scale step. Starts at You approve each one.
Tacticpayback-gatePayback gate pauses a paid Play the moment its cost per first Transaction outruns first-transaction margin, with no approval and no wait. Starts at Sends nothing.
Tacticad-drift-monitorAd drift monitor diffs every live Ad’s rendered creative, text and enhancement settings against the approved source daily, and pauses or resets what drifted. Starts at Sends nothing.
Triggersearch-term-batchSearch term batch: event on Integration.synced, window 7d.
Triggerintent-phrase-foundIntent phrase found: event on Keyword.measured, window 30d.
Triggerpaid-first-transactionPaid first transaction: event on Invoice.collected, window 30d.
Triggerrival-on-brandRival on brand: event on Integration.synced, window 7d.
Triggerrival-momentRival moment: event on Organization.announced, window 14d.
Triggermargin-changeMargin change: event on Offer.repriced, window 1d.
Triggerholdout-closedHoldout closed: event on Experiment.matured, window 7d.

Compiled, not written: edit the Play record paid-search and its Leaseline run, then regenerate. Every step runs from a sentence or a line of code. In your words: the CRM words you know, and where each lives here.