BlogMetering

One event, two limits: gate your premium model without forking your code

The premium model launch was going great until you looked at the bill: free users had figured out the good model and were living on it. You need a sub-limit. You do not need a second code path.

The sub-limit problem

Every multi-model AI app converges on the same plan shape: a generous total cap and a stingy premium cap. 200 images but only 20 on the flagship model. A million tokens but only 100k on the expensive one. The naive implementation is two counters and an if-statement at every call site - and then a third counter when marketing wants a "4k renders" sub-limit, and a fourth for video. Each new plan dimension forks the metering code again, and six months in, your pricing logic is smeared across a dozen call sites that must all agree.

Match rules invert the ownership

Vevee’s model is that an event does not belong to a counter - it matches limit groups, plural. A limit group is a quota plus match rules: patterns over event_type and metadata. When you track image.flux-pro, every group whose rules match increments: the "premium images" group matching image.flux-pro AND the "total images" group matching image.*. When you call canUse, every matched group must allow - the tightest cap wins. The plan owns which limits exist; your code only reports what happened.

The code does not know the plan exists

Here is the entire call-site footprint for an app with a total cap, a premium cap, and a 4k variant cap. Notice what is absent: no plan lookup, no branching, no knowledge of how many groups exist. You could add a fifth limit dimension in the dashboard tonight and this code ships zero changes.

// One line per call site, regardless of how many limits apply
const ok = await vevee.can(userId, 'image.flux-pro', 1, { variant: '4k' });
if (!ok) return paywall();

const img = await flux.generate(prompt, { resolution: '4k' });

await vevee.track(userId, 'image.flux-pro', 1, { variant: '4k' });
// -> increments "total images" (image.*),
//    "premium images" (image.flux-pro),
//    and "4k renders" (image.* + variant=4k) - all from this one event

Name events like you’ll need the hierarchy

The match rules reward a naming convention: make the event type a path from general to specific. image.flux-pro and image.sdxl both match image.*; llm.gpt-5.2 and llm.claude both match llm.*. Put the stable dimension in the event name and the volatile one in metadata - model goes in the name (plans gate on it), output tier goes in metadata.variant (the dashboard has a reserved variant key for exactly this). Decide this in week one; it is the only metering decision that is annoying to change later.

When the answer is no, you know why

canUse returns the full picture, not a bare boolean: which groups matched, each group’s quota and current count, and reasons when blocked. That means your paywall can say "you’ve used all 20 premium generations - 130 standard ones remain" instead of a generic "limit reached". The difference matters for conversion: a user blocked on the premium cap but flush on the total cap should be steered toward the standard model or the upgrade, and those are different buttons. The data to render the right one is already in the response you got.

Plans become a dashboard edit

The payoff compounds when pricing changes - which it will. Tightening the premium cap from 20 to 15, adding a "video seconds" group, giving the Pro plan a separate 4k allowance: all of it is plan-builder edits, none of it is deploys. Your pricing experiments stop being engineering projects, which means you actually run them. The teams that converge on good AI pricing are not the ones who guessed right initially - they are the ones for whom iterating was cheap.

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