Follow-up to #3199 / PR #4563. Surfaced while running the shadow-mode limit-abuse audit against Axiom wm_api_usage.
Problem
Usage telemetry can't attribute an API-key request to the plan/caps it was subject to, so the limit-abuse audit (the thing the #3199 shadow rollout exists to enable) can't be run from telemetry alone — it needs an external Convex/Dodo plan lookup.
Two concrete gaps observed in wm_api_usage:
tier is 0 for most API-key requests. usage.tier is only populated on tier-gated routes; for API-key traffic on non-tier-gated public routes it stays unset → emits tier: 0. But every user_api_key holder is a paid tier 2+ customer (createApiKey gates on apiAccess). Audit result showed 56,988 user_api_key events at tier:0 vs 9,452 at tier:2 — the field is unreliable.
- Starter and Business are indistinguishable. Even with
tier populated, api_starter and api_business are both tier: 2, so the audit cannot tell which cap (60/min·1k/day vs 300/min·10k/day) applied to a given customer without looking up their plan elsewhere.
Impact
The 2026-06-30 audit found 4 customers over the Starter caps but under Business caps; deciding whether enforcing would hurt them required a manual plan lookup per customer. That's exactly the friction this telemetry should remove.
Proposed fix
In the gateway per-account rate-limit block (server/gateway.ts, the #3199 block where getEntitlements(sessionUserId) is already resolved):
- Set
usage.tier = ent.features.tier for eligible user keys (and 3 for enterprise) so the field reflects reality.
- Add a
plan_key field to the usage event (from ent.planKey; 'enterprise' for env keys) so the audit can group by plan and compare each customer's rate against the correct cap with no external lookup. (Recording the resolved caps directly — api_rate_limit / api_daily_allowance — is an equivalent alternative; decide during implementation.)
Thread the new field through UsageIdentityInput → buildUsageIdentity → RequestEvent → buildRequestEvent (mirroring how auth_kind/customer_id flow). Additive/optional — no Axiom schema migration needed.
Acceptance
Follow-up to #3199 / PR #4563. Surfaced while running the shadow-mode limit-abuse audit against Axiom
wm_api_usage.Problem
Usage telemetry can't attribute an API-key request to the plan/caps it was subject to, so the limit-abuse audit (the thing the #3199 shadow rollout exists to enable) can't be run from telemetry alone — it needs an external Convex/Dodo plan lookup.
Two concrete gaps observed in
wm_api_usage:tieris0for most API-key requests.usage.tieris only populated on tier-gated routes; for API-key traffic on non-tier-gated public routes it stays unset → emitstier: 0. But everyuser_api_keyholder is a paid tier 2+ customer (createApiKeygates onapiAccess). Audit result showed 56,988user_api_keyevents attier:0vs 9,452 attier:2— the field is unreliable.tierpopulated,api_starterandapi_businessare bothtier: 2, so the audit cannot tell which cap (60/min·1k/day vs 300/min·10k/day) applied to a given customer without looking up their plan elsewhere.Impact
The 2026-06-30 audit found 4 customers over the Starter caps but under Business caps; deciding whether enforcing would hurt them required a manual plan lookup per customer. That's exactly the friction this telemetry should remove.
Proposed fix
In the gateway per-account rate-limit block (
server/gateway.ts, the#3199block wheregetEntitlements(sessionUserId)is already resolved):usage.tier = ent.features.tierfor eligible user keys (and3for enterprise) so the field reflects reality.plan_keyfield to the usage event (froment.planKey;'enterprise'for env keys) so the audit can group by plan and compare each customer's rate against the correct cap with no external lookup. (Recording the resolved caps directly —api_rate_limit/api_daily_allowance— is an equivalent alternative; decide during implementation.)Thread the new field through
UsageIdentityInput→buildUsageIdentity→RequestEvent→buildRequestEvent(mirroring howauth_kind/customer_idflow). Additive/optional — no Axiom schema migration needed.Acceptance
tier(2/3, not 0).plan_keyor the resolved caps) distinguishing Starter from Business.wm_api_usagealone.