URL: /plays/positioning-tournament · Id: `play:positioning-tournament`

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

Tests three ways of saying what you do, as ads that differ in one phrase, and counts only signups that fit and act.

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

```text title="Run one · Ask"
Hand our latest tournament round to the Marketing Lead agent and show me what it chose and why. Don’t change anything.
```

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

```json title="Run one · Response · sample"
{
  "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"
    ],
    "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"
  },
  "actions": [
    {
      "$id": "https://api.sb/leaseline.app/actions/action_1",
      "$type": "Action",
      "status": "done",
      "mode": "try",
      "by": "worker_6",
      "verb": "Readout.create",
      "checks": [
        {
          "check": "readability",
          "status": "pass"
        }
      ]
    },
    {
      "$id": "https://api.sb/leaseline.app/actions/action_2",
      "$type": "Action",
      "status": "done",
      "mode": "try",
      "by": "worker_6",
      "verb": "Content.draft",
      "checks": [
        {
          "check": "fact-check",
          "status": "pass"
        }
      ]
    },
    {
      "$id": "https://api.sb/leaseline.app/actions/action_3",
      "$type": "Action",
      "status": "pending",
      "reason": "awaitingClaim",
      "mode": "try",
      "by": "worker_6",
      "verb": "Task.assign",
      "checks": [
        {
          "check": "pick",
          "status": "pending"
        }
      ]
    }
  ]
}
```

`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,000 · ■■□□ Early sign · n 84,000 · path → arr`

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

| Worth | Your time | Spend | First signal | Use it when | Instead |
| --- | --- | --- | --- | --- | --- |
| ≈ +$1,300 ARR a month at 60,000 impressions | 2 h week 1, then 30 min a week | $1,500 a round, about $930 a month | Day 5: first qualified signup from the screen | $1,500 a round, a Story with ICP and Problem, 30 quotes | Under 30 quotes: `signup-interviews` · under $1,500: run the Site split (<Op verb="Flag.create" />) |

**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/leaseline.app · 41 Customers at 2026-10-19 · on Lead.qualified · pick card drafted 9 min after the last signup matured · 9 keys Not live yet`

## Start it this week
```bash
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"}'
```


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

| Check | Fix (one call) | Your time | Wait |
| --- | --- | --- | --- |
| <a id="check-use-when"></a>`use-when` · fail: 22 quotes from 8 ICP members (gaps #8) | <Op verb="Interview.create" /> for 2 more members, or add the quotes from your notes | 20 min | none |
| <a id="check-baseline"></a>`baseline` · pass: 0.30 per 1,000, n 61,000 | <Op verb="Metric.query" /> on `qualified-signup-rate`, which fills Before in Your number | 1 min | none |
| <a id="check-fact-check"></a>`fact-check` · <a id="check-price-split-by-buyer"></a>`price-split-by-buyer` · pass: 9 claims; one Price (Not live yet) | nothing; each runs again on every Variant | 0 min | none |
| <a id="check-role-unstaffed"></a>`role-unstaffed` · warn (gaps #8): the slate and the pick come to you | <Op verb="Role.staff" />, or leave it | 0 min | none |

`Your number · Before 0.30 · Now 0.62 per 1,000 for phrase 14 · Target 0.5 by Dec 31 · ■■□□ Early sign · n 84,000 impressions in the finals · Other Startups +0.20, ■■■□ Likely`

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 | <Op verb="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 <Op verb="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=all`) | | | | |

```json title="One signed delivery · StartupBuilder-Signature"
{
  "$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"
    }
  }
}
```

```bash title="Reproduce it in test"
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:marketing-lead`, `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 | <Op verb="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 | — | <Op verb="Approval.approve" /> · <Op verb="Approval.refuse" /> |
| Ask you to pick · Reopen the tournament | You decide, every time | 1 waiting · 0 | never: the pick is yours | — | <Op verb="Task.claim" />, then <Op verb="Decision.create" /> |
| Change the reward rule | You approve each one | 1 merged of 1 | never | — | <Op verb="Change.merge" /> · <Op verb="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 <Op verb="Content.draft" />, <Op verb="Experiment.create" />, <Op verb="Approval.create" />, <Op verb="Flag.create" />, <Op verb="Ad.update" />, <Op verb="Flag.update" />, <Op verb="Experiment.start" />, <Op verb="Readout.create" />, <Op verb="Task.assign" />, and any other call becomes an Approval under the Role’s Mandate.

```md title="The brief (folded): compiled Agent-register Markdown · play:positioning-tournament@1 · each line names its record key"
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 <Op verb="Change.merge" />. **Replayed on the screen**, <Op verb="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.

```bash
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) | <Op verb="Content.draft" /> 40 phrases over 3 stances → <Op verb="Experiment.create" /> → <Op verb="Approval.create" /> 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) | <Op verb="Flag.create" /> for a $0 Site headline split → <Op verb="Experiment.create" /> | 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 | <Op verb="Ad.update" /> 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) | <Op verb="Flag.update" /> for the Site split → <Op verb="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) | <Op verb="Readout.create" /> → <Op verb="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 | <Op verb="Experiment.create" /> with the incumbent as control → <Op verb="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 | <Op verb="Meeting.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 | <Op verb="Conversation.answer" /> |
| Not now | “Just looking for now.” | no interview invite; the signup still counts if they act by day 14 | nobody | — | <Op verb="Decision.list" /> |
| 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 | <Op verb="Conversation.answer" /> |
| Out of office | an auto-reply to the welcome | the interview invite waits for the return date, once | nobody | the return date | <Op verb="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 | <Op verb="Task.assign" /> |
| Stop | “Unsubscribe.” | no welcome follow-up or interview invite sends again | nobody | at once | <Op verb="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).

```json title="What lands in each queue · sample"
[
  {
    "$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 <Op verb="Task.claim" /> (Take this one), then <Op verb="Decision.create" /> for the pick, or <Op verb="Approval.approve" /> and <Op verb="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:** <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 |
| --- | --- | --- | --- |
| `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. | <Op verb="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. | <Op verb="Experiment.start" /> |
| <a id="check-evaluator"></a>`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. | <Op verb="Content.draft" /> |
| <a id="check-rival-name"></a>`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. | <Op verb="Approval.approve" /> |
| <a id="check-coherence"></a>`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. | <Op verb="Page.update" /> |
| <a id="check-status"></a>`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. | <Op verb="Page.update" /> |
| <a id="check-allocation-fence"></a>`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. | <Op verb="Ad.update" /> |
| <a id="check-readability"></a>`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. | <Op verb="Readout.get" /> |
| <a id="check-pick"></a>`pick` (the record’s Pick gate) | a Story change without your pick | Pick from the card; the Story changes on your Decision. | <Op verb="Decision.create" /> |

| Stop (pauses the Play) | Pauses when | Then |
| --- | --- | --- |
| <a id="stop-claims"></a>`claims` | a fact a live phrase relies on changes or fails its recheck | every Variant using it pauses within 1 h |
| <a id="stop-signup-quality"></a>`signup-quality` | excluded signups over 30 % of a day’s, at 20 or more | that Venue pauses; a Task for you |
| <a id="stop-surrogate"></a>`surrogate` | the early ranking and the matured ranking correlate under 0.3 | allocation returns to equal shares |
| <a id="stop-signup-feed"></a>`signup-feed` | no signup recorded for 72 h while spend flows | changes freeze; at 7 days the Play pauses |
| <a id="stop-comment"></a>`comment` | comments calling an Ad misleading over 5 % of 50 or more | that Ad pauses |
| <a id="stop-drift"></a>`drift` | a live Ad or setting differs from the approved state | it pauses or resets on the daily read |
| <a id="stop-spend-limit"></a>`spend-limit` | spend reaches the round’s Limit | every Ad pauses until the next round opens |
| <a id="stop-kill-switch"></a>`kill-switch` | you press it | every Ad and the Site Flag pause through the proxy |

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.

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


```json title="The Stops that fired, as Campaign.paused reasons · sample"
[
  {
    "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 <Op verb="Metric.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,000 · ■■□□ Early sign · n 84,000 · path → arr`

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

```json title="The Campaign card · sample"
{
  "$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=all`; 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 <Op verb="Task.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.²

```diff title="Phrase 9, 2026-08-26 · fact-check refused, then the version that ran"
- 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
```mdx title="plays/positioning-tournament.mdx"
---
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.
```

```text
$ 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
| Key | Id | What it is |
| --- | --- | --- |
| Tactic | <a id="tactic-rival-claim-scan"></a>`rival-claim-scan` | Rival claim scan captures every rival’s homepage, pricing Page and live Ads each month, clusters their claims, and scores how crowded each cluster is. Starts at Sends nothing. |
| Tactic | <a id="tactic-customer-phrase-mine"></a>`customer-phrase-mine` | Customer phrase mine extracts the words ICP members use about the Problem and keeps the ones three or more people say that no rival claims. Starts at Sends nothing. |
| Tactic | <a id="tactic-phrase-slate"></a>`phrase-slate` | Phrase slate drafts 40 Variants that differ in one phrase across three stances, runs the Evaluator and the coherence check, and starts the Experiment. Starts at You approve each one. |
| Tactic | <a id="tactic-headline-screen"></a>`headline-screen` | Headline screen builds 40 headline-only Ads on one Venue, identical but for the phrase, and runs them at equal budgets for the first 4 days. Starts at You approve each one. |
| Tactic | <a id="tactic-headline-allocation"></a>`headline-allocation` | Headline allocation shifts the screen’s daily budget toward ICP-matched signups with logged propensities, and passes the top 6 cells and control to the finals. Starts at You approve each one. |
| Tactic | <a id="tactic-qualified-signup-join"></a>`qualified-signup-join` | Qualified signup join resolves each tournament signup’s ICP match at once and its committed action at day 14, turning provisional credit into the lagged reward. Starts at Sends nothing. |
| Tactic | <a id="tactic-finalist-replication"></a>`finalist-replication` | Finalist replication reruns 6 finalists and the control on a Site headline split and a second Venue for 21 days, marking which cells win again. Starts at You approve each one. |
| Tactic | <a id="tactic-founder-pick"></a>`founder-pick` | Founder pick shows the founder the top three cells with their evidence, records the pick as a Decision, and writes the Story’s stance. Starts at You approve each one. |
| Tactic | <a id="tactic-tournament-reopen"></a>`tournament-reopen` | Tournament reopen restarts the tournament with the incumbent as control when rivals copy its cell, its rate decays or the founding Hypothesis changes. Starts at Sends nothing. |
| Tactic | <a id="tactic-ad-write-proxy"></a>`ad-write-proxy` | Ad write proxy is the only write path to an ads Venue, holding the token outside the model and forcing every create to paused. Starts at You approve each one. |
| Tactic | <a id="tactic-ad-drift-monitor"></a>`ad-drift-monitor` | Ad drift monitor diffs every live Ad’s rendered creative, text and enhancement settings against the approved source daily, and pauses or resets what drifted. Starts at Sends nothing. |
| Trigger | <a id="trigger-story-ungraded"></a>`story-ungraded` | Story ungraded: `state` on `Story.revised`, window 30d. |
| Trigger | <a id="trigger-tournament-funded"></a>`tournament-funded` | Tournament funded: `threshold` on `PlayMix.compiled`, window 7d. |
| Trigger | <a id="trigger-heatmap-due"></a>`heatmap-due` | Heatmap due: `at` on `Dashboard.scheduled`, window 7d. |
| Trigger | <a id="trigger-rival-claim-change"></a>`rival-claim-change` | Rival claim change: `event` on `Monitor.checked`, window 30d. |
| Trigger | <a id="trigger-cell-filled"></a>`cell-filled` | Cell filled: `threshold` on `Monitor.checked`, window 14d. |
| Trigger | <a id="trigger-customer-words-batch"></a>`customer-words-batch` | Customer words batch: `threshold` on `Conversation.closed`, window 30d. |
| Trigger | <a id="trigger-signup-matured"></a>`signup-matured` | Tournament signup matured: `event` on `Lead.qualified`, window 14d. |
| Trigger | <a id="trigger-stance-decay"></a>`stance-decay` | Stance decay: `threshold` on `Metric.watched`, window 30d. |
| Trigger | <a id="trigger-sibling-cell-claim"></a>`sibling-cell-claim` | Sibling cell claim: `event` on `Territory.requested`, window 7d. |
| Sequence | <a id="sequence-tournament-welcome"></a>`tournament-welcome` | Tournament welcome: 2 steps; it ends on A reply; A booked Interview; A Reserve deposit; An unsubscribe. |