🪙 feat: Surface Balance Refill State in User Menu#13233
Conversation
Eliminate user confusion when balance hits zero by surfacing refill state directly in the account-settings popover and refilling eagerly on balance-read (not only on message-send). Backend - Extract refill logic into `maybeAutoRefill` helper, used by both `checkBalance` middleware (existing path) and the new eager-refill call from `balanceController`. - Drop the `lastRefill = Date.now` schema default and stop seeding `lastRefill` on user creation / config sync. A real refill is now the only thing that sets the field, so it reliably signals "recently refilled" to the frontend. Frontend - `AccountSettings` popover now shows a green `(+refillAmount)` badge under the balance for 24h after a refill, with a tooltip showing the exact timestamp (`Refilled on …`) via `TooltipAnchor` so it works on touch. - Otherwise shows a muted `Next refill in X days` subtext so users always know when their next refill arrives. i18n: add `com_nav_balance_just_refilled`, `com_nav_balance_just_refilled_info`, `com_nav_balance_next_refill_in`; update `com_nav_balance_next_refill_info` to reflect the new behaviour.
|
It's great for this to be tackled. The refill is really confusing for users right now. However, maybe I'm misunderstanding something, but this doesn't sound correct.
Especially the time when the green
Assuming there is a refill every 7 days, why would it show this right after a refill? If a refill just happened, no new refill should be available and this should likely show the “next refill on …” instead. Generally, the UI should show the green So I would suggest:
If that's too complex we could also always show either the green |
Per @lkiesow's feedback on the original PR, the green (+refillAmount) badge previously meant "your refill landed (within 24h)" — a backward- looking recent-event signal. With eager refill on balance read, that state is mostly transient, and the badge doesn't tell users what they need to know: how much spending headroom they actually have right now. Switch the semantic to "a refill is available for you" instead: - Green (+refillAmount) when refill is eligible (interval has elapsed, or user has no lastRefill) AND current balance > 0. Effective headroom = current_balance + refillAmount, which the user can now see at a glance. - Just the current balance when balance > 0 and no refill is available yet. - Gray "Next refill in X days" only when balance is at zero AND not yet eligible — i.e. when the user is actually stuck waiting. Implementation drops the 24h "just refilled" window and validLastRefill gating on the badge. Rename i18n keys com_nav_balance_just_refilled[_info] → com_nav_balance_refill_available[_info] with copy that matches the new meaning. Refs: danny-avila#13233 (review feedback).
|
Thanks @lkiesow, your framing makes a lot more sense than what I had. You're right — with the eager refill on read, the "just refilled" badge was a transient backward-looking signal that didn't help the user understand their actual spending headroom. Pushed
The Implementation details: dropped the 24h
|
Match the refreshed UX in danny-avila/LibreChat#13233 review round: the green (+refillAmount) badge now signals refill availability (useful spending headroom) rather than a 24h "just refilled" window, and the "Next refill in X days" subtext only appears when the user is actually waiting (balance at zero and not yet eligible).




Summary
When a user's balance hits zero, today's behaviour is confusing: the next refill only happens on the first message send after the interval elapses, and the UI gives no signal about when the next refill is available. We've had multiple support questions about "why is my balance still 0 — am I out forever?" when users were actually just one message away from being topped up.
This PR addresses both halves of that confusion:
Eager auto-refill on balance read. The balance controller now triggers the existing auto-refill flow when the user is eligible and at zero, so opening the app refills the user without needing a message send first. Refill logic is extracted into a shared
maybeAutoRefillhelper consumed by both thecheckBalancemiddleware (existing path) and the controller.Refill state surfaced in the account-settings popover.
lastRefill), the popover shows a green(+refillAmount)badge under the balance, with aTooltipAnchortooltip showing the exact refill timestamp (works on touch as well as hover).Schema change
The
lastRefillfield on the Balance schema previously defaulted toDate.now, andcreateSetBalanceConfigseparately seeded it on the first request for users with auto-refill enabled. With the new badge logic, this meant freshly-created users would incorrectly appear to have "just been refilled" with the configuredrefillAmount, even though they only receivedstartBalance.This PR removes both the schema default and the seed in
createSetBalanceConfig. `lastRefill` is now set only when a real refill happens (viacreateAutoRefillTransaction). The eligibility check inmaybeAutoRefillalready treats a missinglastRefillas eligible — but the refill still requires `balance ≤ 0`, so fresh users with positivestartBalanceare not eagerly refilled. They simply become eligible the moment their balance drops to zero.Existing users in production are unaffected: their existing
lastRefillvalues are preserved.Behaviour matrix
tokenCredits = startBalance, nolastRefillBalance: 1,000(no badge, no subtext)tokenCredits ≥ refillAmount,lastRefill < 24h agoBalance: 5,000,000+ green(+5,000,000)with tooltip `Refilled on …`tokenCredits = 0, interval elapsedtokenCredits = 0, interval not elapsedBalance: 0+ "Next refill in 5 days"Files changed
Backend
packages/api/src/balance/refill.ts(new) — sharedmaybeAutoRefillhelper.packages/api/src/middleware/checkBalance.ts— refactor to use the helper; droplastRefill = Date.now()seed in lazy init.packages/api/src/middleware/balance.ts— droplastRefillseed inbuildUpdateFields.api/server/controllers/Balance.js— callmaybeAutoRefillbefore returning the balance.packages/data-schemas/src/schema/balance.ts— removeDate.nowdefault fromlastRefill.packages/data-schemas/src/types/balance.ts—lastRefill?: Date.Frontend
client/src/components/Nav/AccountSettings.tsx— newBalanceMenuItemrendering badge + subtext + tooltip.client/src/locales/en/translation.json— addcom_nav_balance_just_refilled,com_nav_balance_just_refilled_info,com_nav_balance_next_refill_in; updatecom_nav_balance_next_refill_infoto reflect the new behaviour.Tests
maybeAutoRefill(10 cases).balanceController(5 cases).checkBalanceandcreateSetBalanceConfigtests to reflect the change tolastRefillseeding semantics.All 50 affected backend tests pass; lint and TypeScript checks clean on changed files.
Screenshots
Manually verified end-to-end against a real Mongo + dev server.
Documentation
Docs draft (to be merged after this PR ships): LibreChat-AI/librechat.ai#583.