Paid search
Bids on searches that show buying intent, and runs only while a first sale earns more than it costs to win.
- Step
- Get found
- Channel
- Ads and sponsorships
- Summary
- startupbuilder.dev
Be our Marketing Lead for search ads: start with a $100 exact-match probe on Bing, block wasted terms every morning, and keep every bid under what a first sale is worth. No spend step without a holdout read and my OK. Show me what’s missing; don’t spend.
# Not live yet (gaps #2)
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"}'import { $ } from 'startup-builder'
return await $.Campaign.create({ play: 'paid-search' }, { mode: 'try' }) // dry run on the day-0 Startup, 2026-08-24 · Not live yet, gaps #2
npx startup-builder do Campaign.create --json '{"play":"paid-search"}' --try # Not live yet (gaps #2){
"name": "do",
"arguments": {
"code": "return await $.Campaign.create({ play: 'paid-search' }, { mode: 'try' }) // dry run on the day-0 Startup, 2026-08-24 · Not live yet, gaps #2",
"mode": "try"
}
}application/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"
},
…
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"
}
}application/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 yetPreviewOn 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 report | Choice | Reason |
|---|---|---|
| Oct 18 terms report: 31 of 214 clicked terms match your negative rules | block-wasted-terms | your rules cover them: blocked at once |
| “renewal notice software for landlords”: 9 clicks, 2 trials, no Keyword yet | queue-new-terms | a new term no rule covers: for your OK |
| The Bing probe’s 28-day switchback: +15.1 ICP Visits per $100, breakeven at 8 | take-the-next-step | above breakeven: open Google Search at $1,000, with your OK |
| A rival started bidding on “leaseline” | defend-your-name | a 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-now | the holdout has not read: spend holds where it is |
- Day 9: first sale from a paid Visit
- First signal
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/
Start it this week
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"}' | Check | Fix | Then |
|---|---|---|
use-when pass · Not live yet, gaps #8 | Nothing to do: it counts searches for your exact-intent terms. | POST /{namespace}/keywords/{keyword}/measure |
baseline warn · Not live yet, gaps #8 | n 0: the probe’s switchback becomes the baseline in 28 days. | POST /{namespace}/metrics/query |
billing-connected fail · Not live yet, gaps #8 | Connect Stripe, so every gate reads first sales from your own records. | POST /{namespace}/integrations/connect |
fact-check fail · Not live yet, gaps #1 | Approve the claims ads may state; every ad line must cite one. | POST /{namespace}/content/draft |
role-unstaffed pass · Not live yet, gaps #8 | Nothing 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.
| Check | Fix (one call) | Your time | Wait |
|---|---|---|---|
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 runs | 2 min | 28 days for the first read |
billing-connected · fail | POST /{namespace}/integrations/connect with system recurring-billing (Stripe) | 5 min | the first sync, about 10 min |
fact-check · fail (Not live yet) | POST /{namespace}/content/draft the claims source from your Pricing Page and approved Pages | 20 min | none |
role-unstaffed · pass: 0 staffing fails on a founder-only Startup | nothing; every change comes to you | 0 min | none |
Your number · Before 0 · Now 13.6 per $100 · Target 12 by Dec 31 · ■■□□ Early sign · n 56 days · Other Startups +9.0,
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
| When | Trigger id | Starts on | Window | How it gets in |
|---|---|---|---|---|
| The Venue reports the terms people clicked since the last read | search-term-batch | event on Integration.synced where report = 'search-terms' | read each morning; a batch for you within 48 h | POST /{namespace}/integrations/connect with venue bing or google-search |
| A Customer’s first sale follows a paid Visit | paid-first-transaction | event on Invoice.collected where first = true and visit.paid = true | 30d | Stripe, through POST /{namespace}/integrations/connect; never the Venue’s own count |
| A switchback or geo holdout is ready to read | holdout-closed | event on Experiment.matured where method in ('geo', 'switchback') | 7d to decide the next step | POST /{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=) |
{
"$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
}
}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:, 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.
| Option | Rung today | Evidence (passed of n) | Moves up at | Drops back when | You can |
|---|---|---|---|---|---|
| Block the wasted terms · Reset bids to breakeven | It does it, you can stop it | 412 negatives · 3 resets | met on day 21 (95 % of 30 change sets passed unedited) | a drift or fence-breach → You approve each one | POST /{namespace}/actions/{action}/cancel before the proxy writes |
| Queue new terms for your OK · Bid on your own name | You approve each one | 44 of 52 approved · 1 of 1 | capped 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 step | You approve each one | 1 of 1 | never: a spend step is yours | — | POST /{namespace}/approvals/{approval}/approve, then POST /{namespace}/limits/{limit}/raise |
| Not now | It does it | 4 of 4 | — | — | POST /{namespace}/actions/{action}/redirect |
| Change a negative rule | You approve each one | 1 merged of 1 | never | — | 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 assets | Refused, every time | 1 drafted, 1 refused | never | — | 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.
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.
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
| Option | Picked when | Decided by | Runs | Hands to | If it’s just you |
|---|---|---|---|---|---|
Block the wasted terms block-wasted-terms | clicked terms match a negative rule | Your rule (line 6) | POST /{namespace}/keywords as negatives, through the proxy | nobody | the same |
Queue new terms for your OK queue-new-terms | a term or negative no rule covers | Agent’s call, with its reason | POST /{namespace}/keywords, paused → POST /{namespace}/approvals in today’s batch | you approve the batch · Take this one | you |
Reset bids to breakeven reset-bids | an Offer’s Price or cost changed | Your rule (line 5) | PATCH /{namespace}/ads/{id} to the new breakeven bids | nobody | the same |
Take the next spend step take-the-next-step | a holdout read beats breakeven | Agent’s call, with its reason | POST /{namespace}/approvals → on yes, POST /{namespace}/limits/{limit}/raise → POST /{namespace}/experiments/{experiment}/start for the next 28-day read | you approve it · Take this one | you |
Bid on your own name defend-your-name | a rival bids on your name | Agent’s call, with its reason | POST /{namespace}/ads for one brand ad group, paused → POST /{namespace}/approvals | you approve it · Take this one | you |
Not now not-now | the holdout has not read, or the payback gate is near its line | Agent’s call, with its reason | nothing; names what holds it: the open Experiment or the payback gate | nobody | the same |
| When they write back | Fixture reply | What happens | Who answers | By when | Call |
|---|---|---|---|---|---|
| Interested | a 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 Visit | SDR · your agent, then AE · Priya Shah | 5 min | POST /{namespace}/leads |
| Send me pricing | a click on the pricing ad, then the Pricing Page | the ad’s Price equals the live Price; nothing follows them | nobody | — | GET /{namespace}/decisions |
| Not now | a click, then no form and no trial | nothing follows; retargeting runs only in its own Play | nobody | — | GET /{namespace}/decisions |
| Wrong person | clicks on “leaseline login” from Customers | a rule negative blocks the term the next morning; Customers see no ad for it | Marketing Lead · your agent | the next report | POST /{namespace}/keywords |
| Out of office | clicks on days the switchback holds ads off | those days are the comparison; no ad shows | nobody | — | GET /{namespace}/experiments/{id} |
| Question it can’t answer | a chat from an ad click: “Do you integrate with our accounting system?” | the chat starts instant-response, which hands it to a person | AE · Priya Shah (person) | 60 min | POST /{namespace}/conversations/{conversation}/escalate |
| Stop | an ad-preferences block on the Venue | the Venue stops showing the ad to them; no list of them is kept | nobody | at once | GET /{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).
[
{
"$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) | Refuses | Fix | Then |
|---|---|---|---|
fact-check (Not live yet) | ad text with a claim the claims source lacks, or a Price that differs from the Offer’s live Price | Cut 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 read | Run the 28-day holdout first; only the probe’s step reads on the probe. | POST /{namespace}/experiments/{experiment}/start |
| Stop (pauses the Play) | Pauses when | Then |
|---|---|---|
payback | a 14-day cost per first sale above first-sale margin, at 5 or more first sales | the Play pauses |
zero-transaction | 14-day spend over 3 times the margin with no first sale | the Play pauses |
ad-group | an ad group spends 3 times breakeven with no first sale | that ad group pauses |
spend-limit | month-to-date spend reaches the monthly Limit | the Play pauses until the month turns; POST /{namespace}/limits/{limit}/raise needs a winning read |
conversion-feed | a failed conversion send, or none for 3 times the expected gap, 72 h at least | bid changes freeze; at 7 times the gap the Play pauses |
feed-mismatch | the Venue’s first sales differ from Stripe’s by over 30 % for 7 days | bid changes freeze |
drift | a live ad or setting differs from the approved state | it pauses or resets at once; a second drift in 7 days pauses the Campaign |
landing | a landing Page fails its status or Price check | its ad groups pause |
fence-breach | 3 refused writes through the proxy in one day | the Play drops to read-only |
kill-switch | you press it | every Campaign pauses through the proxy, and write scope is revoked |
negative-backlog | a term batch waits 48 h for you | the fenced arm’s budget halves; at 96 h the arm pauses |
fenced-arm | the fenced arm is marked the loser | it 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.
| Check | Fix | Then |
|---|---|---|
fact-check fail · Not live yet, gaps #1 | Cut 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.
[
{
"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"
}| Node | Ads-on days | Ads-off days | Added | Confidence | Basis |
|---|---|---|---|---|---|
search-icp-visits-per-100 | 1,140 ICP Visits on $1,180 spend | 980 ICP Visits, $0 spend | +13.6 incremental ICP Visits per $100 [+4.1, +23.1] | ■■□□ Early sign | days switched at random, 28 a half · n 56 days · Other Startups +9.0, ■■■□ Likely, n 47 Startups |
icp-reached | 1,140 a half (285 a week) | 980 (245 a week) | +160 [+48, +272] | ■■□□ Early sign | the 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 tell | carried 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 %.
{
"$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=; three follow the tables.
| What happened, by option | Decisions | Done | Won (credited, not all caused by this Play) |
|---|---|---|---|
| Block the wasted terms · Reset bids to breakeven | 38 · 3 | 412 negatives · 3 resets | — |
| Queue new terms for your OK | 8 batches | 44 of 52 approved | 3 on Pro from approved terms (+$1,041 ARR) |
| Take the next spend step · Bid on your own name · Not now | 1 · 1 · 4 | Google Search opened · 1 brand ad group · spend held 4 times | 1 on Pro (+$347 ARR) · 0 · — |
| At 500 searches a month (the Use it when volume) | about $300 a month of spend | switchback: Too early to tell at 90 days | Other Startups’ results: +9.0 per $100, ■■■□ Likely, n 47 Startups |
| Day | Event | What happened | Your time |
|---|---|---|---|
| 0 | event_118 Campaign.created | the three steps; the dry run fails billing-connected and fact-check | 35 min |
| 0 | event_121 Integration.connected | Stripe connected; the claims source approved with 9 claims | 25 min |
| 9 | event_230 Invoice.collected | the first sale from a paid Visit: a probe click on “lease renewal software” | — |
| 21 | event_420 Role.updated | promoted: rule negatives and bid resets apply without waiting | — |
| 28 · 42 | event_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 |
| 35 | event_640 Limit.raised | the probe’s switchback read +15.1 per $100; you approved Google Search at $1,000 a month | 10 min |
| 39 | event_702 Campaign.paused | conversion-feed: no conversion sent for 81 h; the send was fixed and bids unfroze | 20 min |
| 1–56 | event_140 Approval.created, first of 9 | you approved 8 term batches and 1 spend step; weekly reviews, from the POST /{namespace}/tasks/next log | 3 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.²
- Cut late renewals by 40%
+ Renewal notices sent 60 days outThe 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
---
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.$ 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
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.