Skip to content

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.

Channel
Blog and search
Kind
Search
Run oneMode try
You, to your agent

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"
  }
}
Response200application/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 yetPreview

On 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,020 matured · path → arr

Try another: the visitorChoiceReason
A visitor: 120 units, renewals and owner statements, asked for the shortlistsend-the-shortlistfinished the fit finder and asked by email
A listed tool raised its Pro plan to $49refresh-the-rowa listed product’s pricing page changed
A listed tool: “We do offer owner statements now.”answer-the-correctiona correction filed from its row
Organic visits 34 % under the 4-week mean for 2 weeksnot-nowa visibility drop holds new pages

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/leaseline.app · 41 Customers at 2026-10-19 · on Form.submitted · first shortlist 1m after the fit finder · 4 keys Not live yet

Start it this week

Terminal
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"
422Refusedcategory-hubs can’t start yet: 1 check fails.
CheckFixThen
owned-by-a-person pass · Not live yet, gaps #80 listed products’ contacts are Leads a person ownsPOST /{namespace}/tasks/{task}/assign
use-when pass · Not live yet, gaps #83 of your Startups in property software; 24 products to rank; 2,900 searches a monthPOST /{namespace}/metrics/query
baseline pass · Not live yet, gaps #80 hub visitors; 9 Customers a month from search across your StartupsPOST /{namespace}/metrics/query
approved-pages fail · Not live yet, gaps #8Publish the ranking rule Page; every list links to it.POST /{namespace}/sites/{site}/publish
domain-authenticated passthe hub’s Domain: 3 of 3 records resolvePOST /{namespace}/domains/{domain}/authenticate
role-unstaffed pass · Not live yet, gaps #8Marketing Lead is your agent; rule changes and corrections come to youPOST /{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.

CheckFix (one call)Your timeWait
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-customers1 minnone
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 correction30 minnone
domain-authenticated · role-unstaffed · pass · pass (gaps #8)—0 minnone

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,020 matured · held back at random

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

WhenTrigger idStarts onWindowHow it gets in
A visitor finishes the fit finderhub-fit-requestevent on Form.submitted where form = 'fit-finder', finished on a hub Pagethe shortlist within 2 min; ignored after 1 dthe hub’s fit-finder Form, through POST /{namespace}/forms
A listed product’s pricing or feature page changeshub-fact-changedevent on Monitor.checked where a listed product's pricing, plan, limits or feature Page changedthe row refreshes within 7 dPOST /{namespace}/monitors/{monitor}/check on each listed product
A listed product files a correctionhub-correction-filedevent on Form.submitted where form = 'correction', from a listed product's rowanswered within 5 business daysthe row’s correction Form
+ 4 more for new categories, stale facts, visibility drops and flat rules (?lanes=all)
One signed delivery · StartupBuilder-Signature
{
  "$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"
  }
}
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":"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 click

Who runs it

Marketing Lead · your agent (role:marketing-lead, 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.

OptionRung todayEvidence (passed of n)Moves up atDrops back whenYou can
Refresh the row · Send the shortlist · Tell a product it’s listedIt does it, you can stop it24 of 24 rows approved unedited, then 41 refreshes · 140 of 140 · 24 of 24met on day 15 (the first 24 rows)an independence fail 3 times in 7 days → You approve each onePOST /{namespace}/actions/{action}/cancel inside the hold
Answer the correctionIt does it; a changed rank waits for you6 answered, 2 rank changes approved—correction-backlog → new pages holdPOST /{namespace}/approvals/{approval}/approve
Open a hub · Propose a rule changeYou approve each one, on every rung1 of 1 · 0 proposednever—POST /{namespace}/changes/{change}/merge · POST /{namespace}/changes/{change}/revert
Hold new pagesIt does it1 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 outsideTo a person, every time1 refused, 0 publishednever—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.

The brief (folded): compiled Agent-register Markdown · play:category-hubs@1 · each line names its record key
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.

Terminal
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

OptionPicked whenDecided byRunsHands toIf it’s just you
Open a hub open-a-hub2+ of your Startups share a cluster, no hubYour rule (the category test)POST /{namespace}/sites → POST /{namespace}/sites/{site}/publish with the rule page firstyou approve it · Take this oneyou
Refresh the row refresh-the-rowa listed product’s page changed, or a fact went staleYour rule (the freshness budget)POST /{namespace}/monitors/{monitor}/check → PATCH /{namespace}/pages/{id}nobodythe same
Send the shortlist send-the-shortlista finished fit finder and an email askYour rule (line 5)hub-shortlist (POST /{namespace}/messages/{message}/schedule)the visitor’s clickthe same
Answer the correction answer-the-correctiona correction from a listed productAgent’s call, with its reasonhub-correction-reply → PATCH /{namespace}/pages/{id} if their page shows ityou, when a rank moves, due 5 business days · Take this oneyou
Tell a product it’s listed tell-them-they-are-listeda product’s first listingYour rule (the listing)hub-listed-notice, oncenobodythe same
Propose a rule change propose-a-rule-changethe top 10 within 2 % on one criterionAgent’s call, with its reasonPOST /{namespace}/changes with the new weights replayed on the current rowsyou decide it · Take this oneyou
Hold new pages not-nowa visibility drop, an index hold or a backlogYour rule (the hub Stops)nothing; names the Stop that holds new pagesnobodythe same
When they write backFixture replyWhat happensWho answersBy whenCall
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 LeaselineMarketing Lead · your agent5 minPOST /{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 readMarketing Lead · your agent5 minPOST /{namespace}/conversations/{conversation}/answer
Not now“Bookmarking this for spring.”nothing else sends; the shortlist page stays at its linknobodyat oncePOST /{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 textMarketing Lead · your agent1 business dayPOST /{namespace}/conversations/{conversation}/answer
Out of officean auto-reply until Oct 26nothing; the shortlist never follows upnobody—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 youyou1 business dayPOST /{namespace}/conversations/{conversation}/escalate
Stop“Don’t email me.”nothing else sends to them from any of your Startupsnobodyat oncePOST /{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).

What lands in each queue · sample
[
  {
    "$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)RefusesFixThen
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 ownsLeave 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 pickSend it to their CSM instead.POST /{namespace}/tasks/{task}/assign
independencea page that ranks by anything but the published rule, leaves your Startups unlabeled, or drops a product for being outsideRe-rank by the rule and label what is yours.PATCH /{namespace}/pages/{id}
Stop (pauses the Play)Pauses whenThen
index-holdover 30 % of hub pages crawled but not indexedthe next batch holds
visibilityorganic visits 30 % under the 4-week mean for 2 weeksnew pages freeze and a fact audit opens
steering-checkyour Startups’ share of fit clicks passes 1.5 times their share of top-3 slots, over 90 days and 300 clicksrule changes freeze
top-slotone of your Startups first on more than half the lists, or first on the indexa person re-reads every source behind those ranks
correction-backlogover 10 corrections past 5 business daysnew pages pause until the queue clears
freshness-floorunder 90 % of price facts inside their 30-day budgetnew pages pause
spam-actiona Venue spam or manual action notice on the hubevery publish pauses and you are told
kill-switchyou press itevery 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.

422RefusedThe “best for owner statements” list did not publish: it ranked Leaseline first on a criterion the rule page doesn’t weigh.
CheckFixThen
independence failRe-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.

The Stops that fired, as Campaign.paused reasons · sample
[
  {
    "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,020 matured · path → arr

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"
}
NodeAgent halfYour halfAddedConfidenceBasis
hub-routed-customers0.39 per 100 visitors (2 of 510), fit finder shown0.20 (1 of 510), table only+0.19 [−0.38, +0.76]■□□□ Too early to tellheld 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 tellthe 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 tellcarried 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 %.

The Campaign card · sample
{
  "$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=all; three follow the tables.

What happened, by optionDecisionsDoneCustomers to your Startups (what happened)
Open a hub · Refresh the row1 · 411 hub, 9 pages · 41 refreshed—
Send the shortlist · Tell a product it’s listed140 · 24140 sent, 61 % with an outside product first · 24 told2 from shortlists
Answer the correction · Propose a rule change · Hold new pages6 · 0 · 16 answered, 2 ranks moved · — · 9 days held—
Fit finder · table only840 visitors · 840510 matured · 5103 (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 monthheld back at random; about a year to readOther Startups’ results: +0.12 per 100, ■■□□ Early sign, n 41,000
DayEventWhat happenedYour time
0event_1127 Campaign.created24 products, the 5-criterion rule; the dry run fails approved-pages40 min
1event_1133 Site.publishedthe rule page, then the index and 8 lists30 min
1–15event_1136 Approval.created, first of 24you approved 24 rows unedited35 min
11event_1182 Order.placedthe first Customer from the hub, to Leaseline—
15event_1196 Role.updatedpromoted: rows and shortlists go after a 2-minute hold—
29event_1295 Action.refusedindependence refused a list ranked on a criterion the rule doesn’t weigh—
38 · 47event_1324 Campaign.pausedvisibility: visits 34 % under the mean; the fact audit found 2 stale prices, and pages resumed15 min
1–56event_1137 Task.created, first of 9corrections and the weekly memo, from the POST /{namespace}/tasks/next log40 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).

The owner-statements list, 2026-09-22 · independence refused it, then the list that published
- 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

plays/category-hubs.mdx
---
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.
Text
$ 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

Key Id What it is
Tactic hub-launch Hub launch censuses a category, fixes a ranking rule from a blind order, fills every row from public Pages and publishes the hub in batches. Starts at You approve each one.
Tactic hub-refresh Hub refresh re-reads every row source on a schedule, recomputes ranks from changed facts and publishes only real changes. Starts at You approve each one.
Tactic hub-route Hub route turns a Visitor’s answers into the top 3 fits by the published rule and counts every routed Customer, yours or not. Starts at You approve the first wave.
Tactic independence-guard Independence guard blocks any ranking or comparison Page that hides common ownership, departs from its computed order or claims independence from its owner. Starts at Sends nothing.
Tactic hub-corrections Hub corrections answers every listed product’s correction within 5 business days on one evidence bar, your own Startups included, and logs it in public. Starts at You approve each one.
Tactic hub-rule-change Hub rule change versions the ranking rule in public, shows every rank it moves a week ahead and holds any change that lifts one of your Startups. Starts at You approve each one.
Tactic hub-readout Hub readout counts routed Customers in three columns, runs the steering and decay checks, and publishes where Visitors went. Starts at Sends nothing.
Trigger hub-category-eligible Hub category eligible: state on Startup.advanced, window 30d.
Trigger hub-fact-changed Hub fact changed: event on Monitor.checked, window 7d.
Trigger hub-fact-stale Hub fact stale: at on Monitor.checked, window 30d.
Trigger hub-correction-filed Hub correction filed: event on Form.submitted, window 5d.
Trigger hub-fit-request Hub fit request: event on Form.submitted, window 1d.
Trigger hub-visibility-drop Hub visibility drop: threshold on Metric.queried, window 14d.
Trigger hub-rule-flat Hub rule flat: threshold on Metric.queried, window 90d.
Sequence hub-shortlist Hub shortlist: 2 steps; it ends on An answer to the question; An unsubscribe; A joined routed Customer.
Sequence hub-listed-notice Hub listed notice: 1 steps; it ends on One send; An opt-out; A filed correction.
Sequence hub-correction-reply Hub correction reply: 2 steps; it ends on The decision sent; An unsubscribe.
Benchmark self-promo-list-unrecommended Share of cited self-promoting best-software lists whose owner the Google AI Overviews answer does not recommend: 69 % (Search Engine Journal analysis of Google AI Overviews, 2026-06-18).
Benchmark ai-summary-result-clicks Searches on which the user clicks any result while an AI summary shows: 8 % (Pew Research Center, 2025-07-22).
Benchmark comparison-absorption-uplift Answer-influence uplift for pages carrying comparisons (low) and statistics (high): 55.3–61.5 % (From Citation Selection to Citation Absorption, arXiv 2604.25707, 2026-04-28).

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.