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 eventName 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.
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.