BlogCompose

Stop making users do math on your pricing page: recommend their plan from their own usage

Every pricing page asks the user to solve an estimation problem: "How many credits will I need per month?" Nobody knows. But for any user who has actually used your product, their usage history IS the answer - here is how to put it on the pricing page.

"Which plan fits me?" as an API call

Every pricing page asks the user to solve an estimation problem: "How many credits/tokens/generations will I need per month?" Nobody knows. So they either pick the cheapest tier (and churn when they hit limits) or don't pick at all (and you lose them at the highest-intent page on your site). Meanwhile, for any user who's actually used your product - trial, free tier, whatever - you already know the answer. Their usage history IS the answer. I meter per-user AI usage with Vevee (@vevee/sdk). Its compose() method generates structured output from a user's real usage data, so a plan recommendation is one typed call. The compose type's prompt: "Given this user's usage this period and their growth trend, recommend the cheapest plan they will NOT outgrow within 2 months. Show the math in their own numbers. Never recommend a bigger plan than the data supports." Data sources: user usage, user events. The output schema constrains recommendedPlan to your actual tier names, so the model can't invent a plan.

import { createClient } from "@vevee/sdk";

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

interface PlanRecommendation {
  recommendedPlan: "starter" | "pro" | "max";
  reasoning: string;       // shown to the user, in their numbers
  projectedUsage: string;  // e.g. "~180 generations/mo at your pace"
  wouldHitLimitOn: string; // which cheaper plan they'd outgrow, and when
}

const rec = await vevee.compose<PlanRecommendation>(
  "plan-recommendation",
  userId,
  { plans: PLAN_SUMMARY } // your tiers + limits as vars
);

if (rec.status === "generated") {
  renderRecommendedBadge(rec.output);
}
// opted_out → render the plain pricing table, no badge

What the user sees

Instead of three equal columns and a shrug, the Pro column gets a badge. That's not marketing copy - that's an analyst's answer, in the user's own numbers, generated at render time:

  • Recommended for you
  • You've averaged 140 generations/month over the last 6 weeks and hit the Free cap twice.
  • Starter's 200/mo cap would last you about 3 weeks.
  • Pro covers your pace (~180/mo projected) with room to grow.
The honesty is the conversion mechanism: when the model tells a light user "Starter is plenty for you," every future recommendation becomes credible.

Yes, recommend the cheaper plan sometimes

The counterintuitive part that makes this work: the prompt explicitly forbids over-recommending. Short-term you "lose" a few Pro sales to Starter. Long-term you get:

  • Users on correctly-sized plans, who don't churn from sticker regret
  • Upgrades that happen at the natural moment (they outgrow the plan the data predicted they'd outgrow - and your limit-reached flow catches that)
  • A pricing page that users trust, which is rarer than any feature

Measure the lift

The SDK's analytics taxonomy has the checkout funnel built in as reserved events. Split the funnel by variant (badge vs. no badge - opted-out users are your natural control group) and track whether recommended users pick the recommended plan. Two dashboards, no extra infra.

await vevee.capture({ distinctId: userId, event: "paywall_shown",
  properties: { surface: "pricing_page", variant: "recommended" } });

await vevee.capture({ distinctId: userId, event: "checkout_started",
  properties: { plan: rec.output.recommendedPlan } });

await vevee.capture({ distinctId: userId, event: "checkout_completed" });

Costs and guardrails

The pricing page is the only page where the user is actively trying to predict their own future usage. You have their past usage. Connecting those two things was always the obvious move - it just used to take a data pipeline, and now it takes a typed function call. The guardrails that keep it safe and cheap:

  • Server-side only (sk_* secret key) - compose in the page's server component or route handler.
  • Capped by a monthly compose budget you set in the dashboard; budget exhausted → no badge, plain pricing table. The page never breaks.
  • Each call reports usage.costMicroUsd - typically fractions of a cent, and pricing-page traffic from logged-in users (the only ones this applies to) is small.
  • Cache per user per day. Usage trends don't move faster than that.
Never call compose() from the browser. It runs on your secret key against per-user usage data - keep it in a server component or route handler, and let opted-out users fall through to the plain pricing table.
See your own numbers in two minutes.Add the SDK and your users, costs and revenue fill in on the next request.
Join waitlist

Keep reading

ShareCopy a link to this post.

Set your first limit in about five minutes.

50k operations free every month. No card required.
Join waitlist