Skip to content

fix(telemetry): attribute API-key usage events to tier + plan so the limit-abuse audit is self-serve (#3199 follow-up) #4572

Description

@koala73

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:

  1. 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.
  2. 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 UsageIdentityInputbuildUsageIdentityRequestEventbuildRequestEvent (mirroring how auth_kind/customer_id flow). Additive/optional — no Axiom schema migration needed.

Acceptance

  • An API-key request on a non-tier-gated route emits the customer's real tier (2/3, not 0).
  • The usage event carries a plan identifier (plan_key or the resolved caps) distinguishing Starter from Business.
  • The abuse audit (peak rpm/day per customer vs the customer's own cap) is answerable from wm_api_usage alone.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium priority, schedule when capacity allowsarea: APIBackend API, sidecar, keys

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions