Skip to content

House ads

Shows one labeled Ad for your sister product on a Page whose topic fits, only where it is worth more than the Page’s own unit.

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

Hand the latest host Page to the Marketing Lead agent and show me what it chose and why. Don’t publish.

curl "https://api.sb/leaseline.app/events?where=name%20in%20(%22Site.published%22%2C%20%22Offer.published%22%2C%20%22Content.drafted%22%2C%20%22Schedule.fired%22%2C%20%22Agreement.signed%22%2C%20%22Order.placed%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_1199"}'

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

const { events: [last] } = await $.Event.list({ where: 'name in ("Site.published", "Offer.published", "Content.drafted", "Schedule.fired", "Agreement.signed", "Order.placed")', 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 (\"Site.published\", \"Offer.published\", \"Content.drafted\", \"Schedule.fired\", \"Agreement.signed\", \"Order.placed\")","limit":1}'
npx startup-builder do Trigger.fire --json '{"like":"event_1199"}' --try   # Not live yet (gaps #7)
npx startup-builder do Decision.list --json '{"where":"on = \"page_77\""}'   # Not live yet (gaps #7)
npx startup-builder do Action.list --json '{"where":"on = \"page_77\""}'   # Not live yet (gaps #7)
{
  "name": "do",
  "arguments": {
    "code": "const { events: [last] } = await $.Event.list({ where: 'name in (\"Site.published\", \"Offer.published\", \"Content.drafted\", \"Schedule.fired\", \"Agreement.signed\", \"Order.placed\")', 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_426",
    "$type": "Decision",
    "choice": "show-the-card",
    "reason": "Keyfold’s “Run a move-out inspection” Page: Leaseline’s Ad is worth $97 per 1,000 views net of host loss, over Keyfold’s own unit at $68 × the 1.2 reserve ($82)",
    "stakes": "low",
    "options": [
      "show-the-card",
      "keep-your-own",
      "fill-the-issue-slot",
      "rebuild-the-card",
      "to-you",
      "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_1 places the Ad, labeled “Sponsored · sister company”, the same for crawlers and people; action_2 logs the Exposure with its propensity. In try mode nothing renders.

+0.19 first Orders per 1,000 · ■■□□ Early sign · n 8 host-weeks · path → arr

Try another: the host PageChoiceReason
Keyfold help: “Run a move-out inspection”show-the-card$97 net per 1,000 views over the $82 reserve
Keyfold help: “Export an inspection to PDF”keep-your-ownLeaseline’s Ad is worth $31; the reserve is $82
Keyfold’s October issue: one Offer slot, topic renewalsfill-the-issue-slotthe Ad clears the issue’s reserve
Keyfold pricing Pagenot-nowa never-list Page: no slot, ever

Where it sits: Stranger → Lead → Trialing → Activated → At risk → Churned → Won back · House ad yield → Sent by your other Startups → New Customers ↑ → ARR added each week → ARR

Worth’s formula: about +0.19 first Orders per 1,000 net of host loss (weeks alternated at random) × 21,500 viewable impressions a month × $347 first order ARR ≈ +$1,417. Under First signal: About 12 weeks to tell a +0.19 lift at 21,500 impressions a month with 20 % of host-weeks off; at 8 weeks it reads Early sign.

sample · api.sb/leaseline.app · 41 Customers at 2026-10-19 · on Site.published · first clearing 120 ms · 7 keys Not live yet

Runs inside: Portfolio cross-sell, as one of its options; this page keeps its own record and URL.

Start it this week

Terminal
npx startup-builder add play house-ads   # 1 · writes plays/house-ads.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":"house-ads"}'

# 3 · keep it: Ads start in shadow mode, rendered for you only
curl -X POST https://api.sb/leaseline.app/campaigns \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"play":"house-ads"}'

# on
curl -X PATCH https://api.sb/leaseline.app/campaigns/house-ads \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"endsAt":null}'

# off: every host shows its own unit again
curl -X PATCH https://api.sb/leaseline.app/campaigns/house-ads \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"endsAt":"now"}'
422Refusedhouse-ads can’t start: 2 checks fail.
CheckFixThen
use-when pass · Not live yet, gaps #82 Startups (Leaseline, Keyfold); 5,400 eligible host views a weekPOST /{namespace}/metrics/query
baseline pass · Not live yet, gaps #80.09 Leaseline first Orders per 1,000 Keyfold help views, from any source, n 48,000 views in 90 daysPOST /{namespace}/metrics/query
approved-pages fail · Not live yet, gaps #8Publish Leaseline’s Story Page; an Ad states only published Pages.POST /{namespace}/sites/{site}/publish
slot-id failRecord the slot id on each first Order’s Acquisition, one line at checkout.POST /{namespace}/events/capture
domain-authenticated passboth DomainsPOST /{namespace}/domains/{domain}/authenticate
role-unstaffed pass · Not live yet, gaps #8new Ads and edge Pages go to the founder, DanaPOST /{namespace}/roles/{role}/staff
JSON
{
  "type": "https://startupbuilder.dev/errors/refused",
  "title": "Refused",
  "status": 422,
  "detail": "house-ads can’t start: 2 checks fail.",
  "checks": [
    {
      "check": "use-when",
      "status": "pass",
      "observed": "2 Startups (Leaseline, Keyfold); 5,400 eligible host views a week",
      "doc": "/plays/house-ads#check-use-when",
      "then": "Metric.query"
    },
    {
      "check": "baseline",
      "status": "pass",
      "observed": "0.09 Leaseline first Orders per 1,000 Keyfold help views, from any source, n 48,000 views in 90 days",
      "doc": "/plays/house-ads#check-baseline",
      "then": "Metric.query"
    },
    {
      "check": "approved-pages",
      "status": "fail",
      "doc": "/plays/house-ads#check-approved-pages",
      "fix": "Publish Leaseline’s Story Page; an Ad states only published Pages.",
      "then": "Site.publish"
    },
    {
      "check": "slot-id",
      "status": "fail",
      "doc": "/plays/house-ads#check-slot-id",
      "fix": "Record the slot id on each first Order’s Acquisition, one line at checkout.",
      "then": "Event.capture"
    },
    {
      "check": "domain-authenticated",
      "status": "pass",
      "observed": "both Domains",
      "doc": "/plays/house-ads#check-domain-authenticated",
      "then": "Domain.authenticate"
    },
    {
      "check": "role-unstaffed",
      "status": "pass",
      "observed": "new Ads and edge Pages go to the founder, Dana",
      "doc": "/plays/house-ads#check-role-unstaffed",
      "then": "Role.staff"
    }
  ]
}

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

CheckFix (one call)Your timeWait
use-when · baseline · pass: 2 Startups, 5,400 views a week; 0.09 per 1,000, n 48,000 (gaps #8)POST /{namespace}/metrics/query on house-ad-yield and organic-visits, which fills Before in Your number1 minnone
approved-pages · fail (Not live yet)POST /{namespace}/sites/{site}/publish your Story Page20 minnone
slot-id · fail (gaps #8)POST /{namespace}/events/capture the slot id on the first Order’s Acquisition15 minnone
domain-authenticated · role-unstaffed · passPOST /{namespace}/domains/{domain}/authenticate · POST /{namespace}/roles/{role}/staff, only if they change0 minnone

Your number · Before 0.09 per 1,000, from any source · Now 0.23 per 1,000 with a slot id · Target 0.25 by Dec 31 · ■■□□ Early sign · n 8 host-weeks

An Ad costs nothing to show: the clearing and the render make no model call. A rebuilt Ad costs $0.05 to draft (Worker cost). That comes to about $0.20 a month, and the price that matters is host loss, which prints beside the yield.

When it starts

WhenTrigger idStarts onWindowHow it gets in
A help, guide, receipt or 404 Page that may carry an Ad changeshost-page-publishedevent on Site.published where pageKind in ('docs', 'help', 'article', 'guide', 'changelog', 'receipt', '404')7dyour Site posts to POST /{namespace}/sites/{site}/publish
A host drafts a promotional issue with one Offer slothost-issue-draftedevent on Content.drafted where kind = 'promotional'1dPOST /{namespace}/content/draft in the host’s newsletter
Monday: each host’s week is set on or off at randomblock-startat on Schedule.fired where Monday 00:00 in the host’s time zone, each host set on or off at random, 20 % off7d; 20 % of host-weeks offset by your file’s switchback line
+ 3 more for a sister Offer that changed, a network member joining, and a first Order from an Ad (?lanes=all)
One signed delivery · StartupBuilder-Signature
{
  "$id": "https://api.sb/leaseline.app/events/event_1199",
  "$type": "Event",
  "name": "Site.published",
  "at": "2026-10-19T10:15:44Z",
  "seq": 1199,
  "data": {
    "site": "sites/keyfold-help",
    "page": "pages/page_77",
    "title": "Run a move-out inspection",
    "kind": "help",
    "topics": [
      "move-out",
      "renewals",
      "deposits"
    ],
    "viewsLast7d": 1420
  }
}
Reproduce it in test
# each delivery is signed with the Watch’s secret
curl -X POST https://api.sb/leaseline.app/watches \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"selector":"Site.published","url":"https://leaseline.app/hooks/sb"}'
npx startup-builder trigger Site.published --like event_1199   # replays the delivery through the clearing
npx startup-builder clock advance --by 7d   # starts the next switchback week

Who runs it

Marketing Lead · your agent (role:marketing-lead, worker_8) holds the Key Result for both Startups: it builds each Ad from the receiver’s own Pages and runs the clearing in code. New Ads and edge Pages hand to you (Dana Okafor, person, worker_1, role:founder), who owns both. No Visitor’s data picks an Ad: the Page’s topic does.

OptionRung todayEvidence (passed of n)Moves up atDrops back whenYou can
Show the sister Ad · Keep your own unitIt does it1,188 clearings, 0 labels missingmet on day 14 (shadow mode, 0 audit misses)a label or placement Stop → shadow modePOST /{namespace}/actions/{action}/cancel · the off switch
Fill the issue’s Offer slotIt does it, you can stop it2 of 2the host issue’s own rungan unsubscribe-floor Stop → You approve each onePOST /{namespace}/actions/{action}/cancel inside the hold
Rebuild the Ad · To youYou approve each one2 of 2 · 1 of 1never: an Ad naming another product stays here—POST /{namespace}/approvals/{approval}/approve · POST /{namespace}/approvals/{approval}/refuse
Change the reserve ruleYou approve each one1 merged of 1never—POST /{namespace}/changes/{change}/merge · POST /{namespace}/changes/{change}/revert
Never: a slot on pricing, checkout, sign-up, billing or cancel Pages; “for you” copyRefused, every time3 Pages skipped, 1 line refusednever—the placement Stop and the approved-pages Check

What it reads · What it may call. It reads the host Page’s topic and kind and the receiver’s Pricing Page, never what a Visitor did, read or bought. It may call PATCH /{namespace}/pages/{id}, POST /{namespace}/events/capture and POST /{namespace}/content/draft; an Ad’s first render needs your POST /{namespace}/approvals/{approval}/approve. Its tools are PATCH /{namespace}/pages/{id}, POST /{namespace}/events/capture, POST /{namespace}/content/draft, POST /{namespace}/messages/{message}/broadcast, POST /{namespace}/approvals/{approval}/approve, POST /{namespace}/tasks/{task}/assign, and any other call becomes an Approval under the Role’s Mandate.

The brief (folded): compiled Agent-register Markdown · play:house-ads@1 · each line names its record key
You hold the Marketing Lead Key Result: 0.25 first Orders per 1,000 house impressions by 2026-12-31. ← worker, metric
On each eligible Page or issue, choose one option and say why; a never-list Page gets Not now. ← triggers, chooses
Show a sister Ad only when it beats the host unit by the 1.2 reserve, net of host loss. ← your file, line 3
Label every slot “Sponsored · sister company”, from Portfolio membership, never from copy. ← copyRules 1, 2
State only what the receiver’s Brand, Story, Offer and Pricing Page state.       ← copyRules 5, approved-pages
Never say “for you”: the Page chose it.                                          ← copyRules 3

The slow loop, as a Change. decision_386 (2026-09-21, weekly) read host loss at 0 on receipt Pages and 4 % on help Pages, so change_27 cut the help-Page reserve from 1.3 to 1.2 and kept receipts at 1.0 (your file, line 3), and Dana merged it with POST /{namespace}/changes/{change}/merge. Replayed on past clearings, POST /{namespace}/changes/{change}/review put sister Ads on 31 % more help views; read back at +21d, host loss is 3 %, n 3 host-weeks, ■□□□ Too early to tell (Example, day 49).

Bring your own agent (folded) runs the clearing at the same rung when it matches the seated agent on the eval suite; the clearing is code either way, and POST /{namespace}/roles/{role}/staff seats it.

Terminal
npx startup-builder eval --scenario house-ads-32 --agent "claude -p" --seat marketing-lead   # seated: 0.23 per 1,000, 0 labels missing, 0 never-list renders · yours: 0.21, 0 missing, 1 render on a billing Page → shadow mode

What it can do next

OptionPicked whenDecided byRunsHands toIf it’s just you
Show the sister Ad show-the-cardits value per 1,000 views, less host loss, beats the host unit by the reserveYour rule (line 3)PATCH /{namespace}/pages/{id} places one labeled Ad → POST /{namespace}/events/capture logs the Exposurenobody; the Ad links to the receiver’s own Sitethe same
Keep your own unit keep-your-ownno sister Ad clears the reserveYour rule (line 3)POST /{namespace}/events/capture: checked, host unit keptnobodythe same
Fill the issue’s Offer slot fill-the-issue-slota host’s promotional issue, and the Ad clears its reserveYour rule (line 3)POST /{namespace}/content/draft the slot → the host’s own POST /{namespace}/messages/{message}/broadcast sends the issuethe host issue’s own Playthe same
Rebuild the Ad rebuild-the-carda sister Offer changed, or an Ad’s yield fell by a thirdAgent’s call, with its reasonPOST /{namespace}/content/draft from the receiver’s Pages → your POST /{namespace}/approvals/{approval}/approveyou approve it, due 1 business day · Take this oneyou
To you to-youa Page near a ranked list or a sensitive topic, or a network member asks to joinAgent’s call, with its reasonPOST /{namespace}/tasks/{task}/assignyou, due 1 business day · Take this oneyou
Not now not-nowan off week, a never-list Page, or cross-sell off for either StartupYour rule (the switchback and the never-list)nothing; names the off week or the Page kindnobodythe same
When they write backFixture replyWhat happensWho answersBy whenCall
Interesteda reader of Keyfold’s issue: “Tell me more about Leaseline.”the reply goes to Leaseline’s inbound as a new Lead, with the slot id on its AcquisitionSDR · your agent, through instant-response5 minPOST /{namespace}/leads
Send me pricing“What does Leaseline cost?”answered with Leaseline’s Pricing Page as it standsSDR · your agent5 minPOST /{namespace}/conversations/{conversation}/answer
Not now“Not this year.”nothing changes: Ads follow the Page’s topic, never the readernobodyat onceGET /{namespace}/decisions
Wrong person“Send this to our office manager.”Keyfold’s host thread thanks them; nothing is sent to an address from their textthe host’s own Play1 business dayPOST /{namespace}/conversations/{conversation}/answer
Out of officean auto-reply to the issuenothing: an issue reply carries no follow-upnobody—GET /{namespace}/decisions
Question it can’t answer“Do Keyfold and Leaseline share my data?”To you: the answer comes from the Why this ad Page, in your wordsyou (Dana, person)1 business dayPOST /{namespace}/conversations/{conversation}/escalate
Stop“No more sister-company offers.”the host’s issues carry the host’s own Offer for them from then onnobodyat oncePOST /{namespace}/contacts/{contact}/unsubscribe

When it hands to you

New Ads and edge Pages hand to you (Dana Okafor, person), who runs both Startups; an Ad naming another product stays at You approve each one. As captured, the Approval for a rebuilt Ad (brief, draft and checks are Not live yet, gaps #7).

What lands in each queue · sample
[
  {
    "$id": "https://api.sb/leaseline.app/approvals/approval_91",
    "$type": "Approval",
    "verdict": null,
    "deadline": "2026-10-20T17:00:00Z",
    "draft": "Sponsored · sister company — Leaseline sends every renewal notice 60 days out, per owner. From $29 a month. leaseline.app",
    "route": "rebuild-the-card",
    "checks": [
      {
        "check": "approved-pages",
        "status": "pass"
      },
      {
        "check": "cross-sell-off",
        "status": "pass"
      }
    ]
  }
]

You answer with POST /{namespace}/approvals/{approval}/approve, an edit, or POST /{namespace}/approvals/{approval}/refuse with a reason; an edge Page Task with POST /{namespace}/tasks/{task}/claim (Take this one), POST /{namespace}/tasks/{task}/return with a reason, or POST /{namespace}/actions/{action}/redirect. An Approval unanswered by its deadline expires, and the old Ad keeps showing. Nobody else holds both Startups, so nothing goes up a Role.

On time: You: 3 of 3 answered within 1 business day, median 2h 05m, n 3, ■□□□ Too early to tell beside Other Startups’ 88 % on time. Your queue: counted by kind: in week 1, 2 Ad Approvals and 1 shadow-mode review; after that, about 1 Approval a month and 1 weekly readout.

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
owned-by-a-person (rule j; Not live yet, gaps #8)an issue slot to a Contact whose open Deal a person ownsIt’s theirs: the host’s own Offer shows to them instead.POST /{namespace}/tasks/{task}/assign
approved-pages (Not live yet)an Ad line that the receiver’s Brand, Story, Offer and Pricing Page do not stateCut the line, or publish the Page that says it.POST /{namespace}/sites/{site}/publish
cross-sell-off (the Sensitive stop)an Ad from, or on, a Startup whose cross-sell Setting is offNothing to do: its Ads and slots leave within 1 hour.GET /{namespace}/decisions
Stop (pauses the Play)Pauses whenThen
labela rendered slot lacks its label, its Why this ad link, rel sponsored or the strict-origin referrerevery slot on that host pauses within 15 min
placement (Pages)a slot on a never-list Page, in a machine-readable output, or served differently to a crawlerevery slot on that host pauses
host-loss (Pages)host first Orders per 1,000 views trail the off weeks by over 5 % at minNslots pause on that Page kind
yield-flooryield net of host loss at or under 0 at minNthe Play stops on that host and opens a Decision
search (Pages)a host or receiver loses 30 % of its indexed Pages in a week, or a search Venue sends a links noticesister slots pause on every Startup you run
footprint (Pages)a link-footprint flagslots pause on every host it names
refund-qualityCustomers from Ads refund or cancel within 30 days at twice the receiver’s ratethat Ad pauses
unsubscribe-floor (email)issues with a sister slot draw unsubscribes 50 % above those without, over 2,000 deliveredemail slots end on that host
speed (Pages)slot Pages load 100 ms slower at the 75th percentile than off-week Pagesslots pause on that Page kind
credit-freezedatacenter clicks over 5 %, or a click rate 3 times the Page kind’s median for 3 daysthat member’s hosted credit freezes
kill-switchyou press itevery Tactic pauses at once; every host shows its own unit

The captured refusal, then the Stop that fired as a Campaign.paused reason (Not live yet, gaps #2); the rewrite that showed is the Keyfold receipt thread in Example.

422RefusedThe Leaseline Ad on Keyfold’s receipt Page did not render: one line states what no Leaseline Page says.
CheckFixThen
approved-pages fail · Not live yet, gaps #8Cut the line, or publish the Page that says it.POST /{namespace}/sites/{site}/publish
cross-sell-off pass—GET /{namespace}/decisions
JSON
{
  "type": "https://startupbuilder.dev/errors/refused",
  "title": "Refused",
  "status": 422,
  "detail": "The Leaseline Ad on Keyfold’s receipt Page did not render: one line states what no Leaseline Page says.",
  "checks": [
    {
      "check": "approved-pages",
      "status": "fail",
      "doc": "/plays/house-ads#check-approved-pages",
      "fix": "Cut the line, or publish the Page that says it.",
      "then": "Site.publish"
    },
    {
      "check": "cross-sell-off",
      "status": "pass"
    }
  ]
}

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

The Stops that fired, as Campaign.paused reasons · sample
[
  {
    "reason": {
      "limit": "speed",
      "observed": "+140 ms at p75",
      "bound": "+100 ms",
      "window": "7d, help Pages"
    },
    "doc": "/plays/house-ads#stop-speed",
    "task": "task_477"
  }
]

Did it work

Each host’s weeks are switched on or off at random, 20 % off, so the halves are on weeks and off weeks of the same Pages (rule o, alternating weeks at 10 or more a week). Each figure’s call is POST /{namespace}/metrics/query by node and by block, and every node above the leaf is Not live yet (gaps #4).

+0.19 first Orders per 1,000 · ■■□□ Early sign · n 8 host-weeks · path → arr

Chose
show-the-card
Why
Keyfold’s “Run a move-out inspection” Page: Leaseline’s Ad is worth $97 per 1,000 views net of host loss, over Keyfold’s own unit at $68 × the 1.2 reserve ($82)
From
show-the-card · keep-your-own · fill-the-issue-slot · rebuild-the-card · to-you · not-now
Expects
0.2 first Orders per 1,000 viewable impressions within 30d
Reopens when
Order.placed with this slot id under 0.1 per 1,000 after 10,000 impressions
By
worker_8
JSON
{
  "$id": "decisions/decision_426",
  "$type": "Decision",
  "choice": "show-the-card",
  "reason": "Keyfold’s “Run a move-out inspection” Page: Leaseline’s Ad is worth $97 per 1,000 views net of host loss, over Keyfold’s own unit at $68 × the 1.2 reserve ($82)",
  "stakes": "low",
  "options": [
    "show-the-card",
    "keep-your-own",
    "fill-the-issue-slot",
    "rebuild-the-card",
    "to-you",
    "not-now"
  ],
  "by": "worker_8",
  "on": "page_77",
  "expected": "0.2 first Orders per 1,000 viewable impressions within 30d",
  "reopensWhen": "Order.placed with this slot id under 0.1 per 1,000 after 10,000 impressions"
}
NodeOn weeks (Ads shown)Off weeks (your own unit)AddedConfidenceBasis
house-ad-yield0.31 Leaseline first Orders per 1,000 Keyfold views (7 with a slot id in 30,100 impressions, plus 2 from any source)0.09 per 1,000, from any source (1 in 11,200)+0.19 per 1,000 net of host loss [0.02, 0.36] · Worth about +$1,400 a month■■□□ Early signweeks alternated at random · n 8 host-weeks (26 on, 6 off) · 2 Orders still maturing
sibling-sourced-share38 % of new ARR (7 of 18 Customers)14 % (1 of 7)+24 points [−9, +57] · Not carrying up yet■□□□ Too early to tellthe same assignment
new-arr ↑$6,246 in the on weeks · Booked from Ads: $2,429, 7 Customers on Pro: credited, not all caused by this Play$1,388 in the off weeks+$347 a week [−$410, +$1,104]■□□□ Too early to tell (7 vs 1 Customers)the same · 3 trials still deciding
net-new-arr · arr+$6,246+$1,388carried: it moves no other bridge term■□□□ Too early to tellcarried from new-arr

The leaf counts Leaseline first Orders carrying a house slot id within 30 days of the click, per 1,000 viewable impressions, net of host loss on Keyfold, read on weeks switched at random with a 24-hour burn-in, to 40 first Orders.

How it moves it: an Ad on a Page whose topic matches renewals, at a median 1.4 % click rate, where the host’s own unit drew 0.3 %.

Guardrails: on weeks against off weeks: new-paying-customers on Keyfold, the host, 0.29 against 0.30 per 1,000 views (host loss 3 %), and refunds within 30 days, 0 of 7 against Leaseline’s 4 %.

The Campaign card · sample
{
  "$id": "https://api.sb/leaseline.app/campaigns/house-ads",
  "$type": "Campaign",
  "play": "play:house-ads@1",
  "path": [
    "house-ad-yield",
    "sibling-sourced-share",
    "new-arr",
    "net-new-arr",
    "arr"
  ],
  "experiment": {
    "metric": "house-ad-yield",
    "unit": "per-1000",
    "heldBack": 6,
    "decision": null
  },
  "readout": {
    "value": 0.19,
    "n": 8,
    "maturing": 2,
    "interval": [
      0.02,
      0.36
    ],
    "readability": "C",
    "basis": "alternating-weeks",
    "pooled": false
  },
  "ledger": {
    "spend": 0,
    "workerCost": 0.2,
    "hours": 1.2,
    "pipelineArr": 1041,
    "wonArr": 2429,
    "projectedArr": 1417,
    "paybackDays": 0
  }
}

Outside benchmark: none measures a labeled sister-company Ad against the host’s own unit; Other Startups’ results, +0.15 per 1,000, ■■■□ Likely, n 214 host-weeks, stand in for one.

Before it · After it. Before it: cross-sell decides which sister product fits the moment, and this Ad is one of its options. After it: a first Order from an Ad counts in sibling-sourced-share, and replies go to instant-response.

Example

Leaseline and Keyfold ran it from 2026-08-24 to 2026-10-19 (56 days) on 4 hosts: Keyfold’s help and receipt Pages, and Leaseline’s help and 404 Pages. 32 host-weeks ran, 26 on and 6 off. Read every captured thread at ?threads=all; three follow the tables.

What happened, by optionDecisionsImpressionsClicksFirst Orders (credited, not all caused by this Play)
Show the sister Ad61230,1004217 Leaseline on Pro (+$2,429 ARR)
Keep your own unit576———
Fill the issue’s Offer slot23,800 delivered411 trial still deciding
Rebuild the Ad · To you · Not now2 · 1 · 48———
All · off weeks1,24133,9004627 · 1 from any source
At 21,500 impressions a month (the Use it when volume)about 800 a month21,500about 300Other Startups’ results: +0.15 per 1,000, ■■■□ Likely, n 214 host-weeks
DayEventWhat happenedYour time
0event_127 Campaign.createdthe three steps; dry run fails approved-pages and slot-id40 min
0–13event_140 Page.updated, shadow modeAds rendered for you only; the label audit passed every render15 min
6event_244 Order.placedthe first Customer from an Ad: a 60-unit manager from Keyfold’s move-out guide—
14event_350 Role.updatedpromoted: Ads render to Visitors—
28 · 49event_531 Change.merged · event_874 Readout.createdthe help-Page reserve at 1.2: replayed at 31 % more views, read back at +21d with host loss at 3 %5 min
35event_621 Campaign.pausedspeed: help Pages 140 ms slower; resumed once the Ad loaded after the content20 min
1–56event_150 Approval.created, first of 3you approved 2 Ads and 1 edge Page, and read 8 weekly readouts25 min · total 1.2 h measured, 1.5 h asked

Keyfold help, “Run a move-out inspection”, Aug 30 · after the guide, one aside: *Sponsored · sister company — Leaseline sends every renewal notice 60 days out, per owner. From $29 a month.*¹ *Why this ad: this Page’s topic picked it; nothing about you reaches Leaseline before a click.*²

Aug 30, 14:20 · a 60-unit manager clicked, started a trial, and paid on Sep 2: Order.placed with slot ks_77, +$347 ARR.

Keyfold receipt Page, 2026-09-09 · the Ad line approved-pages refused, then the rewrite that showed
- Recommended for you: Leaseline cuts late renewals in half.
+ Sponsored · sister company — Leaseline sends renewal notices per owner, 60 days out.

Brightwell Storage, 2026-10-02 09:15 · a self-serve Startup asked to join the house network as host and receiver. The agent chose To you, with the Agreement draft attached, due 1 business day. · Dana, 16:40 · read it and returned it with POST /{namespace}/tasks/{task}/return: “Storage buyers aren’t ours; not this year.”

¹ The label comes from Portfolio membership, and the line from Leaseline’s Pricing Page. ² The fixed Why this ad line, compiled from the Play’s Settings.

Your settings

plays/house-ads.mdx
---
switchback: 0.2      # 1 host-week in 5 shows your own unit, at random
reserve: { help: 1.2, receipt: 1.0, 404: 1.0 }
---
Show Leaseline on Keyfold’s help and receipt Pages, and Keyfold on Leaseline’s. Never on pricing, sign-up or billing Pages.
Text
$ npx startup-builder show plays/house-ads
switchback 0.2 · reserve help 1.2, receipt 1.0, 404 1.0     your file  plays/house-ads.mdx:2,3,5 (Ask 2026-08-24; line 3 by change_27, 2026-09-21)
family demand · runs inside cross-sell · worker role:marketing-lead, owns never · chooses 6 (4 rule, 2 read) · clearing in code, 0 model calls · checks owned-by-a-person, approved-pages, cross-sell-off · stops 11 · placements portfolio-network, gmail, outlook, yahoo-mail    catalog  play:house-ads@1 (Not live yet, gaps #5)

Every key from the catalog

KeyIdWhat it is
Tactichouse-slot-inventoryHouse slot inventory marks each host Page eligible or not by its kind, and sets its reserve from the host’s own unit value. Starts at You approve the first wave.
Tactichouse-unit-buildHouse unit build compiles each sibling’s house Ad from its Brand, Story, Offer and Pricing Page, and checks it against the copy rules. Starts at You approve each one.
Tactichouse-slot-clearingHouse slot clearing ranks sibling units on each eligible Page by expected value against the Page’s reserve, in code, with a logged exploration share. Starts at Sends nothing.
Tactichouse-email-slotHouse email slot lets a related Brand’s unit take the one Offer in a host’s promotional issue when it clears the reserve. Starts at You approve the first wave.
Tactichouse-label-auditHouse label audit checks each rendered slot’s label, sponsored link and placement, as crawler and browser, and pauses a host on any miss. Starts at Sends nothing.
Tactichouse-network-joinHouse network join admits a self-serve Startup to house Ads under an Agreement that trades hosted for received impressions and moves no personal data. Starts at You approve each one.
Tactichouse-ad-readoutHouse ad readout runs the host-week switchback and reports house ad yield, host loss and value net, with variance at the host-week. Starts at Sends nothing.
Triggerhost-page-publishedHost page published: event on Site.published, window 7d.
Triggersibling-offer-changedSibling offer changed: event on Offer.published, window 30d.
Triggerhost-issue-draftedHost issue drafted: event on Content.drafted, window 1d.
Triggerblock-startSwitchback week: at on Schedule.fired, window 7d.
Triggernetwork-agreement-signedNetwork agreement signed: event on Agreement.signed, window 30d.
Triggerhouse-sourced-customerHouse sourced customer: event on Order.placed, window 30d.

Compiled, not written: edit the Play record house-ads 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.