Every AI agent product eventually ships the same screen: a bar that says how much of today's allowance you have used, and a smaller card underneath that says when it refills. It looks like a billing detail. It is actually the contract that decides when your agent stops working, what it tells the user while it is stopped, and whether the work it promised yesterday still happens today.
Token metering gets all the design attention, because it is what the vendor pays for. Quotas get ignored, because they are what the user feels. This post is about the second one: how to model a daily usage limit, a reset, and a spendable "reset ticket" so that the system stays predictable at the edges — the moment the limit is hit, the moment it resets, and the moment nobody is watching.
I am writing this from the user side. I run an agent every day and I have watched it hit, skirt and recover from its daily cap. The model below is the one I would want any product in this category to implement, and the checks at the end are the ones I actually run before I trust a quota system.
A quota is not a balance; it is a recurring window
A balance is monotonic and boring: you spend, it goes down, you top it up. A daily quota is a cycle. That single difference produces almost every support ticket in the category.
Three properties have to be explicit in the model, not implied by the UI:
The window has a wall-clock boundary in a stated timezone. "Resets daily" is not a specification. A reset at 04:00 in one fixed timezone is a specification; a reset "24 hours after your first message" is a different product with different expectations; a reset at local midnight is a third. Whichever you choose, one value must be authoritative and everything else — the card, the notification, the API — must render from it. If the user's phone is in another timezone, do not let the device clock decide what has already reset. Show a countdown computed from the server's resets_at, and label the fixed timezone of the schedule somewhere adjacent.
The window refills, it does not accumulate. A daily cap that stacks is just a monthly balance with extra steps. The moment you quietly accumulate unused allowance, users will plan around it, and the first time it does not carry over you have a trust problem that costs more than the compute you saved. State the rule in the same place the number lives.
There is no silent top-up path. If usage can also be bought, the cap stops being a design constraint and becomes a funnel, and users will read every future limit as an upsell. Pick one: either the daily cap is the whole economics of the product, or there is an explicit paid top-up and the UI says so on the same screen as the bar. What must not happen is a cap that behaves like a hard stop while a payment sheet sits one tap away with no explanation.
Model it as a small state machine
The implementation that survives contact with users is embarrassingly small — one record per user per window, plus an append-only ticket ledger. In TypeScript, roughly this:
type WindowId = string; // e.g. "2026-11-15@04:00+09:00" type Scope = "chat" | "call"; // quotas can be per-surface interface QuotaWindow { windowId: WindowId; scope: Scope; cap: number; // in abstract units, not tokens used: number; resetsAt: string; // ISO string, server clock, fixed TZ schedule } interface TicketGrant { id: string; // idempotency key from the grant source kind: "full" | "half"; // 0% of used, or used * 0.5 scope: Scope; grantedAt: string; expiresAt: null; // deliberately: tickets do not expire } interface TicketSpend { id: string; grantId: string; windowId: WindowId; usedBefore: number; usedAfter: number; at: string; } Four things in that shape are load-bearing.
Units are abstract and per-product, and the unit is disclosed. Compute what a unit means internally — one answer, one started call, one executed task — and make sure the interface says which of those costs a unit. A user who cannot predict whether a two-line follow-up in an old conversation costs the same as a new task will assume the worst, and they will be right often enough to change their behaviour.
Tickets are a ledger, not a counter. A number in a column cannot answer "where did this ticket come from, and when did it go". A ledger can: grant, spend, reverse, with an idempotency key on every grant so a retried webhook or a re-delivered email does not mint a duplicate. When a user says "I got a ticket and it vanished", the ledger is the difference between a five-second answer and an argument.
resetsAt is stored, not computed at render time. Compute the next boundary once per window, persist it, and let every surface read it. Recomputing from "now" in a different timezone is how a product ends up resetting twice, or resetting for users who are one hour east of the server.
Windows are keyed, so double-spend is impossible. Every spend transaction includes windowId. A retry after a network timeout either lands in the same window (and is deduplicated by idempotency key) or is visibly a new window, never a mystery second charge inside the same cycle.
The edges are the product
The happy path — spend, hit the cap, wait, reset — needs no design. All the design is in four edges.
The moment the cap is hit. The interface must do three things in the same breath: say that new work cannot start, say exactly when it can, and name the way out. An input box that simply goes dead is the worst version; a modal that sells is the second worst. The good version is a single card in place that carries the countdown, the ticket balance by kind, and a shortcut that spends one ticket without leaving the conversation. Whatever the user was about to do should still be in the box when they come back.
The moment a ticket is spent for nothing. If usage is already low enough that a ticket has nothing to restore, do not burn it. Keep it, and say so on screen: "usage was low, nothing to reset, ticket kept." Products that silently consume a ticket at 6% usage teach users that tickets are a trap, and they stop using the feature that was supposed to smooth the cap.
The moment a scheduled task is skipped. This is the one nobody designs and everybody hits. A daily cap plus a scheduled agent is a promise the system cannot keep on the day the cap fills early. Skipping is acceptable; skipping silently is not. A skipped run needs a record with a reason and a visible next attempt, otherwise the user's mental model becomes "it works sometimes", which is worse than "it does not work".
The moment the cap resets. A reset is an event, not an absence of state. Two lines of text — "reset complete, 100% available" and, on the day it did not fire, the fallback — save an enormous amount of doubt. If the reset job can fail, the product needs to distinguish "clamped to cap because it reset" from "still empty because the job did not run", and it needs to notice the second case itself.
function applyTicket(win: QuotaWindow, grant: TicketGrant, now: string): TicketSpend | null { if (grant.scope !== win.scope) return null; // never cross scopes const saved = grant.kind === "full" ? win.used : Math.floor(win.used * 0.5); if (saved <= 0) return null; // keep the ticket, report it return { id: newId(), grantId: grant.id, windowId: win.windowId, usedBefore: win.used, usedAfter: win.used - saved, at: now, }; } Note what the function does not do: it does not decide policy. Whether the cheap ticket is spent before the expensive one is a product setting, and it belongs in one place with a label the user can read, because "half first" and "full first" give measurably different days to the same person. Automatic spending is the same kind of switch: one toggle, one documented order, and off by default if you want people to trust the cap.
The anti-patterns worth naming
Recharging more than once a day while calling it daily. If the schedule is not what the label says, the label is wrong, not the user. Anything that refills opportunistically — a session restart, a plan change, a support gesture — must be visible as a distinct event, never as the daily reset quietly happening early.
Per-message billing that nobody announced. If a long conversation costs more than a short one, say it. If the product charges per answer and not per message, say that too. Users reconstruct the model from observation, and they reconstruct it wrong.
A cap with no recovery path at all. A hard stop with nothing to spend and nothing to buy means the product's value depends on the user's luck with timing. A ticket, an invite, or a scheduled refill gives the cap a floor, and a cap with a floor is a design; a cap without one is just an outage on a schedule.
A comparison between products that only lists prices. Price is the least comparable number in this category, because the unit is different in every product: tokens, runs, messages, minutes, or a daily allowance with recovery tickets. The honest comparison is what a unit buys, what happens at the limit, and what the product gives back when it stops.
Five checks I run before trusting a quota system
- Find the boundary. Use the product across the reset time on purpose, once. If you cannot tell from the interface whether the boundary passed, the countdown is decorative.
- Read the unit. Ask what one unit buys, and whether a follow-up message in an old conversation costs the same as a new task. If the answer is not in the product, it is not in the product.
- Let a ticket go unused. Get a ticket, do not spend it, use the product for a week. An unused ticket should still be there, and the interface should say it does not expire.
- Try to spend a ticket when usage is low. It should refuse to waste it, and it should tell you it refused.
- Schedule something across the limit. Set a task for a time when the cap is likely to be full, and see whether the skip is recorded. This is the cheapest test of the whole system, and it is the one most products fail.
Where this leaves a daily-cap product
A quota with a stated reset, a visible unit and a spendable recovery ticket is a legible product: the user knows what they have, what they will get back, and what happens in between. That legibility is worth more than the extra headroom a fuzzy cap pretends to give, and it survives the two situations where trust is won or lost — the day the cap fills at 9 a.m., and the night a scheduled task quietly did not run.
I use one product that works this way: a Korean phone-side agent app with a daily allowance that refills once at 04:00 KST, two kinds of recovery ticket (a full reset and a half reset), tickets that do not expire, and an option to spend them automatically at the limit. I wrote up how that specific system behaves, including what the interface says at each step, in the page linked below; the design lessons above are general and do not depend on it.
Disclosure: I am a user of Pickle, the Korean phone-side AI agent app described in the last section. I am not its vendor. The links below are mine: a guide I wrote to its daily limit and reset tickets, a comparison page on where conversational, browser and phone-side assistants stop, and an invite link that gives both sides reset tickets if a sign-up completes through it. Nothing in this post depends on them — read the design notes, skip the links, or take only the five checks.
- Daily usage limits and reset tickets, explained from the app's own notices
- Where each kind of AI assistant stops — conversational, browser and phone-side, compared
- Invite link: https://pickle.com/i/RDKXK77G9ZRT — limited slots per link, first come; the app's own notices govern any benefit.