Editorial SEO
Editorial SEO writes the guides and honest comparisons your buyers search for, from your own records, then refreshes or prunes each Page that slips.
- Step
- Get found
- Channel
- Blog and search
- Kind
- Search
- Summary
- startupbuilder.dev
Be our Content Lead: write the guides and honest comparisons small property managers search for, from our own records. Hold back a third of the topics so we can tell. I approve every rival row. Show me what’s missing; don’t send.
# 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":"editorial-seo"}'import { $ } from 'startup-builder'
return await $.Campaign.create({ play: 'editorial-seo' }, { 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":"editorial-seo"}' --try # Not live yet (gaps #2){
"name": "do",
"arguments": {
"code": "return await $.Campaign.create({ play: 'editorial-seo' }, { 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": "editorial-seo can’t start yet: 1 check fails and 1 warns.",
"checks": [
{
"check": "use-when",
"status": "pass",
"observed": "22 searched query families with no Page of yours; Pricing Page live; 41 Customers",
"doc": "/plays/ editorial-seo#check-use-when",
"fix": "None needed: 3 families are enough.",
"then": "Metric.query"
},
…
Hand our latest search finding to the Content Lead agent and show me what it chose and why. Don’t send.
curl "https://api.sb/leaseline.app/events?where=name%20in%20(%22Keyword.measured%22%2C%20%22Monitor.checked%22%2C%20%22Fact.changed%22%2C%20%22Page.declined%22%2C%20%22Monitor.matched%22%2C%20%22Form.submitted%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_1512"}'
# Not live yet (gaps #7)
curl "https://api.sb/leaseline.app/decisions?where=on%20%3D%20%22keyword_44%22" \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"
# Not live yet (gaps #7)
curl "https://api.sb/leaseline.app/actions?where=on%20%3D%20%22keyword_44%22" \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"import { $ } from 'startup-builder'
const { events: [last] } = await $.Event.list({ where: 'name in ("Keyword.measured", "Monitor.checked", "Fact.changed", "Page.declined", "Monitor.matched", "Form.submitted")', 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 (\"Keyword.measured\", \"Monitor.checked\", \"Fact.changed\", \"Page.declined\", \"Monitor.matched\", \"Form.submitted\")","limit":1}'
npx startup-builder do Trigger.fire --json '{"like":"event_1512"}' --try # Not live yet (gaps #7)
npx startup-builder do Decision.list --json '{"where":"on = \"keyword_44\""}' # Not live yet (gaps #7)
npx startup-builder do Action.list --json '{"where":"on = \"keyword_44\""}' # Not live yet (gaps #7){
"name": "do",
"arguments": {
"code": "const { events: [last] } = await $.Event.list({ where: 'name in (\"Keyword.measured\", \"Monitor.checked\", \"Fact.changed\", \"Page.declined\", \"Monitor.matched\", \"Form.submitted\")', 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_462",
"$type": "Decision",
"choice": "write-a-comparison",
"reason": "alternatives to the largest property-management suite, for under 100 units, runs 2, 400 searches a month, over your 300 floor, and no Page of yours targets it",
"stakes": "medium",
"options": [
"write-a-comparison",
"write-a-guide",
"refresh-the-page",
"merge-or-prune",
"ask-for-a-mention",
"hide-a-disputed-row"
…
sample · 4 Not live yetPreviewOn PATCH /{namespace}/campaigns/{id}Off PATCH /{namespace}/campaigns/{id}Take this one POST /{namespace}/tasks/{task}/claim
action_3 sends you the 14 rival rows, each with its capture; action_4 publishes once you approve them. The Page names Leaseline as its publisher and seller in its first 60 words.
+118 visits per family a month · ■■□□ Early sign · n 24 families · path → arr
| Try another: the query family | Choice | Reason |
|---|---|---|
| Alternatives to the largest suite, for under 100 units · 2,400 a month · no Page | write-a-comparison | over the 300 floor; 14 rival rows go to you |
| How to send a 60-day renewal notice · 880 a month | write-a-guide | your records answer it: 3,520 renewals |
| Guide “Lease renewal letter template” dropped 4 positions on two weekly reads | refresh-the-page | its example still shows a 30-day notice; the template changed Oct 2 |
| Guide on move-out checklists: never indexed after 90 days | merge-or-prune | the move-out guide answers it; merged behind a redirect |
| A rival writes: “Your row says we charge per unit. We don’t.” | hide-a-disputed-row | a written dispute hides the row within 24 h until you review it |
- 70 min week 1, then 15 a day
- Your time
- Day 12: first Page indexed
- First signal
Where it sits: Stranger → Lead → Trialing → Activated → At risk → Churned → Won back · Organic visits → People reached → New Customers ↑ → ARR added each week → ARR
Worth’s formula: about +118 organic visits per Page a month × 4 new Pages a month × 0.6 ICP share × 0.5 paid per 100 visits × $347 first order ARR ≈ +$491. Under First signal: About 12 weeks to tell the leaf’s +118 lift per family at 16 and 8 families; dollars take longer, so new-arr reads Too early to tell.
sample · api.sb/
Start it this week
npx startup-builder add play editorial-seo # 1 · writes plays/editorial-seo.mdx (Not live yet, gaps #5)
# 2 · dry run; below, its 422 on 2026-08-24
curl -X POST "https://api.sb/leaseline.app/campaigns?mode=try" \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"play":"editorial-seo"}'
# 3 · keep it, at You approve each one
curl -X POST https://api.sb/leaseline.app/campaigns \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"play":"editorial-seo"}'
# on
curl -X PATCH https://api.sb/leaseline.app/campaigns/editorial-seo \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"endsAt":null}'
# off: no new Page or request; published Pages stay up
curl -X PATCH https://api.sb/leaseline.app/campaigns/editorial-seo \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"endsAt":"now"}' | Check | Fix | Then |
|---|---|---|
use-when pass · Not live yet, gaps #8 | None needed: 3 families are enough. | POST /{namespace}/metrics/query |
baseline pass · Not live yet, gaps #8 | None needed: 1,240 in 90 days is your Before. | POST /{namespace}/metrics/query |
approved-pages fail · Not live yet, gaps #8 | Publish dated fact records for Price, plans and 3 integrations before a row cites them. | POST /{namespace}/sites/{site}/publish |
role-unstaffed warn · Not live yet, gaps #8 | Staff the Content Lead, or leave it: every rival row comes to you. | POST /{namespace}/roles/{role}/staff |
JSON
{
"type": "https://startupbuilder.dev/errors/refused",
"title": "Refused",
"status": 422,
"detail": "editorial-seo can’t start yet: 1 check fails and 1 warns.",
"checks": [
{
"check": "use-when",
"status": "pass",
"observed": "22 searched query families with no Page of yours; Pricing Page live; 41 Customers",
"doc": "/plays/editorial-seo#check-use-when",
"fix": "None needed: 3 families are enough.",
"then": "Metric.query"
},
{
"check": "baseline",
"status": "pass",
"observed": "1,240 organic visits in 90 days, 6 Pages",
"doc": "/plays/editorial-seo#check-baseline",
"fix": "None needed: 1,240 in 90 days is your Before.",
"then": "Metric.query"
},
{
"check": "approved-pages",
"status": "fail",
"doc": "/plays/editorial-seo#check-approved-pages",
"fix": "Publish dated fact records for Price, plans and 3 integrations before a row cites them.",
"then": "Site.publish"
},
{
"check": "role-unstaffed",
"status": "warn",
"roles": [
"content-lead"
],
"owned": 0,
"doc": "/plays/editorial-seo#check-role-unstaffed",
"fix": "Staff the Content Lead, or leave it: every rival row comes to you.",
"then": "Role.staff"
}
]
}It says no, says why, and names the fix.
| Check | Fix (one call) | Your time | Wait |
|---|---|---|---|
use-when · baseline · pass: 22 families with no Page; 1,240 organic visits in 90 days (gaps #8) | POST /{namespace}/metrics/query on organic-visits, which fills Before in Your number | 1 min | none |
approved-pages · fail (Not live yet) | POST /{namespace}/sites/{site}/publish the facts Page with Price, plans and 3 integrations | 25 min | none |
role-unstaffed · warn (gaps #8): no Content Lead seat | POST /{namespace}/roles/{role}/staff, or leave it: every rival row comes to you | 0 min | none |
Your number · Before 22 per family · Now 140 organic visits per family a month · Target 150 by Dec 31 · ■■□□ Early sign · held back at random · n 24 families
Each market-data read costs about $0.25 (Worker cost), about 4 a month, and drafting a Page costs about $0.30. That comes to about $1 a month at 4 Pages, beside your 10 minutes per comparison.
When it starts
| When | Trigger id | Starts on | Window | How it gets in |
|---|---|---|---|---|
| A searched query family clears the floor with no Page of yours | comparison-query-found | threshold on Keyword.measured where family.volume >= demand floor and no Page targets it | read weekly; 90d | POST /{namespace}/keywords for each family; POST /{namespace}/keywords/{keyword}/measure reads it weekly |
| A Page drops 3+ positions on two weekly reads | page-rank-loss | threshold on Keyword.measured where position drop >= 3 on 2 weekly reads in a row | 14d | the same weekly POST /{namespace}/keywords/{keyword}/measure |
+ 3 more for changed facts, slipping Pages and Pages never indexed (?lanes=) |
{
"$id": "https://api.sb/leaseline.app/events/event_1512" ,
"$type": "Event",
"name": "Keyword.measured",
"at": "2026-10-19T05:30:44Z" ,
"seq": 1512,
"data": {
"keyword": {
"$id": "https://api.sb/leaseline.app/keywords/keyword_44" ,
"family": "alternatives to the largest property-management suite, under 100 units" ,
"volume": 2400,
"floor": 300,
"page": null
}
}
}npx startup-builder trigger Keyword.measured --like event_1512 # replays the delivery
npx startup-builder clock advance --by 28d # then runs the index check on the new Page
npx startup-builder listen Keyword.measured --forward-to http://localhost:3000/hook Who runs it
Content Lead · your agent (role:, worker_7) holds the Key Result. It drafts every Page from your records and captures every rival row; each comparison and each disputed row comes to you (Dana Okafor, person, worker_1), who approves the rows and the byline. Your number links to Did it work.
| Option | Rung today | Evidence (passed of n) | Moves up at | Drops back when | You can |
|---|---|---|---|---|---|
| Write a guide · Refresh the Page | It does it, you can stop it | 9 of 9 · 4 of 4 | met on day 19 (the first 5 guides) | a published sentence fails claims-resolve → You approve each one | POST /{namespace}/actions/{action}/cancel inside the hold |
| Write a comparison · Hide a disputed row | You approve each one | 7 of 7, 92 rows · 1 of 1 | never: rival rows stay with you | — | POST /{namespace}/approvals/{approval}/approve · POST /{namespace}/tasks/{task}/claim |
| Merge or prune · Ask for a mention | It does it | 2 of 2 · 6 of 6 | — | a mention complaint → paused | POST /{namespace}/actions/{action}/redirect |
| Change the backlog rule | You approve each one | 1 merged of 1 | never | — | POST /{namespace}/changes/{change}/merge · POST /{namespace}/changes/{change}/revert |
| Never: a ranked list with you in it, an estimated rival price, a date moved with no fact changed, an invented byline | To a person, every time | 0 asked | never | — | POST /{namespace}/tasks/{task}/claim |
What it reads · What it may call. It reads your fact records, rival Pages as they publish them, and weekly search reads. A Visit, a rank or a rival change picks which Page to work and never appears in a Message. Its tools are POST /{namespace}/content/draft, POST /{namespace}/approvals, POST /{namespace}/content/{content}/publish, PATCH /{namespace}/content/{id}, PATCH /{namespace}/pages/{id}, POST /{namespace}/messages/{message}/schedule, POST /{namespace}/tasks/{task}/assign, and any other call becomes an Approval under the Role’s Mandate.
You hold the Content Lead Key Result: 150 organic visits per family a month by 2026-12-31. ← worker, metric
On each weekly read or change, choose one option and say why. ← triggers, chooses
Name Leaseline as publisher and seller in the first 60 words. ← copyRules 2
Show every tested row a rival wins; never estimate a hidden price. ← copyRules 6, 7
Move a Page’s date only when a Fact it states changes. ← copyRules 4
Hold back 1 family in 3 at random; never draft a held-back family. ← your file, line 2 The slow loop, as a Change. decision_421 (2026-09-21, weekly) read 3 kept families under 400 searches that drew under 20 visits a month each, so change_27 raised the demand floor from 200 to 300 (your file, line 4) and Dana merged it with POST /{namespace}/changes/{change}/merge. Replayed on past reads, POST /{namespace}/changes/{change}/review dropped 3 of 22 families; read back at +28d, the kept families average 140 visits, n 16, ■■□□ Early sign.
Bring your own agent (folded) starts at It does it, you can stop it on guides when it matches or beats the seated agent on the eval suite; comparisons stay with you either way; POST /{namespace}/roles/{role}/staff seats it.
npx startup-builder eval --scenario editorial-seo-24 --agent "claude -p" --seat content-lead # seated: 92 of 92 rows captured, 0 Checks failed sent, 14 of 16 indexed · yours: 90 of 92, 2 failed `fact-check` What it can do next
| Option | Picked when | Decided by | Runs | Hands to | If it’s just you |
|---|---|---|---|---|---|
Write a comparison write-a-comparison | a versus, alternatives or rival-pair family clears the floor | Your rule (line 4) | publish-content → POST /{namespace}/content/draft with captures → POST /{namespace}/approvals for the rows | POST /{namespace}/tasks/{task}/assign → you, due 1 business day · Take this one | you |
Write a guide write-a-guide | a how-to family clears the floor, and your records answer it | Agent’s call, with its reason | publish-content → POST /{namespace}/content/draft → POST /{namespace}/content/{content}/publish | nobody | the same |
Refresh the Page refresh-the-page | a cited fact or rival Page changed, or a Page slipped | Agent’s call, with its reason | PATCH /{namespace}/content/{id} → POST /{namespace}/content/{content}/publish | nobody | the same |
Merge or prune merge-or-prune | never indexed after 90 days, or another Page answers it | Agent’s call, with its reason | PATCH /{namespace}/pages/{id} to the nearest Page, behind a redirect | nobody | the same |
Ask for a mention ask-for-a-mention | a ranking Page misstates you or leaves you out | Agent’s call, with its reason | editorial-mention-request → POST /{namespace}/messages/{message}/schedule | the Page’s author | the same |
Hide a disputed row hide-a-disputed-row | a rival disputes a row in writing | Your rule (the dispute hold) | PATCH /{namespace}/pages/{id} hides the row → POST /{namespace}/tasks/{task}/assign | you review it, due 1 business day · Take this one | you |
| When they write back | Fixture reply | What happens | Who answers | By when | Call |
|---|---|---|---|---|---|
| Interested | “Happy to add you. Send a line and a link.” | answers with one sourced line and the Page link, never a link ask of its own | Content Lead · your agent | 1 business day | POST /{namespace}/conversations/{conversation}/answer |
| Send me pricing | “What does Leaseline cost at 40 units?” | answers with the Pricing Page’s list Price, $29 a month | Content Lead · your agent | 1 business day | POST /{namespace}/conversations/{conversation}/answer |
| Not now | “We revise that list in January.” | the follow-up stops; the Decision reopens at their revision | nobody | at once | POST /{namespace}/enrollments/{enrollment}/cancel |
| Wrong person | “Our editor handles that list.” | asks them to forward it; it never writes to an address from their text | Content Lead · your agent | 1 business day | POST /{namespace}/conversations/{conversation}/answer |
| Out of office | an auto-reply until Oct 28 | the follow-up waits for the return date | nobody | the return date | POST /{namespace}/messages/{message}/schedule |
| Question it can’t answer | “Would you pay for a placement?” | To a person: paid placement is your call, and it carries their sponsored label | you | 1 business day | POST /{namespace}/conversations/{conversation}/escalate |
| Stop | “Please don’t email us again.” | that author is suppressed for all your Startups | nobody | at once | POST /{namespace}/contacts/{contact}/suppress |
When it hands to you
Comparisons and disputed rows come to you: every rival row needs your OK. One Brief per Role, as captured (brief is Not live yet, gaps #7).
[
{
"$id": "https://api.sb/leaseline.app/tasks/task_588" ,
"$type": "Task",
"assignee": "worker_1",
"due": "2026-10-20T05:30:00Z" ,
"on": "keyword_44",
"brief": {
"what": "Alternatives to the largest property-management suite, for under 100 units" ,
"rows": 14,
"captured": "2026-10-18, from their own public Pages" ,
"rivalWins": [
"owner portals",
"accounting"
],
"blank": [
"their price under 100 units: not published"
],
"never": [
"an estimated price",
"a ranked list with Leaseline in it"
]
}
},
{
"$id": "https://api.sb/leaseline.app/approvals/approval_71" ,
"$type": "Approval",
"verdict": null,
"deadline": "2026-10-20T05:30:00Z" ,
"draft": "Published by Leaseline, which sells renewal software. For under 100 units, three options fit…" ,
"route": "write-a-comparison",
"checks": [
{
"check": "claims-resolve",
"status": "pass"
},
{
"check": "fact-check",
"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; an Approval unanswered by its deadline expires and the Page stays in draft. Once you claim a dispute, the agent’s pending edits on that Page cancel with claimedByPerson (Not live yet, gaps #7). A Task nobody claims by its due time goes up through POST /{namespace}/tasks/{task}/escalate, and person-promise fires.
On time: you, 8 of 8 claimed within their due time, median 4 h 20 min, n 8, ■□□□ Too early to tell beside Other Startups’ 93 % on time. Your queue: counted by kind: in week 1, 3 comparisons and 2 guides; since then about 1 comparison a week, 1 dispute in the window and the weekly review.
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 mention request to an author whose Conversation a person claimed | Leave it to the person who owns it: the draft waits in their queue. | POST /{namespace}/tasks/{task}/assign |
claims-resolve (shared) | a sentence the claims source or a rival capture lacks | Cut the claim, or name the record that backs it. | POST /{namespace}/content/draft |
fact-check (shared) | a rival row with no capture, or a price the rival does not publish | Capture the rival’s own Page, or leave the row blank. | POST /{namespace}/content/draft |
dispute-hold (the record’s Dispute hold) | a row a rival disputed in writing, until you review it | Review it with a fresh capture; it stays hidden meanwhile. | POST /{namespace}/tasks/{task}/claim |
The captured refusal, then the Stop that fired as a Campaign.paused reason (Not live yet, gaps #2); the rewrite that published is the second thread in Example.
| Check | Fix | Then |
|---|---|---|
fact-check fail · Not live yet, gaps #1 | Capture the rival’s own Page, or leave the row blank. | POST /{namespace}/content/draft |
claims-resolve pass | — | POST /{namespace}/content/draft |
JSON
{
"type": "https://startupbuilder.dev/errors/refused",
"title": "Refused",
"status": 422,
"detail": "The comparison did not publish: a rival’s price row cites no capture, and their Pricing Page shows no price under 100 units.",
"checks": [
{
"check": "fact-check",
"status": "fail",
"doc": "/plays/editorial-seo#check-fact-check",
"fix": "Capture the rival’s own Page, or leave the row blank.",
"then": "Content.draft"
},
{
"check": "claims-resolve",
"status": "pass"
}
]
}It says no, says why, and names the fix.
[
{
"reason": {
"limit": "index",
"observed": "4 of 11 Pages crawled, not indexed at 28 days (36 %)" ,
"bound": "30 %",
"window": "28d"
},
"doc": "/plays/editorial-seo#stop-index" ,
"task": "task_576"
}
]Did it work
The 24 kept families were assigned at random, 16 drafted and 8 held back, so both halves read the same assignment (rules m–o); each figure’s call is POST /{namespace}/metrics/query by node and by arm, and every node above the leaf is Not live yet (gaps #4).
+118 visits per family a month · ■■□□ Early sign · n 24 families · path → arr
- Chose
- write-a-comparison
- Why
- alternatives to the largest property-management suite, for under 100 units, runs 2,400 searches a month, over your 300 floor, and no Page of yours targets it
- From
- write-a-comparison · write-a-guide · refresh-the-page · merge-or-prune · ask-for-a-mention · hide-a-disputed-row
- Expects
- the Page indexed within 28d, and organic visits past 100 a month by day 60
- Reopens when
- Monitor.matched page-not-indexed for keyword_44’s Page within 28d
- By
- worker_7
JSON
{
"$id": "decisions/decision_462",
"$type": "Decision",
"choice": "write-a-comparison",
"reason": "alternatives to the largest property-management suite, for under 100 units, runs 2,400 searches a month, over your 300 floor, and no Page of yours targets it",
"stakes": "medium",
"options": [
"write-a-comparison",
"write-a-guide",
"refresh-the-page",
"merge-or-prune",
"ask-for-a-mention",
"hide-a-disputed-row"
],
"by": "worker_7",
"on": "keyword_44",
"expected": "the Page indexed within 28d, and organic visits past 100 a month by day 60",
"reopensWhen": "Monitor.matched page-not-indexed for keyword_44’s Page within 28d"
}| Node | Drafted half | Held-back half | Added | Confidence | Basis |
|---|---|---|---|---|---|
organic-visits | 140 per family a month (2,240 over 16) | 22 (176 over 8) | +118 [+52, +184] | ■■□□ Early sign | held back at random · n 24 families · 5 Pages under 60 days still maturing |
icp-reached | 84 ICP visitors per family a month | 14 | +70 [+18, +122] · carrying up | ■■□□ Early sign | the same assignment |
new-arr ↑ | $2,082, 6 Customers on Pro · Booked: credited, not all caused by this Play | $347, 1 Customer (×2 for the half’s size: $694) | +$1,388 [−$700, +$3,500] · Worth about +$490 a month | ■□□□ Too early to tell (6 vs 1 Customers) | the same · 7 trials still deciding · Other Startups’ results +$410 a month per 4 Pages, ■■□□ Early sign, n 380 Startups |
net-new-arr · arr | +$2,082 | +$694 | +$1,388, carried: it moves no other bridge term | ■□□□ Too early to tell | carried from new-arr |
The leaf counts organic visits per kept query family over 28 days, read monthly, to 20 families in each half; at 16 and 8, the Experiment runs on, and the held-back families draft at day 90.
How it moves it: each drafted family has a sourced guide or comparison of yours on its first results page; held-back families have none.
Guardrails: drafted against held back: trials per 100 ICP visitors, 1.4 against 1.3, and site-wide organic visits, 3,180 against a 28-day baseline of 2,950, so no visibility Stop.
{
"$id": "https://api.sb/leaseline.app/campaigns/editorial-seo" ,
"$type": "Campaign",
"play": "play:editorial-seo@1" ,
"path": [
"organic-visits",
"icp-reached",
"new-arr",
"net-new-arr",
"arr"
],
"experiment": {
"metric": "organic-visits",
"unit": "per-family",
"heldBack": 8,
"decision": null
},
"readout": {
"value": 118,
"n": 24,
"maturing": 5,
"interval": [
52,
184
],
"readability": "C",
"basis": "held-back",
"pooled": false
},
"ledger": {
"spend": 2,
"workerCost": 9,
"hours": 4.1,
"openArr": 2429,
"wonArr": 2082,
"projectedArr": 491,
"paybackDays": 4
}
}Outside benchmark: clicks on the top result fell 58 % when an AI overview showed above it (a search-data vendor’s click-through study, in Your settings).
Before it · After it. Before it: positioning-tournament sets the words the Pages use. After it: aeo hears the Pages answer engines cite, directory-listings sends the comparers to your listings, and instant-response answers the readers who write in.
Example
Leaseline ran it from 2026-08-24 to 2026-10-19 (56 days): 24 query families kept, 16 drafted and 8 held back at random; 16 Pages published, 14 indexed, 2 merged. Read every captured thread at ?threads=; three follow the tables.
| What happened, by option | Decisions | Published | Organic visits a month |
|---|---|---|---|
| Write a comparison | 7 | 7, 92 rows you approved | 1,210 |
| Write a guide · Refresh the Page | 9 · 4 | 9 · 4 | 1,030 · 3 Pages back in the top 5 |
| Merge or prune · Ask for a mention · Hide a disputed row | 2 · 6 · 1 | 2 redirects · 2 entries added · 1 row corrected | — |
| Drafted half · held-back half | 16 families | 16 Pages · 0 | 2,240 · 176 |
| Day | Event | What happened | Your time |
|---|---|---|---|
| 0 | event_1470 Campaign.created | the three steps; the dry run fails approved-pages | 20 min |
| 0 | event_1471 Site.published | the facts Page: Price, plans and 3 integrations, dated | 25 min |
| 1–14 | event_1474 Approval.created, first of 7 | you approved 92 rival rows across 7 comparisons | 70 min |
| 12 | event_1480 Page.indexed | the first Page indexed: How to send a 60-day renewal notice | — |
| 19 | event_1486 Role.updated | promoted: guides and refreshes publish after a 1-hour hold | — |
| 28 · 34 | event_1492 Change.merged · event_1497 Campaign.paused | the 300 floor; index: 4 of 11 unindexed, resumed when 2 indexed and 2 merged | 15 min |
| 44 | event_1503 Task.assigned | a rival disputed one row; hidden in 12 min, corrected by you the same day | 20 min |
| 1–56 | event_1476 POST /{namespace}/tasks/next, weekly | the queue: 15 minutes a day in week 1, then about 10, from the POST /{namespace}/tasks/next log | 1 h 45 min · total 4.1 h measured, 4 h asked |
Page, published Sep 6 · “How to send a 60-day renewal notice” · Published by Leaseline, which sells renewal software.¹ Across 3,520 renewals at 38 property managers, leases noticed 60+ days out renewed at 71 % (n 1,940).² Reviewed by Dana Okafor.³
Oct 19 · 410 organic visits in 28 days, position 3; 2 trials started from it.
- Their plan costs about $1.50 a unit.
+ Their Pricing Page shows no price under 100 units (captured 2026-09-18), so this row is blank. A rival’s marketing lead, 2026-10-07 09:40 · “Your row says we charge per unit. We don’t.” · The row was hidden at 09:52, and the Brief went to you (
task_581), due 09:40 on Oct 8; Dana, Oct 7, 15:10 · *checked their Pricing Page, corrected the row with a new capture, and replied.*⁴
¹ The first 60 words name the publisher and the seller. ² The number comes from your records, with its n. ³ The byline names the person who reviewed it. ⁴ A disputed row stays hidden until a person reviews it.
Your settings
---
holdout: 0.33 # 1 kept query family in 3 is held back at random
---
Write guides and honest comparisons for families of 300 searches a month or more. Dana approves every rival row.$ npx startup-builder show plays/editorial-seo
holdout 0.33 · floor 300 a month · rival rows to Dana your file plays/editorial-seo.mdx:2,4 (Ask 2026-08-24; line 4 by change_27, 2026-09-21)
family demand · process content-marketing · worker role:content-lead, owns never · chooses 6 (2 rule, 4 read) · sequence editorial-mention-request · approval every-send · checks owned-by-a-person, claims-resolve, fact-check, dispute-hold · stops 7 · placements google-search, bing, google-ai-overviews, chatgpt, gmail catalog play:editorial-seo@1 (Not live yet, gaps #5) Every key from the catalog
Compiled, not written: edit the Play record editorial-seo 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.