VeveeBlog · 6 min read
Blog · 6 min read

Paywall copy that rewrites itself for every user

Your paywall says "Upgrade to Pro for unlimited generations." A teacher reads it and shrugs. A student on a budget reads it and closes the tab. The same words, two lost conversions - because the words were written for nobody in particular.

Last updated: 2026-06-03

One paywall, many readers

The conversion moment is the least personalized screen in most products. A user hits a limit, sees a generic "Upgrade to Pro," and decides in about three seconds. That decision depends entirely on whether the copy connects what they were just doing to what they would get. A static headline cannot do that, because the same string ships to a teacher building quizzes for thirty students, a student watching every dollar, and a founder evaluating the tool for a team. Each of them needs a different reason to pay, and you are giving all three the same sentence.

The objection is different for each persona

Conversion copy works when it answers the specific objection in the reader’s head. Those objections are not the same. A teacher cares about saving prep time across many students. A student cares about price and whether the free tier is enough. A founder cares about seats, limits, and whether it scales to a team. Write one headline and you answer one of those objections and miss the other two. The information to know which reader you are talking to is already in your analytics: their role, their usage shape, what they just hit a wall on.

  • user_usage - what they consumed and which limit they just hit
  • user_attributes - role, team size, declared goal
  • user_events - the feature they were mid-flow on when blocked
  • conversion_signals - what tends to precede an upgrade for users like them

Generate the pitch at the moment of friction

A compose type for the paywall reads the user’s usage and profile and returns structured copy: a headline, two or three value props, and a CTA label. You define the intent once - "Write a short, honest upgrade pitch for this user based on what they were doing and the limit they hit; speak to their role" - and an output schema. When a user hits a cap, your gate calls compose and renders the result instead of a hardcoded banner. The copy is grounded in their real numbers, so it can say "You have generated 47 quizzes this month" instead of "unlimited generations."

// Called from your limit gate, server-side
import { createClient, VeveeError } from '@vevee/sdk';

const vevee = createClient({ apiKey: process.env.VEVEE_SECRET_KEY! });

async function paywallCopy(userId: string) {
  try {
    const { output } = await vevee.compose<{
      headline: string;
      valueProps: string[];
      ctaLabel: string;
    }>('paywall-pitch', userId);
    return output;
  } catch (e) {
    if (e instanceof VeveeError) return STATIC_PAYWALL; // never hide the upgrade button
    throw e;
  }
}

The same wall, three pitches

One compose type, resolved per user, produces a pitch aimed at the person reading it. The teacher gets a time-saving frame, the student gets a price-and-value frame, the founder gets a scale-and-seats frame - from the same call, with no persona branching in your code. You tune the angles by editing the prompt in the dashboard and watching conversion move, not by shipping a new paywall component for every segment.

  • Teacher: "You’ve built 47 quizzes this month. Pro removes the cap so grading week never stops you."
  • Student: "You’re using Vevee like a pro. Pro is the price of one coffee a month and unlocks the rest of the semester."
  • Founder: "Your team is hitting the team limit. Pro adds seats and a shared usage dashboard for everyone."

Honesty beats hype, and the data enforces it

Because the copy is generated from real usage, it stays truthful: it references numbers the user can verify in their own dashboard, which is more persuasive than a superlative. Constrain the model with a tight output schema and a low maxOutputTokens so it returns a headline and a couple of props, not a wall of marketing text. Keep the prompt instruction explicit - speak to their role, cite their actual usage, no false scarcity. A personalized pitch that is also accurate converts better and ages better than a generic one that overpromises.

Measure it like any other conversion change

Generate the pitch when the user hits the limit, cache it for the life of that paywall view, and capture a paywall_shown analytics event alongside it so the personalized copy sits in the same funnel as everything else. Then compare upgrade rate for the generated paywall against your old static one. Compose returns usage.costMicroUsd per call, so you can put a real number on cost-per-incremental-conversion and decide, with data, whether personalized paywall copy pays for itself. For most AI products that charge a monthly subscription, one extra upgrade covers thousands of generations.

More from the blog

engineering · 5 min

How to reset usage limits when a subscription renews

A weekly plan, used twice. First week: ten generations. Second week: zero, because the counter never reset. This is the cron-job mistake - and the fix is one field on one call.

engineering · 6 min

How to manage subscription renewals: aligning Vevee with Stripe

A user signs up on Jan 15. Stripe charges them on the 15th of every month. Your metering layer resets on the 1st. Two clocks. One angry support ticket per cycle.

engineering · 5 min

Stop hardcoding your pricing page - render it from your metering layer

Every B2B SaaS I have shipped repeats the same mistake: plans live in two places - the dashboard that enforces them, and a const PLANS = [...] on the marketing site. They drift within a quarter.

thinking · 4 min

Meter AI by user, not by account - your margin depends on it

A few users will cost you 100x what your median user costs. If you only meter at the account level, you will not see them coming until your gross margin is gone.

engineering · 5 min

reserve / commit / release: the only correct way to enforce AI quotas

Every team I have seen build per-user AI metering has shipped a version of canUse → call OpenAI → track. It looks correct in single-threaded tests. It is broken in production.

thinking · 4 min

Why Stripe Billing is not enough for AI products

Stripe is excellent at one thing: turning usage into invoices. AI products need three other things, and Stripe does not do any of them.

engineering · 6 min

Dynamic onboarding: a different first step for every user

A teacher and a student sign up the same minute. The teacher wants to build a quiz; the student wants to summarize a lecture. Your onboarding shows them both the same five-step checklist. One of them bounces.