TokenPolice
Docs
Recipes

Cap free-tier users

Give every free-plan user their own monthly spend cap — a rule plus the identity snippet it matches on.

Give every user on your free plan their own monthly cap, so one heavy free user can't run up your bill. This is a per-user budget filtered to the free tier.

The rule

On the Firewall page (Firewall & Budgets), stay on the Cost & access tab and click New rule from template. Pick Cap spend for free users and fill in three fields:

Your free plan name — the exact plan string your app stamps on a call, e.g. free. The rule only watches calls whose paid_plan matches it.

Limit per free user ($) — default 5. Each user gets their own copy of this allowance; a free user is paused only once they personally pass it.

Resets every — default monthly. Everyone's counter starts again at the top of the period.

Under When to turn it on, leave it on Dry-run to watch the projection first, then switch the rule to Enforce when the numbers look right.

Want one shared $5 across the whole free tier instead of $5 each? That's a different template — Stop free-tier abuse (Sybil attack). The difference is the APPLY TO: per user, or one pool. See Filter vs apply-to.

The snippet

The rule matches on paid_plan and pools by user_id, so your app has to attach both. The simplest way is @tp.workflow — it reads them straight from your entrypoint's arguments:

import token_police as tp

@tp.workflow(name="assistant")
def handle(user_id: str, paid_plan: str, query: str):
    return client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": query}],
    )

# paid_plan comes from your own user record
handle(user_id="u_123", paid_plan="free", query="...")
import { workflow } from "token-police";

const handle = workflow({ name: "assistant" }, (input: {
  userId: string; paidPlan: string; query: string;
}) => client.chat.completions.create({
  model: "gpt-4o-mini",
  messages: [{ role: "user", content: input.query }],
}));

handle({ userId: "u_123", paidPlan: "free", query: "..." });

Attach paid_plan on every call, and spell it exactly as the rule does: a call with no plan counts as "free", and matching is case- and whitespace-sensitive, so an unstamped or differently-spelled plan lands in the wrong bucket. Details in Identity.

Next