# Cap free-tier users (/docs/recipes/cap-free-tier)



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 [#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:

<Steps>
  <Step>
    **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.
  </Step>

  <Step>
    **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.
  </Step>

  <Step>
    **Resets every** — default monthly. Everyone's counter starts again at the top of the period.
  </Step>
</Steps>

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.

<Callout type="info">
  Want *one shared* $5 across the whole free tier instead of $5 each? That's a different template
  — &#x2A;*Stop free-tier abuse (Sybil attack)**. The difference is the APPLY TO: per user, or one
  pool. See [Filter vs apply-to](/docs/rules/match-vs-groupby).
</Callout>

## The snippet [#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:

```python
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="...")
```

```typescript
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](/docs/concepts/identity).

## Next [#next]

<Cards>
  <Card title="Multi-tenant budgets" href="/docs/recipes/multi-tenant-budgets" description="The same shape, but per customer/tenant." />

  <Card title="Dry-run vs enforce" href="/docs/concepts/dry-run-vs-enforce" description="Watch the cap before it blocks." />

  <Card title="Free-tier protection" href="/solutions#sybil" description="Why a per-user cap alone doesn't stop signup abuse." />
</Cards>
