Optimize a paywall with an evidence-backed, slot-diverse portfolio of conversion hypotheses — not generic component advice.
CRITICAL: how this skill works (read first)
You do NOT diagnose frictions, draft hypotheses, pick evidence, or write the
report. All of that — friction diagnosis, the structural-twin retrieval, the
mechanism-matched evidence binding, the Confidence × Upside × Boldness scoring,
and the dark "Hallow" report rendering — runs server-side, reusing the
internal paywall pipeline (_agent_synthesize) so the output is identical to the
internal product. The model that writes the hypotheses is the internal model, not
you. Your job is small and mechanical:
- Ground the target paywall (capture / locate a real screenshot).
- Synthesize — call
lazyweb_start_paywall_synthesizewith the screenshot + product + conversion goal, then polllazyweb_get_paywall_synthesizeuntildone(~60-150s — it runs several LLM calls). You get back asynthesis_idand the 4 slot winners, each with aslot,hypothesis_title, and a ready-to-usemockup_prompt. - Mock up each winner — one mockup per winner via the async mockup pair
(
lazyweb_start_mockup→lazyweb_get_mockup), EDIT mode off the current screenshot, using the winner'smockup_prompt. - Render + host — call
lazyweb_render_report(report_skill="optimize-paywall", report_data={ synthesis_id, target_image, mockups, product }). The server reloads the synthesis and renders the full report. That URL is the deliverable.
Do NOT hand-write frictions, candidate hypotheses, evidence_refs,
experiment_verdicts, user_labels, or the report HTML/CSS — the server owns all
of it. Do NOT call lazyweb_paywall_retrieve or lazyweb_paywall_score yourself;
synthesize already does retrieval + scoring internally. After you get the URL,
summarize the 4 winners (one line per slot) and give the user the URL.
The work dir convention is $WORK = .lazyweb/optimize-paywall/{topic-slug}-{YYYY-MM-DD};
save the current screenshot + the generated mockups under $WORK/references/.
Lazyweb MCP Setup
Use hosted Lazyweb MCP tools at https://www.lazyweb.com/mcp. First list the
tools and run lazyweb_health. Required tools:
lazyweb_health— verify connectivity.lazyweb_start_paywall_synthesize+lazyweb_get_paywall_synthesize— THE core call (async). Start runs the full internal generation pipeline from the screenshot (label → retrieve twins → diagnose frictions → synthesize the slot-diverse portfolio with mechanism-bound evidence → stack-rank top-1 per slot). Poll get untildone. Result:{ synthesis_id, winners:[{slot, hypothesis_title, mockup_prompt, change_scope, evidence_company}] }.lazyweb_start_mockup+lazyweb_get_mockup— async paywall mockup generation (the synclazyweb_generate_mockuptimes out through the gateway; use the pair).lazyweb_render_report— server-renders + hosts the report (passreport_skill="optimize-paywall"andreport_data.synthesis_id).
Pass skill: "optimize-paywall" and version: "<x.y.z>" on every call (read
~/.lazyweb/VERSION, fall back "0.0.0"). Optional analytics; never drop a real arg.
If the MCP is missing/auth fails, tell the user to run
curl -fsSL https://www.lazyweb.com/install.sh | bash, reload, and rerun.
Step 1 — Ground the paywall
- Capture or read the target paywall. Prefer a real screenshot or URL over prose.
Save it to
$WORK/references/current-state.png— it becomes the "Current" column and the input to synthesis. Keep the file path — the helper script (Step 2) reads and base64-encodes it in code. Do NOT inline the base64 into a tool call yourself: a large blob gets corrupted in the agent's output ("does not represent a valid image" / "invalid base64-encoded value") and is size-hostile through the gateway.$SKILL_DIRbelow = the directory holding this SKILL.md (whereoptimize_paywall.pyships). - Author a short product brief — the single highest-signal input. This is what
makes the diagnosis specific to THIS product instead of generic corpus advice. In
~3–6 sentences cover: who the user is and why they're on this screen; where
it sits in the flow (what came before, what's after); the free vs paid
boundary — what paying actually unlocks vs the free tier, and why someone
upgrades; and the product's wedge vs alternatives. Use what the user told you
plus what you know about the product; ask ONE concise question only if the
product, conversion goal, or the free-vs-paid story is missing and you can't infer
it. You do NOT need to analyze the screenshot — the server labels the pixels —
but you DO provide this product knowledge: the server treats it as authoritative
and grounds every friction + hypothesis in it. Pass it via
--product-brief(inline, or@path/to/brief.md). Skipping it is the difference between fable-grade and generic output. - Detect platform + screen_type from the screenshot (routes the evidence):
platform:mobile(tall portrait phone screenshot) orweb(wide desktop/browser page).screen_type:paywall(in-app subscription offer) ·pricing(web plans/pricing page) ·landing(marketing homepage/hero) ·signup(account-creation / lead-capture). If it's none of these, tell the user this skill optimizes paywall/pricing/landing/signup and stop. Mobile paywalls behave exactly as before (--platform mobile,--screen-type paywall); pass both to the helper in Step 2. Forweb, evidence is single-snapshot learnings (observed patterns, not A/B-tested), so the report frames hypotheses as "worth testing," not measured lifts.
Step 2 — Synthesize (the server does the thinking)
Run the helper script optimize_paywall.py (it reads the screenshot file,
base64-encodes it in code, calls lazyweb_start_paywall_synthesize, and polls
lazyweb_get_paywall_synthesize for you — so the image bytes never pass through
your output, where they'd be corrupted or hit the gateway size limit):
python "$SKILL_DIR/optimize_paywall.py" synthesize \
--image "$WORK/references/current-state.png" \
--product "<product/company name; excluded from corpus so it isn't benchmarked vs itself>" \
--conversion-goal "<e.g. annual-plan share / trial starts>" \
--plan-structure "<e.g. monthly $6.99 / annual $59.99>" \
--product-brief "<who the user is; free vs paid + why upgrade; where this sits in the flow; the wedge>" \
--platform <mobile|web> --screen-type <paywall|pricing|landing|signup> \
[--category <cat>] [--constraints "<...>"] [--divergence auto|low|med|high] \
--out "$WORK/synthesis.json"
It prints (and writes to --out) { synthesis_id, winners:[{slot, hypothesis_title, change_scope, evidence_company, mockup_prompt}] }.
synthesis_id goes to render; each of the 4 winners carries a ready
mockup_prompt. The script needs LAZYWEB_MCP_TOKEN (or
~/.lazyweb/lazyweb_mcp_token); if your Python has no CA bundle, set
SSL_CERT_FILE to one (e.g. python3 -c 'import certifi;print(certifi.where())').
The script exits non-zero with a clear message on a bad image or a server
status:"error" — surface it and stop (don't hand-write a report).
(Already-hosted screenshot? Passing image_url directly to
lazyweb_start_paywall_synthesize also works. Never hand-pass image_base64 —
that is the fragile path this script exists to replace.)
Step 3 — Generate one mockup per winner (async)
For EACH of the 4 winners, generate a mockup that is an EDIT of the CURRENT
screenshot (keeps the real brand/layout/dimensions), conditioned on the winner's
mockup_prompt prefixed with the ENFORCED PREAMBLE below.
- If you ARE Codex → use built-in
image_gen(gpt-image-2) with the current screenshot as the reference image. - Otherwise (Claude Code, etc.) → run the helper script once per winner — it
posts the current screenshot byte-perfectly in EDIT mode (
image_base64built from the file in code,sizeomitted) and pollslazyweb_get_mockupfor you. Do NOT calllazyweb_generate_mockup(times out) and do NOT hand-passimage_base64. Run the 4 in parallel (background them):
Each printspython "$SKILL_DIR/optimize_paywall.py" mockup \ --image "$WORK/references/current-state.png" \ --prompt "<ENFORCED PREAMBLE + winner.mockup_prompt>" \ --out "$WORK/mock-<slot>.json" &{ image_url }(a signed URL) — use that as the winner's mockup, NOT base64 (four base64 mockups overflow the gateway request-size limit; the renderer fetches the URL server-side). - Fallback — only if a mockup truly can't be generated
(
MOCKUP_IMAGE_KEY_MISSING/MOCKUP_DAILY_LIMIT, anerrorstatus, or stillpendingafter ~180s): omit that one slot's key from themockupsmap. The server renders a "(no mockup)" placeholder for it; the rest of the report is unaffected. Never block the whole report on one missing mockup; never use ASCII art. A mix (3 real + 1 placeholder) is fine.
ENFORCED PREAMBLE — prepend to every winner's mockup_prompt
ENFORCED CONSTRAINTS — the output is a redesigned paywall mockup based on the baseline image. (1) EXACT VISUAL STYLE PRESERVATION: match the baseline's background color, brand palette, typography (family/weight/size hierarchy), border-radius, icon/illustration style, and overall feel — it must look like the same product. (2) DIMENSION + ASPECT PRESERVATION: keep the baseline's aspect ratio and status-bar / safe-area insets in the same proportions. (3) PRIMARY CTA PROMINENCE LOCK: the dominant purchase/subscribe button stays AT LEAST as large and as visually dominant as in the baseline — you may move it, never shrink it; if the change needs room, collapse other content rather than miniaturizing the CTA. (4) CHANGE SCOPE — apply the change at the
change_scopenamed for this winner (copy_tweak = change only the named text; component_swap = restyle/replace one component; section_restructure = reflow one section; full_redesign = restructure the layout while preserving brand identity
- aspect ratio). === CHANGE TO APPLY === {the winner's mockup_prompt}
Step 4 — Render + host the report
Call lazyweb_render_report with report_skill="optimize-paywall" and:
{
"synthesis_id": "<from lazyweb_get_paywall_synthesize>",
"target_image": "data:image/png;base64,… (the current paywall screenshot)",
"mockups": { "safe_bet":"<image_url>", "high_value_bet":"…", "bold_swing":"…", "contrarian":"…" },
"product": "<the product name, e.g. Reddit>"
}
Notes:
synthesis_idis the ONLY source of the frictions, hypotheses, evidence, prioritization, and momentum — the server reloads the stored synthesis and renders it. You do not passrecommendations/frictions/experiments/user_labels; they live in the synthesis.mockupsis keyed by each winner'sslot; each value is theimage_urlfromlazyweb_get_mockup. Omit a slot only if its mockup couldn't be generated.target_imageis the current screenshot as a base64data:URI (one image is fine through the gateway).- Pass a stable
idempotency_key(e.g."optimize-paywall/{topic}-{date}") so a re-render dedupes to the same URL. - On a
400withcode:"render_field_missing", thedetailnames the bad field (e.g.synthesis_id) — fix it and retry ONCE. If the synthesis expired, re-run Step 2.
The response { ok, id, url } carries the hosted report URL. That URL is the
deliverable.
After rendering
- Give the user the report URL.
- Summarize the 4 winners: one line per slot (Safe bet / High-value bet / Bold
swing / Contrarian), naming each winner's
hypothesis_titleand itsevidence_company(when present). - Suggest next steps: implement a winner, or ask
/lazyweb-ab-test-researchto mine the experiment corpus deeper.
Operating principles
- The server owns the thinking. Frictions, hypotheses, evidence binding,
scoring, momentum, and look all come from the internal pipeline via
synthesize. Do not second-guess, re-implement, or supplement them by hand-writing report_data — that's exactly what made earlier versions worse. - Synthesis is slow by design (~60-150s, several LLM calls). That latency is the internal pipeline doing real work; poll patiently, don't time out early.
- Don't fabricate a fallback report. If
synthesizeerrors, surface the error — never hand-assemble a substitute report, which would not match the internal output.