URL: /plays/category-hubs · Id: `play:category-hubs`

# 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.

Ranks every product in a category by one published rule, labels your own, and sends each visitor to the best fit, outside products included.

```text title="Set it up · Ask"
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.
```

```text title="Run one · Ask"
Hand the latest fit-finder visitor to the Marketing Lead agent and show me what it chose and why. Don’t send.
```

```ts title="Run one · SDK"
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 }
```

```json title="Run one · Response · sample"
{
  "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",
      "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"
  },
  "actions": [
    {
      "$id": "https://api.sb/leaseline.app/actions/action_1",
      "$type": "Action",
      "status": "done",
      "mode": "try",
      "by": "worker_6",
      "verb": "Collection.query",
      "checks": [
        {
          "check": "independence",
          "status": "pass"
        }
      ]
    },
    {
      "$id": "https://api.sb/leaseline.app/actions/action_2",
      "$type": "Action",
      "status": "pending",
      "reason": "awaitingTime",
      "mode": "try",
      "by": "worker_6",
      "verb": "Message.schedule",
      "checks": [
        {
          "check": "owned-by-a-person",
          "status": "pass"
        },
        {
          "check": "customer-collision",
          "status": "pass"
        },
        {
          "check": "independence",
          "status": "pass"
        }
      ]
    }
  ]
}
```

`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 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 |

| Worth | Your time | Spend | First signal | Use it when | Instead |
| --- | --- | --- | --- | --- | --- |
| ≈ +$680 ARR a month at 900 visitors | 1 h week 1, then 20 min a week | $0.08 per page refresh, ~$15 a month | Day 11: first Customer from the hub | 2+ of your Startups in a category, 500+ searches a month | Under 10 products to rank: `editorial-seo` · no published rule: publish it (<Op verb="Site.publish" />) |

**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
```bash
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"
```


```json title="The dry run’s 422 · 2026-08-24 · sample"
{
  "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"
    }
  ]
}
```

| Check | Fix (one call) | Your time | Wait |
| --- | --- | --- | --- |
| <a id="check-owned-by-a-person"></a>`owned-by-a-person` · <a id="check-use-when"></a>`use-when` · <a id="check-baseline"></a>`baseline` · pass: 0 owned; 3 Startups, 24 products; 9 Customers a month from search (gaps #8) | <Op verb="Metric.query" /> on `hub-routed-customers` | 1 min | none |
| <a id="check-approved-pages"></a>`approved-pages` · fail: no ranking rule Page (Not live yet) | <Op verb="Site.publish" /> the rule, its weights and how a product files a correction | 30 min | none |
| <a id="check-domain-authenticated"></a>`domain-authenticated` · <a id="check-role-unstaffed"></a>`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,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
| 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 <Op verb="Form.create" /> |
| 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 | <Op verb="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=all`) | | | | |

```json title="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"
  }
}
```

```bash title="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.

| 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 | <Op verb="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 | <Op verb="Approval.approve" /> |
| Open a hub · Propose a rule change | You approve each one, on every rung | 1 of 1 · 0 proposed | never | — | <Op verb="Change.merge" /> · <Op verb="Change.revert" /> |
| Hold new pages | It does it | 1 hold, 9 days | — | — | <Op verb="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 | — | <Op verb="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 <Op verb="Site.create" />, <Op verb="Site.publish" />, <Op verb="Monitor.check" />, <Op verb="Page.update" />, <Op verb="Message.schedule" />, <Op verb="Change.create" />, and any other call becomes an Approval under the Role’s Mandate.

```md title="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.

```bash
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) | <Op verb="Site.create" /> → <Op verb="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) | <Op verb="Monitor.check" /> → <Op verb="Page.update" /> | nobody | the same |
| Send the shortlist `send-the-shortlist` | a finished fit finder and an email ask | Your rule (line 5) | `hub-shortlist` (<Op verb="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` → <Op verb="Page.update" /> 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 | <Op verb="Change.create" /> 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 | <Op verb="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 | <Op verb="Conversation.answer" /> |
| Not now | “Bookmarking this for spring.” | nothing else sends; the shortlist page stays at its link | nobody | at once | <Op verb="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 | <Op verb="Conversation.answer" /> |
| Out of office | an auto-reply until Oct 26 | nothing; the shortlist never follows up | nobody | — | <Op verb="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 | <Op verb="Conversation.escalate" /> |
| Stop | “Don’t email me.” | nothing else sends to them from any of your Startups | nobody | at once | <Op verb="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).

```json title="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 <Op verb="Task.claim" /> (Take this one), <Op verb="Task.return" /> with a reason, <Op verb="Approval.approve" /> or <Op verb="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:** <Op verb="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. | <Op verb="Task.assign" /> |
| <a id="check-customer-collision"></a>`customer-collision` (Not live yet, gaps #1) | a shortlist to an active Customer of the top pick | Send it to their CSM instead. | <Op verb="Task.assign" /> |
| <a id="check-independence"></a>`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. | <Op verb="Page.update" /> |

| Stop (pauses the Play) | Pauses when | Then |
| --- | --- | --- |
| <a id="stop-index-hold"></a>`index-hold` | over 30 % of hub pages crawled but not indexed | the next batch holds |
| <a id="stop-visibility"></a>`visibility` | organic visits 30 % under the 4-week mean for 2 weeks | new pages freeze and a fact audit opens |
| <a id="stop-steering-check"></a>`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 |
| <a id="stop-top-slot"></a>`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 |
| <a id="stop-correction-backlog"></a>`correction-backlog` | over 10 corrections past 5 business days | new pages pause until the queue clears |
| <a id="stop-freshness-floor"></a>`freshness-floor` | under 90 % of price facts inside their 30-day budget | new pages pause |
| <a id="stop-spam-action"></a>`spam-action` | a Venue spam or manual action notice on the hub | every publish pauses and you are told |
| <a id="stop-kill-switch"></a>`kill-switch` | you press it | every Tactic pauses, the dataset and fit finder freeze; <Op verb="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.

```json title="The captured refusal · sample"
{
  "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"
    }
  ]
}
```


```json title="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 <Op verb="Metric.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`

| 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 %.

```json title="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 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 <Op verb="Task.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).

```diff title="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
```mdx title="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 | <a id="tactic-hub-launch"></a>`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 | <a id="tactic-hub-refresh"></a>`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 | <a id="tactic-hub-route"></a>`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 | <a id="tactic-independence-guard"></a>`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 | <a id="tactic-hub-corrections"></a>`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 | <a id="tactic-hub-rule-change"></a>`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 | <a id="tactic-hub-readout"></a>`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 | <a id="trigger-hub-category-eligible"></a>`hub-category-eligible` | Hub category eligible: `state` on `Startup.advanced`, window 30d. |
| Trigger | <a id="trigger-hub-fact-changed"></a>`hub-fact-changed` | Hub fact changed: `event` on `Monitor.checked`, window 7d. |
| Trigger | <a id="trigger-hub-fact-stale"></a>`hub-fact-stale` | Hub fact stale: `at` on `Monitor.checked`, window 30d. |
| Trigger | <a id="trigger-hub-correction-filed"></a>`hub-correction-filed` | Hub correction filed: `event` on `Form.submitted`, window 5d. |
| Trigger | <a id="trigger-hub-fit-request"></a>`hub-fit-request` | Hub fit request: `event` on `Form.submitted`, window 1d. |
| Trigger | <a id="trigger-hub-visibility-drop"></a>`hub-visibility-drop` | Hub visibility drop: `threshold` on `Metric.queried`, window 14d. |
| Trigger | <a id="trigger-hub-rule-flat"></a>`hub-rule-flat` | Hub rule flat: `threshold` on `Metric.queried`, window 90d. |
| Sequence | <a id="sequence-hub-shortlist"></a>`hub-shortlist` | Hub shortlist: 2 steps; it ends on An answer to the question; An unsubscribe; A joined routed Customer. |
| Sequence | <a id="sequence-hub-listed-notice"></a>`hub-listed-notice` | Hub listed notice: 1 steps; it ends on One send; An opt-out; A filed correction. |
| Sequence | <a id="sequence-hub-correction-reply"></a>`hub-correction-reply` | Hub correction reply: 2 steps; it ends on The decision sent; An unsubscribe. |
| Benchmark | <a id="benchmark-self-promo-list-unrecommended"></a>`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 | <a id="benchmark-ai-summary-result-clicks"></a>`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 | <a id="benchmark-comparison-absorption-uplift"></a>`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). |