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.
Why hardcoded pricing pages drift
You ship v1 with a clean const PLANS = [{ name: "Pro", images: 500 }]. Two weeks later you bump Pro to 1000 images in the dashboard because a deal needs it, and you ship. The marketing page still says 500. A prospect signs up, sees 1000 in the app, and either feels cheated (the page lied) or never knows about the extra (you under-sold). Multiply by every plan, every limit, every metadata-gated variant. The drift is invisible until a customer asks.
The single-source-of-truth fix
If your metering layer already owns the canonical plan structure - names, limits, units, period rollover, even which models count toward which bucket - your pricing page should read from it. Not duplicate it. The API call is dirt cheap (one row read per plan, cacheable for minutes), and the response shape maps cleanly onto a pricing card.
What "rendering from metering" actually looks like
In Vevee the endpoint is GET /api/v1/plans (or vevee.availablePlans() from the SDK). It accepts a public key - same key class you would expose to a browser - and returns every plan for the calling app with id, display name, period type ("monthly"), period anchor ("subscription_start" vs "calendar"), and the full limit-group list. Each limit group includes its label, unit (count / tokens / seconds / cents), quota, the event pattern it meters against (e.g. "image.*"), and the full match rules - so you can render "500 Flux Pro renders (4k only)" without re-encoding the filter logic in your frontend.
What you keep doing yourself
Two things stay on your side. One: price tags. The metering layer is not the right home for "$20/mo" - Stripe is. Keep a small map of planId → display copy, or join Stripe products via metadata. Two: display order. APIs return plans in creation order; pricing pages need a deliberate cheap → expensive sequence. Hardcode an array of plan ids if you reshuffle plans often.
Variant-gated limits are the real win
The case for fetching plans is strongest when plans differ on metadata, not just total quotas. "Pro gets 4k images, Free is capped at 1k" is one match rule per group - and the moment you express it in code on the frontend, you have two copies of the same truth and a bug factory. availablePlans() ships the full matches array, so the pricing page describes the gating exactly as the meter enforces it. Same line of truth on both sides.
It is the same instinct as feature flags
Hardcoding plan structure on a marketing site is the same anti-pattern as hardcoding feature-flag values in client code. You centralise flags because they change faster than deploys. Plans change faster than deploys too - every promo, every enterprise carve-out, every Q4 price test. Putting them behind one API and reading from it consistently is just good hygiene. We added availablePlans() to Vevee specifically to close this gap.
Keep reading
The race condition in "if (usage < limit)" that is costing your AI app money
A user at nine of ten images opens six tabs and clicks generate in all of them. Every tab passes your limit check, every tab gets an image, and you pay for all six. The bug is one read-then-write - and your unit tests will never catch it.
MeteringHow to build a credits system for your AI app (ledger design, rollover, refunds)
A user emails: "I had 40 credits this morning and now I have 12, and I only made one image." If your balance is a column you mutated, you cannot answer that email. Here is the ledger design that can.