Positioning tournament
Tests three ways of saying what you do, as ads that differ in one phrase, and counts only signups that fit and act.
- Channel
- Signup flows
- Summary
- startupbuilder.dev
Be our Marketing Lead for positioning: test three ways of saying what Leaseline does, as LinkedIn ads that differ only in the headline, and count only signups who fit our ICP and set up a lease within 14 days. Rerun the best 6 against our current headline. I make the pick. Show me what’s missing; don’t spend.
# 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":"positioning-tournament"}'import { $ } from 'startup-builder'
return await $.Campaign.create({ play: 'positioning-tournament' }, { 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":"positioning-tournament"}' --try # Not live yet (gaps #2){
"name": "do",
"arguments": {
"code": "return await $.Campaign.create({ play: 'positioning-tournament' }, { 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": "positioning-tournament can’t start yet: 1 check fails and 1 warns.",
"checks": [
{
"check": "use-when",
"status": "fail",
"observed": "the Story has its ICP and Problem; 22 quotes from 8 ICP members",
"doc": "/plays/ positioning-tournament#check-use-when",
"fix": "Add 8 more quotes from 2 more ICP members; your interview notes hold them.",
"then": "Interview.create"
},
…
Hand our latest tournament round to the Marketing Lead agent and show me what it chose and why. Don’t change anything.
curl "https://api.sb/leaseline.app/events?where=name%20in%20(%22Story.revised%22%2C%20%22PlayMix.compiled%22%2C%20%22Dashboard.scheduled%22%2C%20%22Monitor.checked%22%2C%20%22Conversation.closed%22%2C%20%22Lead.qualified%22%2C%20%22Metric.watched%22%2C%20%22Territory.requested%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_1418"}'
# Not live yet (gaps #7)
curl "https://api.sb/leaseline.app/decisions?where=on%20%3D%20%22experiment_11%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%22experiment_11%22" \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"import { $ } from 'startup-builder'
const { events: [last] } = await $.Event.list({ where: 'name in ("Story.revised", "PlayMix.compiled", "Dashboard.scheduled", "Monitor.checked", "Conversation.closed", "Lead.qualified", "Metric.watched", "Territory.requested")', 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 (\"Story.revised\", \"PlayMix.compiled\", \"Dashboard.scheduled\", \"Monitor.checked\", \"Conversation.closed\", \"Lead.qualified\", \"Metric.watched\", \"Territory.requested\")","limit":1}'
npx startup-builder do Trigger.fire --json '{"like":"event_1418"}' --try # Not live yet (gaps #7)
npx startup-builder do Decision.list --json '{"where":"on = \"experiment_11\""}' # Not live yet (gaps #7)
npx startup-builder do Action.list --json '{"where":"on = \"experiment_11\""}' # Not live yet (gaps #7){
"name": "do",
"arguments": {
"code": "const { events: [last] } = await $.Event.list({ where: 'name in (\"Story.revised\", \"PlayMix.compiled\", \"Dashboard.scheduled\", \"Monitor.checked\", \"Conversation.closed\", \"Lead.qualified\", \"Metric.watched\", \"Territory.requested\")', 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_596",
"$type": "Decision",
"choice": "ask-for-the-pick",
"reason": "the finals’ Readout is graded B: phrase 14 (“every owner’s renewal notice, 60 days out”) won on LinkedIn and on the Site split, 0.62 against the control’s 0.31 qualified signups per 1, 000 impressions; phrases 3 and 22 trail it",
"stakes": "high",
"options": [
"start-a-round",
"probe-instead",
"shift-budget",
"rerun-the-finalists",
"ask-for-the-pick",
"reopen"
…
sample · 4 Not live yetPreviewOn PATCH /{namespace}/campaigns/{id}Off PATCH /{namespace}/campaigns/{id}Take this one POST /{namespace}/tasks/{task}/claim
action_2 is the pick card: the top three phrases with their evidence, and no click-through anywhere on it. action_3 hands it to you; the Story changes only on your pick (pick).
+0.31 per 1,
| Try another: the round | Choice | Reason |
|---|---|---|
| Round 1 finals: phrase 14 at 0.62 per 1,000 against the control’s 0.31, graded B | ask-for-the-pick | an admissible read: the pick is yours |
| Round 1 screen, day 28: the top 6 phrases and the control | rerun-the-finalists | the screen passed its finalists: 21 days on a second Venue and the Site |
| Screen day 9: phrases 3, 14 and 22 lead on ICP-matched signups | shift-budget | inside the fence: no phrase under a quarter or over 4 times its share |
| A rival’s homepage now says “renewals, per owner” | reopen | a rival is saying the winning phrase: a new round with it as the control |
- 2 h week 1, then 30 min a week
- Your time
- Day 5: first qualified signup from the screen
- First signal
Where it sits: Stranger → Lead → Trialing → Activated → At risk → Churned → Won back · Qualified signup rate → New Leads → New Customers ↑ → ARR added each week → ARR
Worth’s formula: about +0.31 qualified signups per 1,000 impressions (your finals, ■■□□ Early sign) × 60,000 impressions a month × 0.20 qualified signups to paid × $347 Pro ARR ≈ +$1,290, once the winning phrase runs on your ads and Site. Under First signal: A round takes 7 weeks: 4 days of equal budgets, the screen to day 28, then 21 days of finals; a signup counts 14 days after it lands.
sample · api.sb/
Start it this week
npx startup-builder add play positioning-tournament # 1 · writes plays/positioning-tournament.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":"positioning-tournament"}'
# 3 · keep it: you approve the slate and make the pick
curl -X POST https://api.sb/leaseline.app/campaigns \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"play":"positioning-tournament"}'
# the round’s Limit
curl -X POST https://api.sb/leaseline.app/limits \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"positioning-round","amount":1500,"period":"round"}'
# off: every Ad and the Site Flag pause through the proxy
curl -X PATCH https://api.sb/leaseline.app/campaigns/positioning-tournament \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"endsAt":"now"}' | Check | Fix | Then |
|---|---|---|
use-when fail · Not live yet, gaps #8 | Add 8 more quotes from 2 more ICP members; your interview notes hold them. | POST /{namespace}/interviews |
baseline pass · Not live yet, gaps #8 | Nothing to do: your current headline becomes the control. | POST /{namespace}/metrics/query |
fact-check pass · Not live yet, gaps #1 | Nothing to do: every phrase must cite one of the 9. | POST /{namespace}/content/draft |
price-split-by-buyer pass | Nothing to do: Variants differ only in the phrase. | POST /{namespace}/experiments/{experiment}/start |
role-unstaffed warn · Not live yet, gaps #8 | Staff the Marketing Lead, or leave it: the slate and the pick come to you. | POST /{namespace}/roles/{role}/staff |
JSON
{
"type": "https://startupbuilder.dev/errors/refused",
"title": "Refused",
"status": 422,
"detail": "positioning-tournament can’t start yet: 1 check fails and 1 warns.",
"checks": [
{
"check": "use-when",
"status": "fail",
"observed": "the Story has its ICP and Problem; 22 quotes from 8 ICP members",
"doc": "/plays/positioning-tournament#check-use-when",
"fix": "Add 8 more quotes from 2 more ICP members; your interview notes hold them.",
"then": "Interview.create"
},
{
"check": "baseline",
"status": "pass",
"observed": "the current headline: 0.30 qualified signups per 1,000 impressions, n 61,000",
"doc": "/plays/positioning-tournament#check-baseline",
"fix": "Nothing to do: your current headline becomes the control.",
"then": "Metric.query"
},
{
"check": "fact-check",
"status": "pass",
"observed": "the claims source holds 9 claims",
"doc": "/plays/positioning-tournament#check-fact-check",
"fix": "Nothing to do: every phrase must cite one of the 9.",
"then": "Content.draft"
},
{
"check": "price-split-by-buyer",
"status": "pass",
"observed": "one Offer and one Price on every Variant",
"doc": "/plays/positioning-tournament#check-price-split-by-buyer",
"fix": "Nothing to do: Variants differ only in the phrase.",
"then": "Experiment.start"
},
{
"check": "role-unstaffed",
"status": "warn",
"roles": [
"marketing-lead"
],
"owned": 0,
"doc": "/plays/positioning-tournament#check-role-unstaffed",
"fix": "Staff the Marketing Lead, or leave it: the slate and the pick come to you.",
"then": "Role.staff"
}
]
}It says no, says why, and names the fix.
| Check | Fix (one call) | Your time | Wait |
|---|---|---|---|
use-when · fail: 22 quotes from 8 ICP members (gaps #8) | POST /{namespace}/interviews for 2 more members, or add the quotes from your notes | 20 min | none |
baseline · pass: 0.30 per 1,000, n 61,000 | POST /{namespace}/metrics/query on qualified-signup-rate, which fills Before in Your number | 1 min | none |
fact-check · price-split-by-buyer · pass: 9 claims; one Price (Not live yet) | nothing; each runs again on every Variant | 0 min | none |
role-unstaffed · warn (gaps #8): the slate and the pick come to you | POST /{namespace}/roles/{role}/staff, or leave it | 0 min | none |
Your number · Before 0.30 · Now 0.62 per 1,
A round spends $1,500 of media under its Limit over 7 weeks, about $930 a month; the Evaluator, the claims check and the phrase slate cost about $4 a round (Worker cost). The Site headline split costs nothing. That comes to about $930 a month while a round runs, and $0 between rounds.
When it starts
| When | Trigger id | Starts on | Window | How it gets in |
|---|---|---|---|---|
| A tournament signup reaches day 14, and its committed action counts or not | signup-matured | event on Lead.qualified where experiment != null and age = 14d | 14d | POST /{namespace}/leads/{lead}/qualify at day 14, from your product’s setup Event |
| Your Story has no stance backed by behavior | story-ungraded | state on Story.revised where stance.backedBy = null | 30d | a state on the Story; no call |
| Two or more rivals start saying the winning phrase | cell-filled | threshold on Monitor.checked where rivalsSaying(winningCell) >= 2 | 14d | the monthly rival scan’s POST /{namespace}/monitors/{monitor}/check |
+ 6 more: a funded round, the monthly scan date, a rival’s claim change, a batch of Customer words, a decaying stance, and your other Startup asking for the same phrase (?lanes=) |
{
"$id": "https://api.sb/leaseline.app/events/event_1418" ,
"$type": "Event",
"name": "Lead.qualified",
"at": "2026-10-19T09:00:03Z" ,
"seq": 1418,
"data": {
"lead": {
"$id": "https://api.sb/leaseline.app/leads/lead_47" ,
"icp": true,
"action": "first lease set up",
"day": 14
},
"experiment": {
"$id": "https://api.sb/leaseline.app/experiments/experiment_11" ,
"variant": "phrase-14",
"round": "finals"
},
"matured": {
"finals": "all"
}
}
}npx startup-builder trigger Lead.qualified --like event_1418 # replays the last matured signup in test
npx startup-builder clock advance --by 5bd # checks for your pick (reopensWhen)
npx startup-builder listen Decision.created --forward-to http://localhost:3000/hook Who runs it
Marketing Lead · your agent (role:, worker_6) holds the Key Result: it scans rivals, mines Customer words, drafts the slate, runs the screen and the finals, and writes the pick card. You (Dana Okafor, person, worker_1) approve the slate, approve any rival’s name, and make the pick; the Story changes only on your pick. Your number links to Did it work.
| Option | Rung today | Evidence (passed of n) | Moves up at | Drops back when | You can |
|---|---|---|---|---|---|
| Shift budget to the leaders · Rerun the finalists | It does it, you can stop it | 19 daily shifts · 1 of 1 | met on day 5 (allocation inside the fence) | the surrogate Stop → equal shares again | POST /{namespace}/actions/{action}/cancel before the proxy writes |
| Start a round · Run a stance probe instead | You approve each one | 1 of 1 · 0 | never: a round spends your money | — | POST /{namespace}/approvals/{approval}/approve · POST /{namespace}/approvals/{approval}/refuse |
| Ask you to pick · Reopen the tournament | You decide, every time | 1 waiting · 0 | never: the pick is yours | — | POST /{namespace}/tasks/{task}/claim, then POST /{namespace}/decisions |
| Change the reward rule | You approve each one | 1 merged of 1 | never | — | POST /{namespace}/changes/{change}/merge · POST /{namespace}/changes/{change}/revert |
| Never: a rival’s claim quoted, a superlative no fact holds, two Prices for one Offer | Refused, every time | 2 drafted, 2 refused | never | — | none: the Check holds it |
What it reads · What it may call. It reads ICP-matched signups, their committed action at day 14 and spend, never click-through. Rival pages are read monthly and never quoted, and Customer words become phrases only when three or more people say them. Its tools are POST /{namespace}/content/draft, POST /{namespace}/experiments, POST /{namespace}/approvals, POST /{namespace}/flags, PATCH /{namespace}/ads/{id}, PATCH /{namespace}/flags/{id}, POST /{namespace}/experiments/{experiment}/start, POST /{namespace}/readouts, POST /{namespace}/tasks/{task}/assign, and any other call becomes an Approval under the Role’s Mandate.
You hold the Marketing Lead Key Result: 0.5 qualified signups per 1,000 impressions by 2026-12-31. ← worker, metric
On each matured signup, funding, scan or decay, choose one option and say why. ← triggers, chooses
Variants differ in the phrase alone; image, Offer, Price and Page stay the same. ← copyRules 2
No phrase states a fact the claims source lacks; no superlative a fact doesn’t hold. ← copyRules 1, 6
Count a signup only if it fits the ICP and sets up a lease within 14 days. ← your file, line 6
Never change the Story: hand Dana the pick card. ← Pick gate, chooses.ask-for-the-pick The slow loop, as a Change. decision_409 (2026-09-07, weekly) read the screen’s day-7 ranking by signups against its day-21 ranking by qualified signups: a rank correlation of 0.41, with 2 of the day-7 top 6 falling out. change_32 made allocation wait for ICP match at signup, not signups alone (your file, line 7), and Dana merged it with POST /{namespace}/changes/{change}/merge. Replayed on the screen, POST /{namespace}/changes/{change}/review put the correlation at 0.68; the finals read back at 0.71, n 7 phrases, ■□□□ Too early to tell (Example, day 49).
Bring your own agent (folded) starts at It does it, you can stop it when it matches or beats the seated agent on the eval suite, and at You approve each one when it does not; the pick stays yours either way.
npx startup-builder eval --scenario positioning-tournament-40 --agent "claude -p" --seat marketing-lead # seated: 0 claims failed sent, rank correlation 0.68 · yours: 1 superlative passed → You approve each one What it can do next
| Option | Picked when | Decided by | Runs | Hands to | If it’s just you |
|---|---|---|---|---|---|
Start a round start-a-round | no stance backed by behavior, and $1,500 funded | Your rule (line 5) | POST /{namespace}/content/draft 40 phrases over 3 stances → POST /{namespace}/experiments → POST /{namespace}/approvals for the slate | you approve the slate · Take this one | you |
Run a stance probe instead probe-instead | under $1,500 funded | Your rule (line 5) | POST /{namespace}/flags for a $0 Site headline split → POST /{namespace}/experiments | nobody | the same |
Shift budget to the leaders shift-budget | from day 5, ICP-matched signups favor some phrases | Agent’s call, with its reason | PATCH /{namespace}/ads/{id} budgets, inside the allocation fence | nobody | the same |
Rerun the finalists rerun-the-finalists | the screen passes its top 6 and the control | Your rule (line 8) | PATCH /{namespace}/flags/{id} for the Site split → POST /{namespace}/experiments/{experiment}/start on a second Venue, 21 days | nobody | the same |
Ask you to pick ask-for-the-pick | the finals read graded A or B | Your rule (the Readability gate) | POST /{namespace}/readouts → POST /{namespace}/tasks/{task}/assign the pick card | you pick, within 1 business day · Take this one | you |
Reopen the tournament reopen | rivals say the winning phrase, it decays, or the Hypothesis changes | Agent’s call, with its reason | POST /{namespace}/experiments with the incumbent as control → POST /{namespace}/tasks/{task}/assign | you approve the new round · Take this one | you |
| When they write back | Fixture reply | What happens | Who answers | By when | Call |
|---|---|---|---|---|---|
| Interested | a reply to the welcome: “Happy to talk. Thursday?” | a 20-minute interview on your calendar; their words join the next phrase mine | you (Dana Okafor, person) | 1 business day | POST /{namespace}/meetings/book |
| Send me pricing | “What does it cost after the trial?” | the Pricing Page’s list Price, the same on every Variant | Marketing Lead · your agent | 5 min | POST /{namespace}/conversations/{conversation}/answer |
| Not now | “Just looking for now.” | no interview invite; the signup still counts if they act by day 14 | nobody | — | GET /{namespace}/decisions |
| Wrong person | “My boss decides on tools.” | it thanks them and asks nothing more; it never writes to someone from their text | Marketing Lead · your agent | 1 business day | POST /{namespace}/conversations/{conversation}/answer |
| Out of office | an auto-reply to the welcome | the interview invite waits for the return date, once | nobody | the return date | POST /{namespace}/messages/{message}/schedule |
| Question it can’t answer | “Is 60 days the right notice period in my state?” | To a person: it is never the agent’s to answer | you (Dana Okafor, person) | 1 business day | POST /{namespace}/tasks/{task}/assign |
| Stop | “Unsubscribe.” | no welcome follow-up or interview invite sends again | nobody | at once | POST /{namespace}/contacts/{contact}/unsubscribe |
When it hands to you
The slate, every rival’s name, every new round and the pick come to you: the Marketing Lead Role has no person, and the Story is yours. One Brief, as captured, then the Approval (brief, draft and checks are Not live yet, gaps #7).
[
{
"$id": "https://api.sb/leaseline.app/tasks/task_481" ,
"$type": "Task",
"assignee": "worker_1",
"due": "2026-10-20T17:00:00Z" ,
"on": "experiment_11",
"brief": {
"what": "the round 1 pick card",
"top": [
"phrase 14: every owner’s renewal notice, 60 days out (0.62 per 1,000)" ,
"phrase 3: renewals done in an afternoon (0.48)" ,
"phrase 22: no lease renews late (0.44)"
],
"control": "the current headline (0.31)",
"grade": "B",
"chose": "ask-for-the-pick",
"never": [
"click-through as a reason"
]
}
},
{
"$id": "https://api.sb/leaseline.app/approvals/approval_52" ,
"$type": "Approval",
"verdict": "approved",
"deadline": "2026-08-27T17:00:00Z" ,
"draft": "40 phrases over 3 stances: per-owner reliability (14), time saved (13), fewer late renewals (13)…" ,
"route": "start-a-round",
"checks": [
{
"check": "evaluator",
"status": "pass"
},
{
"check": "fact-check",
"status": "pass"
},
{
"check": "rival-name",
"status": "pass"
}
]
}
]You answer with POST /{namespace}/tasks/{task}/claim (Take this one), then POST /{namespace}/decisions for the pick, or POST /{namespace}/approvals/{approval}/approve and POST /{namespace}/approvals/{approval}/refuse for a slate, each a one-call window. With no pick in 5 business days, the Story keeps its stance and the round’s read waits; nothing is picked for you on a Startup with a person.
On time: you, 1 of 1 slates approved by its deadline, 38 of 40 phrases kept, n 1; the pick of Oct 19 waits for you, ■□□□ Too early to tell beside Other Startups’ 86 % on time. Your queue: counted by kind: the slate in week 1, a rival-name approval if one comes up, and the pick card at the end of each round.
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 |
|---|---|---|---|
fact-check (Not live yet) | a phrase stating a fact the claims source lacks, or a superlative or number no sourced fact holds | Cut the claim, or name the record that backs it. | POST /{namespace}/content/draft |
price-split-by-buyer (gaps #8) | a Variant that shows a different Price or Offer for the same thing | Assign the Price test by market or by time. | POST /{namespace}/experiments/{experiment}/start |
evaluator (the record’s Evaluator gate) | a phrase that fails the Evaluator or the claims check | Fix the lines the Evaluator lists, then submit the phrase again. | POST /{namespace}/content/draft |
rival-name (the record’s Rival-name gate) | a phrase naming a rival with no supporting fact and your approval | Cite a dated fact for the rival, then approve the phrase. | POST /{namespace}/approvals/{approval}/approve |
coherence (the record’s Coherence gate) | a Variant whose landing Page’s first screen does not deliver what the phrase promises | Fix the Page’s first screen, or drop the phrase. | PATCH /{namespace}/pages/{id} |
status (the record’s Status gate) | any Ad while the tournament Page hides whether the product is live | State the product’s status on the Page’s first screen. | PATCH /{namespace}/pages/{id} |
allocation-fence (the record’s Allocation fence) | a shift that puts a phrase under a quarter or over 4 times its share, or a kill before day 14 | Shift spend inside the fence, and kill nothing before day 14. | PATCH /{namespace}/ads/{id} |
readability (the record’s Readability gate) | a Story write from a Readout graded inadmissible | Let the finals mature; only an A or B read reaches the pick card. | GET /{namespace}/readouts/{id} |
pick (the record’s Pick gate) | a Story change without your pick | Pick from the card; the Story changes on your Decision. | POST /{namespace}/decisions |
The captured refusal, then the Stop that fired as a Campaign.paused reason (Not live yet, gaps #2); the rewrite that ran is phrase 9 in Example.
| Check | Fix | Then |
|---|---|---|
fact-check fail · Not live yet, gaps #1 | Cut the claim, or name the record that backs it. | POST /{namespace}/content/draft |
price-split-by-buyer pass | — | POST /{namespace}/experiments/{experiment}/start |
JSON
{
"type": "https://startupbuilder.dev/errors/refused",
"title": "Refused",
"status": 422,
"detail": "Phrase 9 did not enter the slate: it states a superlative no sourced fact holds.",
"checks": [
{
"check": "fact-check",
"status": "fail",
"doc": "/plays/positioning-tournament#check-fact-check",
"fix": "Cut the claim, or name the record that backs it.",
"then": "Content.draft"
},
{
"check": "price-split-by-buyer",
"status": "pass"
}
]
}It says no, says why, and names the fix.
[
{
"reason": {
"limit": "signup-quality",
"observed": "34 % excluded (10 of 29 signups)",
"bound": "30 %",
"window": "1 day on Reddit, Sep 3"
},
"doc": "/plays/positioning-tournament#stop-signup-quality" ,
"task": "task_402"
}
]Did it work
Impressions were assigned at random across the finalists and the control (rule o); the winning phrase is read against the control from that assignment, on LinkedIn and on the Site split. Each figure’s call is POST /{namespace}/metrics/query by node and by arm; the parent field and every node above the leaf are Not live yet (gaps #4).
+0.31 per 1,
- Chose
- ask-for-the-pick
- Why
- the finals’ Readout is graded B: phrase 14 (“every owner’s renewal notice, 60 days out”) won on LinkedIn and on the Site split, 0.62 against the control’s 0.31 qualified signups per 1,000 impressions; phrases 3 and 22 trail it
- From
- start-a-round · probe-instead · shift-budget · rerun-the-finalists · ask-for-the-pick · reopen
- Expects
- your pick written to the Story within 1 business day
- Reopens when
- no Decision.created by worker_1 on experiment_11 within 5 business days: the Story keeps its stance
- By
- worker_6
JSON
{
"$id": "decisions/decision_596",
"$type": "Decision",
"choice": "ask-for-the-pick",
"reason": "the finals’ Readout is graded B: phrase 14 (“every owner’s renewal notice, 60 days out”) won on LinkedIn and on the Site split, 0.62 against the control’s 0.31 qualified signups per 1,000 impressions; phrases 3 and 22 trail it",
"stakes": "high",
"options": [
"start-a-round",
"probe-instead",
"shift-budget",
"rerun-the-finalists",
"ask-for-the-pick",
"reopen"
],
"by": "worker_6",
"on": "experiment_11",
"expected": "your pick written to the Story within 1 business day",
"reopensWhen": "no Decision.created by worker_1 on experiment_11 within 5 business days: the Story keeps its stance"
}| Node | Phrase 14 | The control (your current headline) | Added | Confidence | Basis |
|---|---|---|---|---|---|
qualified-signup-rate | 0.62 per 1,000 impressions (26 of 42,000) | 0.31 (13 of 42,000) | +0.31 [+0.07, +0.55] | ■■□□ Early sign | assigned at random · n 84,000 impressions · 0 still maturing · won again on the Site split · Other Startups +0.20, ■■■□ Likely, n 29 Startups |
new-leads | 1.10 per 1,000 (46 of 42,000), any signup | 0.71 (30 of 42,000) | +0.39 [−0.05, +0.83] | ■□□□ Too early to tell | the same assignment |
new-arr ↑ | $694, 2 Customers · Booked: credited, not all caused by this Play | $347, 1 Customer | +$347 [−$500, +$1,200] · Not carrying up yet · Worth about +$1,300 a month | ■□□□ Too early to tell (2 vs 1 Customers) | the same · 9 trials still deciding |
net-new-arr · arr | +$694 | +$347 | +$347, carried: it moves no other bridge term | ■□□□ Too early to tell | carried from new-arr |
The leaf counts signups who match the ICP and set up a first lease within 14 days, per 1,000 impressions, for each phrase; the screen ran 40 phrases on LinkedIn for 28 days, and the finals 6 phrases and the control for 21 days on Facebook and the Site headline split.
How it moves it: the phrase changed and nothing else: same image, Offer, Price and Page body, so the gap is the words (qualified-signup-rate by phrase).
Guardrails: treated against the control: trial to first lease set up, 57 % against 43 %, and comments calling an Ad misleading, 0 of 61 against a Stop at 5 %.
{
"$id": "https://api.sb/leaseline.app/campaigns/positioning-tournament" ,
"$type": "Campaign",
"play": "play:positioning-tournament@1" ,
"path": [
"qualified-signup-rate",
"new-leads",
"new-arr",
"net-new-arr",
"arr"
],
"experiment": {
"metric": "qualified-signup-rate",
"unit": "per-1000",
"heldBack": 42000,
"decision": null
},
"readout": {
"value": 0.31,
"n": 84000,
"maturing": 0,
"interval": [
0.07,
0.55
],
"readability": "B",
"basis": "held-back",
"pooled": false
},
"ledger": {
"spend": 1500,
"workerCost": 4,
"hours": 4.5,
"pipelineArr": 0,
"wonArr": 694,
"projectedArr": 1290,
"paybackDays": 420
}
}Outside benchmark: the catalog holds none for this Play yet, so Other Startups’ results stand in: +0.20 qualified signups per 1,000 impressions over the control, ■■■□ Likely, n 29 Startups.
Before it · After it. Before it: signup-interviews and objection-watch supply the Customer words it mines. After it: your pick writes the Story, and paid-social, paid-search, organic-social and cold-email open lines carry the winning phrase; the old control keeps 10 % of the Site Flag for 30 days.
Example
Leaseline ran round 1 from 2026-08-24 to 2026-10-19 (56 days): 40 phrases in the screen, 6 finalists and the control in the finals, $1,500 spent, 214 signups, 71 of them qualified. The pick waits for Dana. Read every captured thread at ?threads=; three follow the tables.
| What happened, by option | Decisions | Done | Won (credited, not all caused by this Play) |
|---|---|---|---|
| Start a round | 1 | 40 phrases drafted, 38 approved, 2 refused | — |
| Shift budget to the leaders · Rerun the finalists | 19 · 1 | inside the fence every day · 6 finalists and the control, 21 days | 3 on Pro from tournament signups (+$1,041 ARR) |
| Ask you to pick · Reopen the tournament · Run a stance probe instead | 1 · 0 · 0 | waiting on you · — · — | — |
| At $1,500 a round (the Use it when volume) | 1 round in 7 weeks | finals assigned at random | Other Startups’ results: +0.20 per 1,000, ■■■□ Likely, n 29 Startups |
| Day | Event | What happened | Your time |
|---|---|---|---|
| 0 | event_118 Campaign.created | the three steps; the dry run fails use-when: 22 quotes from 8 members | 25 min |
| 2 | event_140 Interview.created | 2 more ICP members interviewed; 31 quotes from 10 | 50 min |
| 3 | event_150 Approval.approved | you approved 38 of 40 phrases; the screen started at equal budgets | 30 min |
| 5 | event_190 Lead.created | the first ICP-matched signup, on phrase 14 | — |
| 10 | event_244 Campaign.paused | signup-quality on Reddit: 34 % excluded; Reddit stayed off for the round | 10 min |
| 14 · 49 | event_301 Change.merged · event_861 Readout.created | allocation waits for ICP match: replayed at a 0.68 correlation, read back at 0.71 | 5 min |
| 28 | event_560 Experiment.started | the finals: 6 phrases and the control on Facebook and the Site split | — |
| 56 | event_1418 Lead.qualified | the last finals signup matured; the pick card went to you | — |
| 1–56 | event_131 Approval.created, first of 2 | the slate and a rival-name check; weekly reviews, from the POST /{namespace}/tasks/next log | 2 h 40 min · total 4.5 h measured, 4 h asked |
Phrase 14 on LinkedIn, Sep 30 · “Every owner’s renewal notice, 60 days out.”¹ The same image, Offer and Page as the other 39.
The signup, Oct 1, 10:20 · a 140-unit manager who fits the ICP; she set up her first lease on Oct 3 (qualified, day 2), replied to the welcome, and talked with Dana on Oct 9. Her words, “I just need every owner to get it on time”, joined the next phrase mine.²
- The fastest renewal software for landlords
+ Renewal notices sent 60 days out, for every owner The round 1 slate, 2026-08-26 15:00 · 40 phrases, 2 refused by the Checks, and one naming a rival with a dated fact · the Approval went to Dana with a deadline of Aug 27 at 17:00 (
approval_52).³ Dana approved 38 phrases on Aug 27 at 11:10, inside the deadline, and struck the rival phrase.
¹ Variants differ in this phrase alone. ² A Customer’s words become a phrase only when three or more people say them. ³ A slate and any rival’s name are yours to approve, on every rung.
Your settings
---
limit: 1500 # dollars a round
venues: [linkedin, facebook]
---
Run a round when the Story has no stance backed by behavior, and $1,500 is funded.
Count a signup only if it fits our ICP and sets up a lease within 14 days.
Shift budget only on ICP-matched signups.
Rerun the best 6 against our current headline.$ npx startup-builder show plays/positioning-tournament
limit $1,500 a round · venues linkedin, facebook · a lease set up by day 14 · ICP-matched allocation · 6 finalists your file plays/positioning-tournament.mdx:2,3,5,6,7,8 (Ask 2026-08-24; line 7 by change_32, 2026-09-07)
family positioning · worker role:marketing-lead, owns never · chooses 6 (4 rule, 2 read) · sequence tournament-welcome · approval every-send · checks fact-check, price-split-by-buyer, evaluator, rival-name, coherence, status, allocation-fence, readability, pick · stops 8 · placements linkedin, facebook + the Site Flag catalog play:positioning-tournament@1 (Not live yet, gaps #5) Every key from the catalog
Compiled, not written: edit the Play record positioning-tournament 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.