marketing-mindset 1.12.0 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -12
- package/bin/cli.js +229 -0
- package/package.json +6 -2
- package/skill/SKILL.md +19 -0
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ npx marketing-mindset install # copy the skill into your agent's sk
|
|
|
17
17
|
|
|
18
18
|
`npx marketing-mindset jd --level senior` prints the JD with the artifacts due by day 90,
|
|
19
19
|
the sample-size floors, the kill rule and the screening questions. Web version:
|
|
20
|
-
https://axelfreeman.github.io/marketing-mindset/tools/marketing-engineer-jd-generator.html
|
|
20
|
+
https://axelfreeman.github.io/marketing-mindset/tools/marketing-engineer-jd-generator.html?utm_source=npm&utm_medium=readme&utm_campaign=mindset
|
|
21
21
|
|
|
22
22
|
## Floors that do not move
|
|
23
23
|
|
|
@@ -30,11 +30,11 @@ https://axelfreeman.github.io/marketing-mindset/tools/marketing-engineer-jd-gene
|
|
|
30
30
|
|
|
31
31
|
## Links
|
|
32
32
|
|
|
33
|
-
- Skill page: https://axelfreeman.github.io/marketing-mindset
|
|
33
|
+
- Skill page: https://axelfreeman.github.io/marketing-mindset/?utm_source=npm&utm_medium=readme&utm_campaign=mindset
|
|
34
34
|
- Source: https://github.com/axelfreeman/marketing-mindset
|
|
35
|
-
- Free test-size calculator (browser, no signup): https://axelfreeman.github.io/marketing-mindset/tools/email-test-planner.html
|
|
35
|
+
- Free test-size calculator (browser, no signup): https://axelfreeman.github.io/marketing-mindset/tools/email-test-planner.html?utm_source=npm&utm_medium=readme&utm_campaign=mindset
|
|
36
36
|
- MCP server, same method over stdio: https://www.npmjs.com/package/marketing-mindset-mcp
|
|
37
|
-
- `llms.txt` for agents: https://axelfreeman.com/llms.txt
|
|
37
|
+
- `llms.txt` for agents: https://axelfreeman.com/llms.txt?utm_source=npm&utm_medium=readme&utm_campaign=mindset
|
|
38
38
|
|
|
39
39
|
## Need this done for you?
|
|
40
40
|
|
|
@@ -43,8 +43,8 @@ sending setup that passes a deliverability checklist, tests with a written kill
|
|
|
43
43
|
measurement inside your own analytics property — is sold as fixed-scope packages, with the
|
|
44
44
|
scope and the prices published before you get on a call:
|
|
45
45
|
|
|
46
|
-
**https://axelfreeman.com/marketing-engineer.html** — Sprint $900 · Engine $1,900/month · Full build $2,900.
|
|
47
|
-
Scope: https://axelfreeman.com/scope.html · Pricing: https://axelfreeman.com/pricing.html
|
|
46
|
+
**https://axelfreeman.com/marketing-engineer.html?utm_source=npm&utm_medium=readme&utm_campaign=mindset** — Sprint $900 · Engine $1,900/month · Full build $2,900.
|
|
47
|
+
Scope: https://axelfreeman.com/scope.html?utm_source=npm&utm_medium=readme&utm_campaign=mindset · Pricing: https://axelfreeman.com/pricing.html?utm_source=npm&utm_medium=readme&utm_campaign=mindset
|
|
48
48
|
|
|
49
49
|
MIT © Axel Freeman
|
|
50
50
|
|
|
@@ -55,10 +55,30 @@ npx marketing-mindset aeo # 23 weighted checks, grouped, with the max s
|
|
|
55
55
|
npx marketing-mindset aeo --json # same data for a script or an agent
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
+
### Tag every outbound link before it ships
|
|
59
|
+
|
|
60
|
+
A channel you cannot attribute is a channel you cannot kill, so the rule is mechanical:
|
|
61
|
+
links that leave for your own property carry UTM, internal links never do.
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
npx marketing-mindset utm https://axelfreeman.com/offer.html --source devto --medium article --campaign mindset
|
|
65
|
+
npx marketing-mindset utm --file article.md --source devto --medium article --campaign mindset
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`--file` rewrites the file in place and reports how many links it tagged; a second run tags nothing
|
|
69
|
+
(idempotent). Third-party links are left alone, existing query params are kept, and sentence
|
|
70
|
+
punctuation after a URL stays where it belongs. The convention it writes:
|
|
71
|
+
|
|
72
|
+
| parameter | what it holds | examples |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `utm_source` | the channel that sent the click | `devto`, `github`, `npm`, `telegram`, `habr`, `reddit` |
|
|
75
|
+
| `utm_medium` | the placement | `article`, `readme`, `package`, `post`, `profile` |
|
|
76
|
+
| `utm_campaign` | the product or the cluster | `mindset`, `tapac`, `hire`, `lyzhi`, `engine` |
|
|
77
|
+
|
|
58
78
|
The same checklist is a browser tool with a live score:
|
|
59
|
-
<https://axelfreeman.github.io/marketing-mindset/tools/aeo-readiness-checklist.html>.
|
|
79
|
+
<https://axelfreeman.github.io/marketing-mindset/tools/aeo-readiness-checklist.html?utm_source=npm&utm_medium=readme&utm_campaign=mindset>.
|
|
60
80
|
If you want the plumbing in that checklist built for you (offer page, query-intent pages, schema,
|
|
61
|
-
`llms.txt`, IndexNow, the tracked event), packages are published at https://axelfreeman.com/marketing-engineer.html.
|
|
81
|
+
`llms.txt`, IndexNow, the tracked event), packages are published at https://axelfreeman.com/marketing-engineer.html?utm_source=npm&utm_medium=readme&utm_campaign=mindset.
|
|
62
82
|
|
|
63
83
|
### Sending ramp: `warmup`
|
|
64
84
|
|
|
@@ -66,7 +86,7 @@ If you want the plumbing in that checklist built for you (offer page, query-inte
|
|
|
66
86
|
npx -y marketing-mindset@1.4.0 warmup --start 2026-09-21 --target 200 --days 21 --mailboxes 4
|
|
67
87
|
```
|
|
68
88
|
|
|
69
|
-
Day-by-day ramp for a new sending domain: starts at 20/day (or the target, if smaller), grows by half a day, never above the target, and prints the four stop rules (bounce >2%/day, complaints >0.1%, any reply ends that sequence, opens halving with flat sends -> drop back two ramp days). `--json` gives the machine-readable schedule. Work with me: https://axelfreeman.com/marketing-engineer.html
|
|
89
|
+
Day-by-day ramp for a new sending domain: starts at 20/day (or the target, if smaller), grows by half a day, never above the target, and prints the four stop rules (bounce >2%/day, complaints >0.1%, any reply ends that sequence, opens halving with flat sends -> drop back two ramp days). `--json` gives the machine-readable schedule. Work with me: https://axelfreeman.com/marketing-engineer.html?utm_source=npm&utm_medium=readme&utm_campaign=mindset
|
|
70
90
|
|
|
71
91
|
### Test budget: `budget`
|
|
72
92
|
|
|
@@ -74,14 +94,14 @@ Day-by-day ramp for a new sending domain: starts at 20/day (or the target, if sm
|
|
|
74
94
|
npx -y marketing-mindset@1.5.0 budget --base 0.03 --lift 0.2 --cpl 4 --daily 500
|
|
75
95
|
```
|
|
76
96
|
|
|
77
|
-
The money side of the same floor `sample` computes: contacts needed in both arms, cost per arm and cost to read the test at your cost per contact, days to the floor at a given daily volume, and the spend guard (below the floor the difference is noise, not a result). `--json` for agents. Work with me: https://axelfreeman.com/marketing-engineer.html
|
|
97
|
+
The money side of the same floor `sample` computes: contacts needed in both arms, cost per arm and cost to read the test at your cost per contact, days to the floor at a given daily volume, and the spend guard (below the floor the difference is noise, not a result). `--json` for agents. Work with me: https://axelfreeman.com/marketing-engineer.html?utm_source=npm&utm_medium=readme&utm_campaign=mindset
|
|
78
98
|
### Dated plan: `plan`
|
|
79
99
|
|
|
80
100
|
```
|
|
81
101
|
npx -y marketing-mindset plan --base 0.03 --lift 0.2 --daily 500 --weeks 4
|
|
82
102
|
```
|
|
83
103
|
|
|
84
|
-
The floor from `sample`/`budget` laid out on a calendar: contacts per week, cumulative volume against the floor, the first date the test can honestly be read, the mechanics-only first week, and an honest verdict when the volume cannot reach the floor inside the plan. `--json` for agents. Work with me: https://axelfreeman.com/marketing-engineer.html
|
|
104
|
+
The floor from `sample`/`budget` laid out on a calendar: contacts per week, cumulative volume against the floor, the first date the test can honestly be read, the mechanics-only first week, and an honest verdict when the volume cannot reach the floor inside the plan. `--json` for agents. Work with me: https://axelfreeman.com/marketing-engineer.html?utm_source=npm&utm_medium=readme&utm_campaign=mindset
|
|
85
105
|
|
|
86
106
|
## `brief` — the plan as markdown you can send
|
|
87
107
|
|
|
@@ -93,7 +113,7 @@ One page: the floor in contacts per arm and in total, what reaching it costs at
|
|
|
93
113
|
the dated read, a week-by-week table with the percentage of the floor covered, the three gates, and the
|
|
94
114
|
kill rule. `--json` returns the same object for an agent or a script.
|
|
95
115
|
|
|
96
|
-
Method, packages and the paid version of this work: https://axelfreeman.com/marketing-engineer.html
|
|
116
|
+
Method, packages and the paid version of this work: https://axelfreeman.com/marketing-engineer.html?utm_source=npm&utm_medium=readme&utm_campaign=mindset
|
|
97
117
|
|
|
98
118
|
## `mde` — what a volume you can afford can actually read
|
|
99
119
|
|
|
@@ -200,3 +220,28 @@ The floor comes from the same formula as `sample`, and the arms split the earlie
|
|
|
200
220
|
so a unit chosen here, a queue planned with `queue` and a result read with `verdict` stay consistent.
|
|
201
221
|
Two things it refuses to hide: the earliest step reads soonest and owns the least, and money always reads
|
|
202
222
|
one step behind whatever unit you chose.
|
|
223
|
+
|
|
224
|
+
## `fit` — which channel can actually fund a verdict
|
|
225
|
+
|
|
226
|
+
Two channels can both be worth testing and only one of them can be read this quarter. `fit` takes
|
|
227
|
+
`name:contacts_per_month:cost_per_contact` for each channel, applies the same floor as `sample`, and
|
|
228
|
+
says which channel reaches that floor inside the horizon and what one readable test costs there.
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
npx marketing-mindset fit --channels email:9000:0.02,linkedin:1200:0.35,seo:30000:0.005
|
|
232
|
+
```
|
|
233
|
+
```
|
|
234
|
+
email: 9,000 contacts a month at 0.02 per contact -> floor 13,914 per arm, 4,500 events per arm a month, 4 month(s) to a verdict, one test 556.56, fits: yes
|
|
235
|
+
linkedin: 1,200 contacts a month at 0.35 per contact -> floor 13,914 per arm, 600 events per arm a month, 24 month(s) to a verdict, one test 9739.8, fits: no
|
|
236
|
+
seo: 30,000 contacts a month at 0.005 per contact -> floor 13,914 per arm, 15,000 events per arm a month, 1 month(s) to a verdict, one test 139.14, fits: yes
|
|
237
|
+
first to run: seo - 139.14 for one readable test in 1 month(s)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
At a 3% base rate and a +20% relative lift the floor is 13,914 contacts per arm on every channel, so the
|
|
241
|
+
channel is not chosen by a better conversion rate but by how fast it can deliver those contacts and what
|
|
242
|
+
each contact costs. `--json` returns the same object: one entry per channel with `floor_per_arm`,
|
|
243
|
+
`events_per_arm_per_month`, `months_to_verdict`, `cost_per_arm`, `cost_per_test` and `fits`, plus
|
|
244
|
+
`first_to_run`, `unreadable_channels` and three gates. `first_to_run` is the fitting channel with the
|
|
245
|
+
lowest `cost_per_test`, ties broken by fewest months; when no channel fits it is a sentence saying so.
|
|
246
|
+
Two things it refuses to hide: the floor is set by the base rate and the lift rather than by the channel,
|
|
247
|
+
and a cheap contact list that cannot reach the floor inside the horizon funds nothing.
|
package/bin/cli.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* marketing-mindset verdict read a finished test: interval, floor, honest verdict
|
|
8
8
|
* marketing-mindset queue volume into a queue of honest tests with read-out dates
|
|
9
9
|
* marketing-mindset cost the cost of one readable verdict, and funded tests per budget
|
|
10
|
+
* marketing-mindset fit which channel can actually fund a verdict inside a horizon
|
|
10
11
|
* marketing-mindset unit which funnel step can fund a verdict, and in how many months
|
|
11
12
|
* marketing-mindset sections list every section of SKILL.md
|
|
12
13
|
* marketing-mindset read <text> print one section by fuzzy title match
|
|
@@ -65,6 +66,107 @@ function readSkill() {
|
|
|
65
66
|
return fs.readFileSync(SKILL, "utf8");
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
/* --- UTM tagging: every OUTBOUND link carries source, medium, campaign -------------------
|
|
70
|
+
* Rule: tag links that leave for our own domains; never touch internal site links,
|
|
71
|
+
* never tag a link twice, never drop existing query params.
|
|
72
|
+
*/
|
|
73
|
+
const OUR_HOSTS = [
|
|
74
|
+
"axelfreeman.com", "axelfreeman.ru", "axelfreeman.github.io", "tapacapi.com", "tapac.ai",
|
|
75
|
+
"gymcamanalytics.com", "releazer.online", "dublicator.online", "spbilo.ru", "avtootkliki.ru",
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
function utmTag(url, source, medium, campaign, content) {
|
|
79
|
+
if (!/^https?:\/\//i.test(url)) return url;
|
|
80
|
+
let host;
|
|
81
|
+
try {
|
|
82
|
+
host = new URL(url).hostname.replace(/^www\./, "");
|
|
83
|
+
} catch (e) {
|
|
84
|
+
return url;
|
|
85
|
+
}
|
|
86
|
+
if (OUR_HOSTS.indexOf(host) === -1) return url; // only our own destinations
|
|
87
|
+
if (/[?&]utm_source=/i.test(url)) return url; // idempotent
|
|
88
|
+
const parts = [];
|
|
89
|
+
if (source) parts.push("utm_source=" + encodeURIComponent(source));
|
|
90
|
+
if (medium) parts.push("utm_medium=" + encodeURIComponent(medium));
|
|
91
|
+
if (campaign) parts.push("utm_campaign=" + encodeURIComponent(campaign));
|
|
92
|
+
if (content) parts.push("utm_content=" + encodeURIComponent(content));
|
|
93
|
+
if (!parts.length) return url;
|
|
94
|
+
return url + (url.indexOf("?") === -1 ? "?" : "&") + parts.join("&");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function runUtm(opts) {
|
|
98
|
+
const source = opts.source ? String(opts.source) : null;
|
|
99
|
+
const medium = opts.medium ? String(opts.medium) : "referral";
|
|
100
|
+
const campaign = opts.campaign ? String(opts.campaign) : null;
|
|
101
|
+
const content = opts.content ? String(opts.content) : null;
|
|
102
|
+
|
|
103
|
+
// Tag links in prose, but never inside fenced blocks or `inline code` — those are examples.
|
|
104
|
+
function tagText(text) {
|
|
105
|
+
let n = 0;
|
|
106
|
+
let inFence = false;
|
|
107
|
+
const lines = text.split("\n").map((line) => {
|
|
108
|
+
if (/^\s*(```|~~~)/.test(line)) {
|
|
109
|
+
inFence = !inFence;
|
|
110
|
+
return line;
|
|
111
|
+
}
|
|
112
|
+
if (inFence) return line;
|
|
113
|
+
const parts = line.split("`");
|
|
114
|
+
return parts
|
|
115
|
+
.map((seg, i) => {
|
|
116
|
+
if (i % 2 === 1) return seg; // inline code — leave as is
|
|
117
|
+
return seg.replace(/https?:\/\/[^\s)>"'*`]+/g, (u) => {
|
|
118
|
+
const tailMatch = /[.,;:!?]+$/.exec(u); // sentence punctuation is not part of the url
|
|
119
|
+
const tail = tailMatch ? tailMatch[0] : "";
|
|
120
|
+
const core = tail ? u.slice(0, -tail.length) : u;
|
|
121
|
+
const t = utmTag(core, source, medium, campaign, content);
|
|
122
|
+
if (t !== core) n++;
|
|
123
|
+
return t + tail;
|
|
124
|
+
});
|
|
125
|
+
})
|
|
126
|
+
.join("`");
|
|
127
|
+
});
|
|
128
|
+
return { text: lines.join("\n"), count: n };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (opts.file) {
|
|
132
|
+
const p = String(opts.file);
|
|
133
|
+
const src = fs.readFileSync(p, "utf8");
|
|
134
|
+
const res = tagText(src);
|
|
135
|
+
if (res.count) fs.writeFileSync(p, res.text);
|
|
136
|
+
console.log("tagged links: " + res.count + (res.count ? " in " + p : " (nothing to tag)"));
|
|
137
|
+
console.log("utm_source=" + (source || "-") + " utm_medium=" + medium + " utm_campaign=" + (campaign || "-"));
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const url = opts.url ? String(opts.url) : (opts._ ? String(opts._) : null);
|
|
142
|
+
if (!url) {
|
|
143
|
+
console.log(`utm — tag every outbound link, once, before it ships
|
|
144
|
+
|
|
145
|
+
Usage:
|
|
146
|
+
npx marketing-mindset utm <url> --source devto --medium article --campaign mindset
|
|
147
|
+
npx marketing-mindset utm --file article.md --source devto --medium article --campaign mindset
|
|
148
|
+
|
|
149
|
+
Convention:
|
|
150
|
+
utm_source the channel that sent the click devto | github | npm | telegram | habr | reddit | colony
|
|
151
|
+
utm_medium the placement article | readme | package | post | comment | profile | bio
|
|
152
|
+
utm_campaign the product or cluster mindset | tapac | hire | lyzhi | engine
|
|
153
|
+
|
|
154
|
+
Rules:
|
|
155
|
+
- internal links of your own site carry NO utm — they would restart the session
|
|
156
|
+
- tag only links that leave for our own domains; a third-party link stays clean
|
|
157
|
+
- never tag twice: a url that already has utm_source is returned untouched
|
|
158
|
+
- existing query params are kept`);
|
|
159
|
+
return 1;
|
|
160
|
+
}
|
|
161
|
+
const tagged = utmTag(url, source, medium, campaign, content);
|
|
162
|
+
console.log(tagged);
|
|
163
|
+
if (tagged === url) {
|
|
164
|
+
console.error("(not tagged: internal/no-own-domain/already tagged — see rules)");
|
|
165
|
+
return 1;
|
|
166
|
+
}
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
68
170
|
|
|
69
171
|
/* --- agent-readiness / AEO checklist (23 weighted checks) -------------------------------
|
|
70
172
|
* The same checklist that runs in the browser tool, as data: agents can read it and score
|
|
@@ -730,6 +832,125 @@ function runUnit(opts) {
|
|
|
730
832
|
return 0;
|
|
731
833
|
}
|
|
732
834
|
|
|
835
|
+
var FIT_ZA = { "0.05": 1.9599639845, "0.01": 2.5758293036, "0.1": 1.6448536270 };
|
|
836
|
+
var FIT_ZB = { "0.8": 0.8416212336, "0.9": 1.2815515655 };
|
|
837
|
+
function fitRound2(x) { return Math.round(x * 100) / 100; }
|
|
838
|
+
function fitFloor(rate, lift, power, alpha) {
|
|
839
|
+
const p1 = Math.min(Math.max(rate, 0.000001), 0.999999);
|
|
840
|
+
const p2 = Math.min(p1 * (1 + lift), 0.999999);
|
|
841
|
+
const pbar = (p1 + p2) / 2;
|
|
842
|
+
const za = FIT_ZA[String(alpha)] || FIT_ZA["0.05"];
|
|
843
|
+
const zb = FIT_ZB[String(power)] || FIT_ZB["0.8"];
|
|
844
|
+
const num = Math.pow(za * Math.sqrt(2 * pbar * (1 - pbar)) + zb * Math.sqrt(p1 * (1 - p1) + p2 * (1 - p2)), 2);
|
|
845
|
+
return Math.ceil(num / Math.pow(p2 - p1, 2));
|
|
846
|
+
}
|
|
847
|
+
function planFit(input) {
|
|
848
|
+
const channels = input.channels || [];
|
|
849
|
+
const base = Number(input.base !== undefined ? input.base : 0.03);
|
|
850
|
+
const lift = Number(input.lift !== undefined ? input.lift : 0.2);
|
|
851
|
+
const power = Number(input.power !== undefined ? input.power : 0.8);
|
|
852
|
+
const alpha = Number(input.alpha !== undefined ? input.alpha : 0.05);
|
|
853
|
+
const horizon = Number(input.horizon !== undefined ? input.horizon : 6);
|
|
854
|
+
const problems = [];
|
|
855
|
+
if (channels.length === 0) { problems.push("channels: pass --channels email:9000:0.02,linkedin:1200:0.35,seo:30000:0.005"); }
|
|
856
|
+
channels.forEach(function (c) {
|
|
857
|
+
if (!(Number(c.contacts_per_month) > 0)) { problems.push("contacts per month for '" + c.name + "' must be a positive number of contacts a month"); }
|
|
858
|
+
if (!(Number(c.cost_per_contact) >= 0)) { problems.push("cost per contact for '" + c.name + "' cannot be negative"); }
|
|
859
|
+
});
|
|
860
|
+
if (!(base > 0) || !(base < 1)) { problems.push("base rate: a number between 0 and 1 (0.03 = 3%)"); }
|
|
861
|
+
if (!(lift > 0) || !(lift < 5)) { problems.push("lift worth detecting: a fraction, 0.2 = +20% relative"); }
|
|
862
|
+
if (!FIT_ZB[String(power)]) { problems.push("power must be 0.8 or 0.9"); }
|
|
863
|
+
if (!FIT_ZA[String(alpha)]) { problems.push("alpha must be 0.05, 0.01 or 0.1"); }
|
|
864
|
+
if (!(horizon > 0)) { problems.push("horizon: months you are willing to wait, a positive number"); }
|
|
865
|
+
if (problems.length) { return { problems: problems }; }
|
|
866
|
+
|
|
867
|
+
const floor = fitFloor(base, lift, power, alpha);
|
|
868
|
+
const rows = channels.map(function (c) {
|
|
869
|
+
const perMonth = Number(c.contacts_per_month);
|
|
870
|
+
const cost = Number(c.cost_per_contact);
|
|
871
|
+
const events = perMonth / 2;
|
|
872
|
+
const months = Math.ceil(floor / events);
|
|
873
|
+
const costArm = fitRound2(floor * cost);
|
|
874
|
+
return {
|
|
875
|
+
channel: c.name, contacts_per_month: perMonth, cost_per_contact: cost,
|
|
876
|
+
floor_per_arm: floor, events_per_arm_per_month: events, months_to_verdict: months,
|
|
877
|
+
cost_per_arm: costArm, cost_per_test: fitRound2(costArm * 2), fits: months <= horizon
|
|
878
|
+
};
|
|
879
|
+
});
|
|
880
|
+
let best = null;
|
|
881
|
+
rows.forEach(function (c) {
|
|
882
|
+
if (!c.fits) { return; }
|
|
883
|
+
if (!best || c.cost_per_test < best.cost_per_test ||
|
|
884
|
+
(c.cost_per_test === best.cost_per_test && c.months_to_verdict < best.months_to_verdict)) { best = c; }
|
|
885
|
+
});
|
|
886
|
+
const none = "none of the listed channels can finish a verdict inside " + horizon + " month(s)";
|
|
887
|
+
const unreadable = rows.filter(function (c) { return !c.fits; }).map(function (c) { return c.channel; });
|
|
888
|
+
const gates = [
|
|
889
|
+
"the floor does not depend on the channel: " + fmtNum(floor) + " contacts per arm in every row, because it comes from the base rate and the lift",
|
|
890
|
+
best
|
|
891
|
+
? "fund " + best.channel + " first: " + best.cost_per_test + " for one readable test (" + best.months_to_verdict + " month(s) at " + best.cost_per_contact + " per contact)"
|
|
892
|
+
: none + " - move the horizon or find a channel with more contacts",
|
|
893
|
+
unreadable.length
|
|
894
|
+
? "out of reach inside " + horizon + " month(s): " + unreadable.join(", ")
|
|
895
|
+
: "every channel listed reaches a verdict inside " + horizon + " month(s)"
|
|
896
|
+
];
|
|
897
|
+
return {
|
|
898
|
+
base_rate: base, lift: lift, power: power, alpha: alpha, horizon_months: horizon, floor_per_arm: floor,
|
|
899
|
+
channels: rows,
|
|
900
|
+
first_to_run: best ? best.channel : none,
|
|
901
|
+
first_to_run_cost_per_test: best ? best.cost_per_test : 0,
|
|
902
|
+
first_to_run_months: best ? best.months_to_verdict : 0,
|
|
903
|
+
unreadable_channels: unreadable, gates: gates
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function runFit(opts) {
|
|
908
|
+
const spec = opts.channels;
|
|
909
|
+
const base = Number(opts.base !== undefined ? opts.base : 0.03);
|
|
910
|
+
const lift = Number(opts.lift !== undefined ? opts.lift : 0.2);
|
|
911
|
+
const power = Number(opts.power !== undefined ? opts.power : 0.8);
|
|
912
|
+
const alpha = Number(opts.alpha !== undefined ? opts.alpha : 0.05);
|
|
913
|
+
const horizon = Number(opts.horizon !== undefined ? opts.horizon : 6);
|
|
914
|
+
const problems = [];
|
|
915
|
+
let channels = [];
|
|
916
|
+
if (typeof spec !== "string" || !spec.trim()) {
|
|
917
|
+
problems.push("channels: pass --channels email:9000:0.02,linkedin:1200:0.35,seo:30000:0.005");
|
|
918
|
+
} else {
|
|
919
|
+
spec.split(",").forEach(function (part) {
|
|
920
|
+
const bits = part.trim().split(":");
|
|
921
|
+
if (bits.length !== 3 || !bits[0].trim() || !bits[1].trim() || !bits[2].trim() ||
|
|
922
|
+
!isFinite(Number(bits[1])) || !isFinite(Number(bits[2]))) {
|
|
923
|
+
problems.push("channels: '" + part.trim() + "' must look like name:contacts_per_month:cost_per_contact");
|
|
924
|
+
} else {
|
|
925
|
+
channels.push({ name: bits[0].trim(), contacts_per_month: Number(bits[1]), cost_per_contact: Number(bits[2]) });
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
if (problems.length) { console.error("fit: " + problems.join("; ")); return 1; }
|
|
930
|
+
const out = planFit({ channels: channels, base: base, lift: lift, power: power, alpha: alpha, horizon: horizon });
|
|
931
|
+
if (out.problems) { console.error("fit: " + out.problems.join("; ")); return 1; }
|
|
932
|
+
if (opts.json) { console.log(JSON.stringify(out, null, 2)); } else {
|
|
933
|
+
const head = ["channel", "contacts/mo", "cost/contact", "floor/arm", "events/arm/mo", "months", "cost/test", "fits"];
|
|
934
|
+
const rows = out.channels.map(function (c) {
|
|
935
|
+
return [c.channel, fmtNum(c.contacts_per_month), String(c.cost_per_contact), fmtNum(c.floor_per_arm),
|
|
936
|
+
fmtNum(c.events_per_arm_per_month), String(c.months_to_verdict), String(c.cost_per_test),
|
|
937
|
+
c.fits ? "yes" : "no"];
|
|
938
|
+
});
|
|
939
|
+
const widths = head.map(function (h, i) {
|
|
940
|
+
return Math.max(h.length, Math.max.apply(null, rows.map(function (r) { return r[i].length; })));
|
|
941
|
+
});
|
|
942
|
+
function pad(s, i) { return s + " ".repeat(widths[i] - s.length); }
|
|
943
|
+
console.log(head.map(pad).join(" "));
|
|
944
|
+
console.log(widths.map(function (w) { return "-".repeat(w); }).join(" "));
|
|
945
|
+
rows.forEach(function (r) { console.log(r.map(pad).join(" ")); });
|
|
946
|
+
console.log(out.first_to_run_cost_per_test
|
|
947
|
+
? "first to run: " + out.first_to_run + " - " + out.first_to_run_cost_per_test + " for one readable test in " + out.first_to_run_months + " month(s)"
|
|
948
|
+
: "first to run: " + out.first_to_run);
|
|
949
|
+
out.gates.forEach(function (g) { console.log("gate: " + g); });
|
|
950
|
+
}
|
|
951
|
+
return 0;
|
|
952
|
+
}
|
|
953
|
+
|
|
733
954
|
function fmtNum(n) {
|
|
734
955
|
return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
735
956
|
}
|
|
@@ -866,6 +1087,8 @@ if (cmd === "sample") {
|
|
|
866
1087
|
process.exit(runQueue(flags(rest)));
|
|
867
1088
|
} else if (cmd === "unit") {
|
|
868
1089
|
process.exit(runUnit(flags(rest)));
|
|
1090
|
+
} else if (cmd === "fit") {
|
|
1091
|
+
process.exit(runFit(flags(rest)));
|
|
869
1092
|
} else if (cmd === "cost") {
|
|
870
1093
|
process.exit(runCost(flags(rest)));
|
|
871
1094
|
} else if (cmd === "aeo") {
|
|
@@ -893,6 +1116,8 @@ if (cmd === "sample") {
|
|
|
893
1116
|
process.exit(1);
|
|
894
1117
|
}
|
|
895
1118
|
console.log("## " + hit.title + "\n" + hit.body.join("\n").trim());
|
|
1119
|
+
} else if (cmd === "utm") {
|
|
1120
|
+
process.exit(runUtm(flags(rest)));
|
|
896
1121
|
} else if (cmd === "install") {
|
|
897
1122
|
require(path.join(__dirname, "install.js"));
|
|
898
1123
|
} else {
|
|
@@ -913,6 +1138,8 @@ Usage:
|
|
|
913
1138
|
npx marketing-mindset verdict was the difference real, and was the volume ever enough?
|
|
914
1139
|
npx marketing-mindset queue how many honest tests your volume can finish, and when each reads
|
|
915
1140
|
npx marketing-mindset cost what one readable verdict costs to finish, and how many a budget funds
|
|
1141
|
+
npx marketing-mindset fit which channel can actually fund a verdict, and what one test costs there
|
|
1142
|
+
--channels email:9000:0.02,linkedin:1200:0.35,seo:30000:0.005 [--base 0.03] [--lift 0.2] [--power 0.8] [--alpha 0.05] [--horizon 6] [--json]
|
|
916
1143
|
npx marketing-mindset unit which step of your funnel can fund a verdict, and how many months it needs
|
|
917
1144
|
--steps visits:12000,signups:900,activations:270,paid:40 [--lift 0.2] [--power 0.8] [--alpha 0.05] [--horizon 6]
|
|
918
1145
|
--cost 0.02 --daily 500 [--budget 5000] [--base 0.03] [--lift 0.2] [--power 0.8] [--alpha 0.05]
|
|
@@ -926,6 +1153,8 @@ Usage:
|
|
|
926
1153
|
--level senior --mode fulltime --surfaces seo,out,analytics
|
|
927
1154
|
npx marketing-mindset aeo agent-readiness / AEO checklist (23 weighted checks)
|
|
928
1155
|
--json machine-readable
|
|
1156
|
+
npx marketing-mindset utm tag every outbound link, once, before it ships
|
|
1157
|
+
--source devto --medium article --campaign mindset [--url <url> | --file article.md]
|
|
929
1158
|
npx marketing-mindset sections every section of the skill
|
|
930
1159
|
npx marketing-mindset read numbers print one section
|
|
931
1160
|
npx marketing-mindset install copy the skill into your agent's skills dir
|
package/package.json
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "marketing-mindset",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "A marketer's operating mindset as an installable agent skill and CLI: test-size floors, kill rules, honest verdicts on finished tests, which funnel step can fund a verdict, the lift a volume can read, a queue of readable tests with read-out dates, the cost of one readable verdict. npx marketing-mindset sample|kill|verdict|unit|queue|cost|mde|budget|brief|plan|warmup|aeo|jd|sections|read|install.",
|
|
3
|
+
"version": "1.14.0",
|
|
4
|
+
"description": "A marketer's operating mindset as an installable agent skill and CLI: test-size floors, kill rules, honest verdicts on finished tests, which funnel step can fund a verdict, which channel can actually fund a verdict, the lift a volume can read, a queue of readable tests with read-out dates, the cost of one readable verdict, and UTM tagging on every outbound link. npx marketing-mindset sample|kill|verdict|unit|fit|queue|cost|mde|budget|brief|plan|warmup|aeo|utm|jd|sections|read|install.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-skill",
|
|
7
7
|
"skill",
|
|
8
8
|
"marketing",
|
|
9
9
|
"growth",
|
|
10
10
|
"aeo",
|
|
11
|
+
"utm",
|
|
12
|
+
"attribution",
|
|
13
|
+
"link-tagging",
|
|
11
14
|
"claude-code",
|
|
12
15
|
"codex",
|
|
13
16
|
"llm",
|
|
@@ -15,6 +18,7 @@
|
|
|
15
18
|
"sample-size",
|
|
16
19
|
"test-queue",
|
|
17
20
|
"cost-per-verdict",
|
|
21
|
+
"channel-fit",
|
|
18
22
|
"funnel-unit",
|
|
19
23
|
"ab-test-verdict",
|
|
20
24
|
"minimum-detectable-effect",
|
package/skill/SKILL.md
CHANGED
|
@@ -236,6 +236,25 @@ Sharp, honest, zero fluff. No corporate-speak, no generic templates, no hedging.
|
|
|
236
236
|
- Using paid data sources when free ones exist
|
|
237
237
|
- Working for free — zero-exchange tasks, "for the portfolio", or treating training time as free
|
|
238
238
|
|
|
239
|
+
## Tagging: Every Outbound Link Carries UTM
|
|
240
|
+
|
|
241
|
+
A campaign you cannot attribute is a campaign you cannot kill. The rule is mechanical:
|
|
242
|
+
**every link that leaves for your own property carries UTM; internal links never do.**
|
|
243
|
+
|
|
244
|
+
- `utm_source` — the channel that sent the click: `devto`, `github`, `npm`, `telegram`, `habr`, `reddit`, `colony`.
|
|
245
|
+
- `utm_medium` — the placement: `article`, `readme`, `package`, `post`, `comment`, `profile`, `bio`.
|
|
246
|
+
- `utm_campaign` — the product or cluster: `mindset`, `tapac`, `hire`, `lyzhi`, `engine`.
|
|
247
|
+
|
|
248
|
+
Rules that keep the data clean:
|
|
249
|
+
|
|
250
|
+
- Tag only destinations you own. A third-party link stays clean.
|
|
251
|
+
- **Never tag internal site links.** An internal link with UTM restarts the session, so one visit looks like two and the page's own report becomes wrong.
|
|
252
|
+
- Never tag twice: a URL that already has `utm_source` is done. Tagging happens before the text ships, not after.
|
|
253
|
+
- Keep existing query params — append, never rebuild the URL.
|
|
254
|
+
- Run it: `npx marketing-mindset utm --file article.md --source devto --medium article --campaign mindset`
|
|
255
|
+
|
|
256
|
+
Why it pays: a channel with a tagged link can be read in the same report as everything else and killed on evidence. An untagged one only feels like it is working.
|
|
257
|
+
|
|
239
258
|
## Pitfalls
|
|
240
259
|
|
|
241
260
|
- The temptation to answer from general marketing knowledge is strong — resist it; check fresh, recent context first.
|