Category hubs
Ranks every product in a category by one published rule, labels your own, and sends each visitor to the best fit, outside products included.
- Step
- Earn referrals
- Channel
- Blog and search
- Kind
- Search
- Summary
- startupbuilder.dev
Be our Marketing Lead for a property software hub: rank every tool a small property manager might use by one rule we publish, label Leaseline and our other two Startups as ours, and send each visitor to the best fit, even when it isn’t us. I approve every rule change. Show me what’s missing; don’t publish.
# 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":"category-hubs"}'import { $ } from 'startup-builder'
return await $.Campaign.create({ play: 'category-hubs' }, { 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":"category-hubs"}' --try # Not live yet (gaps #2){
"name": "do",
"arguments": {
"code": "return await $.Campaign.create({ play: 'category-hubs' }, { 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": "category-hubs can’t start yet: 1 check fails.",
"checks": [
{
"check": "owned-by-a-person",
"status": "pass",
"observed": "0 listed products’ contacts are Leads a person owns",
"doc": "/plays/ category-hubs#check-owned-by-a-person",
"then": "Task.assign"
},
{
…
Hand the latest fit-finder visitor to the Marketing Lead agent and show me what it chose and why. Don’t send.
curl "https://api.sb/leaseline.app/events?where=name%20in%20(%22Startup.advanced%22%2C%20%22Monitor.checked%22%2C%20%22Form.submitted%22%2C%20%22Metric.queried%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_1431"}'
# Not live yet (gaps #7)
curl "https://api.sb/leaseline.app/decisions?where=on%20%3D%20%22contact_52%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%22contact_52%22" \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"import { $ } from 'startup-builder'
const { events: [last] } = await $.Event.list({ where: 'name in ("Startup.advanced", "Monitor.checked", "Form.submitted", "Metric.queried")', 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 (\"Startup.advanced\", \"Monitor.checked\", \"Form.submitted\", \"Metric.queried\")","limit":1}'
npx startup-builder do Trigger.fire --json '{"like":"event_1431"}' --try # Not live yet (gaps #7)
npx startup-builder do Decision.list --json '{"where":"on = \"contact_52\""}' # Not live yet (gaps #7)
npx startup-builder do Action.list --json '{"where":"on = \"contact_52\""}' # Not live yet (gaps #7){
"name": "do",
"arguments": {
"code": "const { events: [last] } = await $.Event.list({ where: 'name in (\"Startup.advanced\", \"Monitor.checked\", \"Form.submitted\", \"Metric.queried\")', 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_507",
"$type": "Decision",
"choice": "send-the-shortlist",
"reason": "finished the fit finder for 120 units, renewals and owner statements, and asked for the shortlist by email",
"stakes": "low",
"options": [
"open-a-hub",
"refresh-the-row",
"send-the-shortlist",
"answer-the-correction",
"tell-them-they-are-listed",
"propose-a-rule-change",
…
sample · 4 Not live yetPreviewOn POST /{namespace}/processes/{process}/enableOff POST /{namespace}/processes/{process}/pauseTake this one POST /{namespace}/tasks/{task}/claim
action_2 is the shortlist email, held 2 minutes: an outside tool first, Leaseline second, each with its live Price and the rule that ranked it, and Leaseline labeled as yours.
+0.19 Customers per 100 visitors · ■□□□ Too early to tell · n 1,
| Try another: the visitor | Choice | Reason |
|---|---|---|
| A visitor: 120 units, renewals and owner statements, asked for the shortlist | send-the-shortlist | finished the fit finder and asked by email |
| A listed tool raised its Pro plan to $49 | refresh-the-row | a listed product’s pricing page changed |
| A listed tool: “We do offer owner statements now.” | answer-the-correction | a correction filed from its row |
| Organic visits 34 % under the 4-week mean for 2 weeks | not-now | a visibility drop holds new pages |
- 1 h week 1, then 20 min a week
- Your time
- Day 11: first Customer from the hub
- First signal
Where it sits: Stranger → Lead → Trialing → Activated → At risk → Churned → Won back · Hub-routed customers → Sent by your other Startups → New Customers ↑ → ARR added each week → ARR
Worth’s formula: about +0.19 Customers per 100 visitors × 900 hub visitors a month × $400 first order ARR across your Startups ≈ +$684. Under First signal: About 7 months to tell a +0.2 lift at 900 visitors a month with half held back; until then it prints Too early to tell beside Other Startups’ results.
sample · api.sb/
Start it this week
npx startup-builder add play category-hubs # 1 · writes plays/category-hubs.mdx (Not live yet, gaps #5)
# 2 · dry run; below, its 422 on the day-0 Startup (clock at 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":"category-hubs"}'
# 3 · keep it, at You approve each one for the first 24 rows
curl -X POST https://api.sb/leaseline.app/campaigns \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"play":"category-hubs"}'
# on
curl -X POST https://api.sb/leaseline.app/processes/content-marketing/enable \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"
# off: the dataset and the fit finder freeze; published pages stay
curl -X POST https://api.sb/leaseline.app/processes/content-marketing/pause \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"| Check | Fix | Then |
|---|---|---|
owned-by-a-person pass · Not live yet, gaps #8 | 0 listed products’ contacts are Leads a person owns | POST /{namespace}/tasks/{task}/assign |
use-when pass · Not live yet, gaps #8 | 3 of your Startups in property software; 24 products to rank; 2,900 searches a month | POST /{namespace}/metrics/query |
baseline pass · Not live yet, gaps #8 | 0 hub visitors; 9 Customers a month from search across your Startups | POST /{namespace}/metrics/query |
approved-pages fail · Not live yet, gaps #8 | Publish the ranking rule Page; every list links to it. | POST /{namespace}/sites/{site}/publish |
domain-authenticated pass | the hub’s Domain: 3 of 3 records resolve | POST /{namespace}/domains/{domain}/authenticate |
role-unstaffed pass · Not live yet, gaps #8 | Marketing Lead is your agent; rule changes and corrections come to you | POST /{namespace}/roles/{role}/staff |
JSON
{
"type": "https://startupbuilder.dev/errors/refused",
"title": "Refused",
"status": 422,
"detail": "category-hubs can’t start yet: 1 check fails.",
"checks": [
{
"check": "owned-by-a-person",
"status": "pass",
"observed": "0 listed products’ contacts are Leads a person owns",
"doc": "/plays/category-hubs#check-owned-by-a-person",
"then": "Task.assign"
},
{
"check": "use-when",
"status": "pass",
"observed": "3 of your Startups in property software; 24 products to rank; 2,900 searches a month",
"doc": "/plays/category-hubs#check-use-when",
"then": "Metric.query"
},
{
"check": "baseline",
"status": "pass",
"observed": "0 hub visitors; 9 Customers a month from search across your Startups",
"doc": "/plays/category-hubs#check-baseline",
"then": "Metric.query"
},
{
"check": "approved-pages",
"status": "fail",
"doc": "/plays/category-hubs#check-approved-pages",
"fix": "Publish the ranking rule Page; every list links to it.",
"then": "Site.publish"
},
{
"check": "domain-authenticated",
"status": "pass",
"observed": "the hub’s Domain: 3 of 3 records resolve",
"doc": "/plays/category-hubs#check-domain-authenticated",
"then": "Domain.authenticate"
},
{
"check": "role-unstaffed",
"status": "pass",
"observed": "Marketing Lead is your agent; rule changes and corrections come to you",
"doc": "/plays/category-hubs#check-role-unstaffed",
"then": "Role.staff"
}
]
}It says no, says why, and names the fix.
| Check | Fix (one call) | Your time | Wait |
|---|---|---|---|
owned-by-a-person · use-when · baseline · pass: 0 owned; 3 Startups, 24 products; 9 Customers a month from search (gaps #8) | POST /{namespace}/metrics/query on hub-routed-customers | 1 min | none |
approved-pages · fail: no ranking rule Page (Not live yet) | POST /{namespace}/sites/{site}/publish the rule, its weights and how a product files a correction | 30 min | none |
domain-authenticated · role-unstaffed · pass · pass (gaps #8) | — | 0 min | none |
Your number · Before 0 Customers from the hub · Now 0.39 per 100 visitors · Target 0.5 by Dec 31 · ■□□□ Too early to tell · n 1,
Each row read costs $0.01 a page, and each page refresh $0.08 from your Domain (Worker cost); the shortlist email costs $0.04. That comes to about $15 a month at 24 products, 9 hub pages and 140 shortlists.
When it starts
| When | Trigger id | Starts on | Window | How it gets in |
|---|---|---|---|---|
| A visitor finishes the fit finder | hub-fit-request | event on Form.submitted where form = 'fit-finder', finished on a hub Page | the shortlist within 2 min; ignored after 1 d | the hub’s fit-finder Form, through POST /{namespace}/forms |
| A listed product’s pricing or feature page changes | hub-fact-changed | event on Monitor.checked where a listed product's pricing, plan, limits or feature Page changed | the row refreshes within 7 d | POST /{namespace}/monitors/{monitor}/check on each listed product |
| A listed product files a correction | hub-correction-filed | event on Form.submitted where form = 'correction', from a listed product's row | answered within 5 business days | the row’s correction Form |
+ 4 more for new categories, stale facts, visibility drops and flat rules (?lanes=) |
{
"$id": "https://api.sb/leaseline.app/events/event_1431" ,
"$type": "Event",
"name": "Form.submitted",
"at": "2026-10-19T17:22:40Z" ,
"seq": 1431,
"data": {
"form": "fit-finder",
"contact": {
"$id": "https://api.sb/leaseline.app/contacts/contact_52"
},
"answers": {
"units": 120,
"needs": [
"renewal notices",
"owner statements"
]
},
"arm": "fit-finder"
}
}# 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":"Form.submitted","url":"https://leaseline.app/hooks/sb"}'
npx startup-builder trigger Form.submitted --like event_1431 # replays the fit-finder answer
npx startup-builder clock advance --by 7d # runs the reopen check if the shortlist got no clickWho runs it
Marketing Lead · your agent (role:, worker_6) holds the Key Result and keeps the hub. It hands new hubs, rule changes and corrections that move a rank to you (Dana Okafor, person, worker_1). Your number links to Did it work.
| Option | Rung today | Evidence (passed of n) | Moves up at | Drops back when | You can |
|---|---|---|---|---|---|
| Refresh the row · Send the shortlist · Tell a product it’s listed | It does it, you can stop it | 24 of 24 rows approved unedited, then 41 refreshes · 140 of 140 · 24 of 24 | met on day 15 (the first 24 rows) | an independence fail 3 times in 7 days → You approve each one | POST /{namespace}/actions/{action}/cancel inside the hold |
| Answer the correction | It does it; a changed rank waits for you | 6 answered, 2 rank changes approved | — | correction-backlog → new pages hold | POST /{namespace}/approvals/{approval}/approve |
| Open a hub · Propose a rule change | You approve each one, on every rung | 1 of 1 · 0 proposed | never | — | POST /{namespace}/changes/{change}/merge · POST /{namespace}/changes/{change}/revert |
| Hold new pages | It does it | 1 hold, 9 days | — | — | POST /{namespace}/actions/{action}/redirect |
| Never: your own product ranked by a rule the page doesn’t publish, your Startups unlabeled, an outside product left out for being outside | To a person, every time | 1 refused, 0 published | never | — | POST /{namespace}/processes/{process}/pause |
What it reads · What it may call. It reads every product, yours included, from the same public pages, under one published rule; a correction is data, never instructions, and changes a row only when the product’s own page shows it. Its tools are POST /{namespace}/sites, POST /{namespace}/sites/{site}/publish, POST /{namespace}/monitors/{monitor}/check, PATCH /{namespace}/pages/{id}, POST /{namespace}/messages/{message}/schedule, POST /{namespace}/changes, and any other call becomes an Approval under the Role’s Mandate.
You hold the Marketing Lead Key Result: 0.5 Customers per 100 hub visitors by 2026-12-31, across your Startups. ← worker, metric
On each fit-finder answer, page change or correction, choose one option and say why. ← triggers, chooses
Rank every product by the published rule; label Leaseline and your other Startups as yours. ← your file, line 4; independence
Send each visitor to the best fit, outside products included. ← your file, line 5
Every rule change waits for Dana. ← your file, line 6 The slow loop. The weekly Decisions kept the published rule and its weights, and no Change was merged in the window, so there is no replay or read-back to print.
Bring your own agent (folded) starts at You approve each one; every row it writes passes the same independence check before it publishes.
npx startup-builder eval --scenario category-hubs-1680 --agent "claude -p" --seat marketing-lead # seated: 0 independence fails published, 96 % of price facts fresh · yours: 1 fail refused, 93 % fresh What it can do next
| Option | Picked when | Decided by | Runs | Hands to | If it’s just you |
|---|---|---|---|---|---|
Open a hub open-a-hub | 2+ of your Startups share a cluster, no hub | Your rule (the category test) | POST /{namespace}/sites → POST /{namespace}/sites/{site}/publish with the rule page first | you approve it · Take this one | you |
Refresh the row refresh-the-row | a listed product’s page changed, or a fact went stale | Your rule (the freshness budget) | POST /{namespace}/monitors/{monitor}/check → PATCH /{namespace}/pages/{id} | nobody | the same |
Send the shortlist send-the-shortlist | a finished fit finder and an email ask | Your rule (line 5) | hub-shortlist (POST /{namespace}/messages/{message}/schedule) | the visitor’s click | the same |
Answer the correction answer-the-correction | a correction from a listed product | Agent’s call, with its reason | hub-correction-reply → PATCH /{namespace}/pages/{id} if their page shows it | you, when a rank moves, due 5 business days · Take this one | you |
Tell a product it’s listed tell-them-they-are-listed | a product’s first listing | Your rule (the listing) | hub-listed-notice, once | nobody | the same |
Propose a rule change propose-a-rule-change | the top 10 within 2 % on one criterion | Agent’s call, with its reason | POST /{namespace}/changes with the new weights replayed on the current rows | you decide it · Take this one | you |
Hold new pages not-now | a visibility drop, an index hold or a backlog | Your rule (the hub Stops) | nothing; names the Stop that holds new pages | nobody | the same |
| When they write back | Fixture reply | What happens | Who answers | By when | Call |
|---|---|---|---|---|---|
| Interested | “Your second pick looks right. Can I try it?” | answers with the trial link for that product, and Leaseline’s own trial if it is Leaseline | Marketing Lead · your agent | 5 min | POST /{namespace}/conversations/{conversation}/answer |
| Send me pricing | “What would the top 3 cost for 120 units?” | answers with each product’s live Price from its own page, with the date read | Marketing Lead · your agent | 5 min | POST /{namespace}/conversations/{conversation}/answer |
| Not now | “Bookmarking this for spring.” | nothing else sends; the shortlist page stays at its link | nobody | at once | POST /{namespace}/leads/{lead}/nurture |
| Wrong person | “Sending this to our office manager.” | replies with the shortlist link they can forward; it never writes to a name from their text | Marketing Lead · your agent | 1 business day | POST /{namespace}/conversations/{conversation}/answer |
| Out of office | an auto-reply until Oct 26 | nothing; the shortlist never follows up | nobody | — | POST /{namespace}/messages/{message}/schedule |
| Question it can’t answer | “Which of these do you own?” | answers with the labels and the ownership line; anything more goes to you | you | 1 business day | POST /{namespace}/conversations/{conversation}/escalate |
| Stop | “Don’t email me.” | nothing else sends to them from any of your Startups | nobody | at once | POST /{namespace}/contacts/{contact}/suppress |
When it hands to you
New hubs, rule changes and corrections that move a rank come to you. What lands in your queue, as captured: the Brief, then the Approval (brief, draft, route and checks are Not live yet, gaps #7).
[
{
"$id": "https://api.sb/leaseline.app/tasks/task_316" ,
"$type": "Task",
"assignee": "worker_1",
"due": "2026-10-09T17:00:00Z" ,
"on": "organization_172",
"brief": {
"who": "a listed tool’s founder",
"said": "We do offer owner statements now.",
"chose": "answer-the-correction",
"why": "a correction that moves their rank from 7th to 4th",
"shown": "their features page added owner statements on 2026-09-30"
}
},
{
"$id": "https://api.sb/leaseline.app/approvals/approval_94" ,
"$type": "Approval",
"verdict": null,
"deadline": "2026-10-09T17:00:00Z" ,
"draft": "Row 4 · owner statements: yes (read 2026-10-02 from their features page)" ,
"route": "answer-the-correction",
"checks": [
{
"check": "independence",
"status": "pass"
}
]
}
]You answer with POST /{namespace}/tasks/{task}/claim (Take this one), POST /{namespace}/tasks/{task}/return with a reason, POST /{namespace}/approvals/{approval}/approve or POST /{namespace}/approvals/{approval}/refuse, each a one-call window, and an Approval unanswered by its deadline expires and the rank stays as read. More than 10 corrections past 5 business days hold new pages (correction-backlog).
On time: you, 3 of 3 approvals by their deadline, n 3, ■□□□ Too early to tell; 6 of 6 corrections answered within 5 business days. Your queue: counted by kind: in weeks 1 and 2, the rule page and 24 row approvals; since, about 1 correction a week.
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 |
|---|---|---|---|
owned-by-a-person (rule j; Not live yet, gaps #8) | a shortlist or notice to a Contact whose Lead or open Deal a person owns | Leave it to the person who owns it: the draft waits in their queue. | POST /{namespace}/tasks/{task}/assign |
customer-collision (Not live yet, gaps #1) | a shortlist to an active Customer of the top pick | Send it to their CSM instead. | POST /{namespace}/tasks/{task}/assign |
independence | a page that ranks by anything but the published rule, leaves your Startups unlabeled, or drops a product for being outside | Re-rank by the rule and label what is yours. | PATCH /{namespace}/pages/{id} |
| Stop (pauses the Play) | Pauses when | Then |
|---|---|---|
index-hold | over 30 % of hub pages crawled but not indexed | the next batch holds |
visibility | organic visits 30 % under the 4-week mean for 2 weeks | new pages freeze and a fact audit opens |
steering-check | your Startups’ share of fit clicks passes 1.5 times their share of top-3 slots, over 90 days and 300 clicks | rule changes freeze |
top-slot | one of your Startups first on more than half the lists, or first on the index | a person re-reads every source behind those ranks |
correction-backlog | over 10 corrections past 5 business days | new pages pause until the queue clears |
freshness-floor | under 90 % of price facts inside their 30-day budget | new pages pause |
spam-action | a Venue spam or manual action notice on the hub | every publish pauses and you are told |
kill-switch | you press it | every Tactic pauses, the dataset and fit finder freeze; POST /{namespace}/processes/{process}/enable after review |
The captured refusal, then the Stop that fired as a Campaign.paused reason (Not live yet, gaps #2); the list that published instead is the second thread in Example.
| Check | Fix | Then |
|---|---|---|
independence fail | Re-rank by the rule and label what is yours. | PATCH /{namespace}/pages/{id} |
owned-by-a-person pass · Not live yet, gaps #8 | — | POST /{namespace}/tasks/{task}/assign |
JSON
{
"type": "https://startupbuilder.dev/errors/refused",
"title": "Refused",
"status": 422,
"detail": "The “best for owner statements” list did not publish: it ranked Leaseline first on a criterion the rule page doesn’t weigh.",
"checks": [
{
"check": "independence",
"status": "fail",
"observed": "rank by “ease of setup”, not in the published rule",
"doc": "/plays/category-hubs#check-independence",
"fix": "Re-rank by the rule and label what is yours.",
"then": "Page.update"
},
{
"check": "owned-by-a-person",
"status": "pass"
}
]
}It says no, says why, and names the fix.
[
{
"reason": {
"limit": "visibility",
"observed": "organic visits 34 % under the 4-week mean, 2 weeks" ,
"bound": "30 %",
"window": "2 weeks"
},
"doc": "/plays/category-hubs#stop-visibility" ,
"held": "9 days"
}
]Did it work
Visitors were split at random, half shown the fit finder and half the ranked table only (rules m–o); a visitor counts once 30 days have passed since their first visit. Each figure’s call is POST /{namespace}/metrics/query by node and by arm; every node above the leaf is Not live yet (gaps #4).
+0.19 Customers per 100 visitors · ■□□□ Too early to tell · n 1,
- Chose
- send-the-shortlist
- Why
- finished the fit finder for 120 units, renewals and owner statements, and asked for the shortlist by email
- From
- open-a-hub · refresh-the-row · send-the-shortlist · answer-the-correction · tell-them-they-are-listed · propose-a-rule-change · not-now
- Expects
- a trial or Order at the top fit within 30d
- Reopens when
- no click on the shortlist from contact_52 within 7d
- By
- worker_6
JSON
{
"$id": "decisions/decision_507",
"$type": "Decision",
"choice": "send-the-shortlist",
"reason": "finished the fit finder for 120 units, renewals and owner statements, and asked for the shortlist by email",
"stakes": "low",
"options": [
"open-a-hub",
"refresh-the-row",
"send-the-shortlist",
"answer-the-correction",
"tell-them-they-are-listed",
"propose-a-rule-change",
"not-now"
],
"by": "worker_6",
"on": "contact_52",
"expected": "a trial or Order at the top fit within 30d",
"reopensWhen": "no click on the shortlist from contact_52 within 7d"
}| Node | Agent half | Your half | Added | Confidence | Basis |
|---|---|---|---|---|---|
hub-routed-customers | 0.39 per 100 visitors (2 of 510), fit finder shown | 0.20 (1 of 510), table only | +0.19 [−0.38, +0.76] | ■□□□ Too early to tell | held back at random by visitor · n 1,020 matured · 660 still maturing · Other Startups’ results +0.12, ■■□□ Early sign, n 41,000 |
sibling-sourced-share | $1,582 to your Startups: 2 to Leaseline, 1 to your deposits Startup | $347: 1 to Leaseline | +$1,235 [−$480, +$2,950] · Not carrying up yet | ■□□□ Too early to tell | the same assignment |
new-arr ↑ | $1,582, 3 Customers · Booked: credited, not all caused by this Play | $347, 1 Customer | +$1,235 [−$480, +$2,950] · Worth about +$680 a month | ■□□□ Too early to tell (3 vs 1 Customers) | the same · 4 trials still deciding |
net-new-arr · arr | +$1,582 | +$347 | +$1,235, carried: it moves no other bridge term | ■□□□ Too early to tell | carried from new-arr |
The leaf counts paying Customers of any of your Startups whose first visit came through the hub, per 100 hub visitors assigned, within 30 days. 61 % of fit-finder clicks went to outside products, and those count toward no node.
How it moves it: each visitor saw 24 products ranked by one published rule, with yours labeled, and the fit finder named a top match for their units and needs, where before they compared ten vendor pages.
Guardrails: fit finder against table only: your Startups’ share of fit clicks, 39 %, against their 38 % share of top-3 slots (under the 1.5 times line), and organic-share of visits to your Startups, 34 % against 31 %.
{
"$id": "https://api.sb/leaseline.app/campaigns/category-hubs" ,
"$type": "Campaign",
"play": "play:category-hubs@1" ,
"path": [
"hub-routed-customers",
"sibling-sourced-share",
"new-arr",
"net-new-arr",
"arr"
],
"experiment": {
"metric": "hub-routed-customers",
"unit": "per-100",
"heldBack": 840,
"decision": null
},
"readout": {
"value": 0.19,
"n": 1020,
"maturing": 660,
"interval": [
-0.38,
0.76
],
"readability": "inadmissible",
"basis": "held-back",
"pooled": false
},
"ledger": {
"spend": 18,
"workerCost": 9,
"hours": 2.4,
"wonArr": 1582,
"projectedArr": 684,
"paybackDays": 2
}
}Outside benchmark: readers trusted comparison lists that ranked a rival first more than lists that ranked their publisher first (a 2026 search-quality study of self-promotional lists).
Before it · After it. Before it: editorial-seo and aeo bring the searches the hub answers. After it: instant-response answers the visitors sent to Leaseline, and your other Startups’ own Plays answer theirs.
Example
Leaseline ran it from 2026-08-24 to 2026-10-19 (56 days): one hub ranking 24 property-management tools by a published rule of 5 weighted criteria, 3 of them your Startups, labeled. 1,680 visitors came; half saw the fit finder, at random, and 140 finished it. Read every captured page and email at ?threads=; three follow the tables.
| What happened, by option | Decisions | Done | Customers to your Startups (what happened) |
|---|---|---|---|
| Open a hub · Refresh the row | 1 · 41 | 1 hub, 9 pages · 41 refreshed | — |
| Send the shortlist · Tell a product it’s listed | 140 · 24 | 140 sent, 61 % with an outside product first · 24 told | 2 from shortlists |
| Answer the correction · Propose a rule change · Hold new pages | 6 · 0 · 1 | 6 answered, 2 ranks moved · — · 9 days held | — |
| Fit finder · table only | 840 visitors · 840 | 510 matured · 510 | 3 (2 of 510 matured) · 1 ($1,582 · $347, credited, not all caused by this Play) |
| At 500 searches a month (the Use it when volume) | about 500 visitors a month | held back at random; about a year to read | Other Startups’ results: +0.12 per 100, ■■□□ Early sign, n 41,000 |
| Day | Event | What happened | Your time |
|---|---|---|---|
| 0 | event_1127 Campaign.created | 24 products, the 5-criterion rule; the dry run fails approved-pages | 40 min |
| 1 | event_1133 Site.published | the rule page, then the index and 8 lists | 30 min |
| 1–15 | event_1136 Approval.created, first of 24 | you approved 24 rows unedited | 35 min |
| 11 | event_1182 Order.placed | the first Customer from the hub, to Leaseline | — |
| 15 | event_1196 Role.updated | promoted: rows and shortlists go after a 2-minute hold | — |
| 29 | event_1295 Action.refused | independence refused a list ranked on a criterion the rule doesn’t weigh | — |
| 38 · 47 | event_1324 Campaign.paused | visibility: visits 34 % under the mean; the fact audit found 2 stale prices, and pages resumed | 15 min |
| 1–56 | event_1137 Task.created, first of 9 | corrections and the weekly memo, from the POST /{namespace}/tasks/next log | 40 min · total 2.4 h measured, 2.5 h asked |
The hub → a visitor, Oct 2, 17:24 · Your shortlist for 120 units, renewals and owner statements: 1. an outside tool, $59 a month (its pricing page, read Oct 1). 2. Leaseline, $29 a month for 50 units, Team $79 for 150 (ours).¹ 3. another outside tool, $45 a month. Ranked by the rule at the link, which weighs renewal timing 30 %.²
Oct 3 · they clicked Leaseline, started a trial, and took Team on Oct 14 (+$945 ARR).
- 1. Leaseline (easiest setup) · 2. an outside tool · 3. an outside tool
+ 1. an outside tool (owner statements: full) · 2. Leaseline, ours (owner statements: per owner) · 3. an outside tool A listed tool’s founder, 2026-10-02 11:40 · “We do offer owner statements now.” The re-read found it on their features page, which moves them from 7th to 4th, so Answer the correction went to Dana (
task_316, due Oct 9). She approved the new rank on Oct 6, and the reply went the same hour.³
¹ Your own Startups carry the label “ours”. ² Every list links the one published rule. ³ A correction that moves a rank waits for a person.
Your settings
---
holdout: 0.5 # half the visitors see the ranked table only, chosen at random
---
Rank every tool by the published rule; label Leaseline and our other Startups as ours.
Send each visitor to the best fit, even when it isn’t ours.
Every rule change waits for me.$ npx startup-builder show plays/category-hubs
holdout 0.5 · the rule and labels · best fit first · rule changes wait your file plays/category-hubs.mdx:2,4,5,6 (Ask 2026-08-24)
family demand · process content-marketing · worker role:marketing-lead, owns never · chooses 7 (5 rule, 2 read) · sequences hub-shortlist, hub-listed-notice, hub-correction-reply · approval every-send · checks owned-by-a-person, customer-collision, independence · stops 8 catalog play:category-hubs@1 Every key from the catalog
Compiled, not written: edit the Play record category-hubs 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.