Skip to content

🪙 feat: Surface Balance Refill State in User Menu#13233

Open
Odrec wants to merge 5 commits into
danny-avila:devfrom
Odrec:feat/balance-refill-indicator
Open

🪙 feat: Surface Balance Refill State in User Menu#13233
Odrec wants to merge 5 commits into
danny-avila:devfrom
Odrec:feat/balance-refill-indicator

Conversation

@Odrec
Copy link
Copy Markdown
Contributor

@Odrec Odrec commented May 21, 2026

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:

  1. 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 maybeAutoRefill helper consumed by both the checkBalance middleware (existing path) and the controller.

  2. Refill state surfaced in the account-settings popover.

    • Right after a refill (within 24h of lastRefill), the popover shows a green (+refillAmount) badge under the balance, with a TooltipAnchor tooltip showing the exact refill timestamp (works on touch as well as hover).
    • Otherwise it shows a muted "Next refill in X days" subtext so the user always knows when their next refill arrives.

Schema change

The lastRefill field on the Balance schema previously defaulted to Date.now, and createSetBalanceConfig separately 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 configured refillAmount, even though they only received startBalance.

This PR removes both the schema default and the seed in createSetBalanceConfig. `lastRefill` is now set only when a real refill happens (via createAutoRefillTransaction). The eligibility check in maybeAutoRefill already treats a missing lastRefill as eligible — but the refill still requires `balance ≤ 0`, so fresh users with positive startBalance are not eagerly refilled. They simply become eligible the moment their balance drops to zero.

Existing users in production are unaffected: their existing lastRefill values are preserved.

Behaviour matrix

State Mongo state Popover shows
Fresh user tokenCredits = startBalance, no lastRefill Balance: 1,000 (no badge, no subtext)
Just refilled tokenCredits ≥ refillAmount, lastRefill < 24h ago Balance: 5,000,000 + green (+5,000,000) with tooltip `Refilled on …`
Eligible + at zero tokenCredits = 0, interval elapsed On next balance fetch → eager refill fires → moves to "Just refilled" state
Not eligible + at zero tokenCredits = 0, interval not elapsed Balance: 0 + "Next refill in 5 days"

Files changed

Backend

  • packages/api/src/balance/refill.ts (new) — shared maybeAutoRefill helper.
  • packages/api/src/middleware/checkBalance.ts — refactor to use the helper; drop lastRefill = Date.now() seed in lazy init.
  • packages/api/src/middleware/balance.ts — drop lastRefill seed in buildUpdateFields.
  • api/server/controllers/Balance.js — call maybeAutoRefill before returning the balance.
  • packages/data-schemas/src/schema/balance.ts — remove Date.now default from lastRefill.
  • packages/data-schemas/src/types/balance.tslastRefill?: Date.

Frontend

  • client/src/components/Nav/AccountSettings.tsx — new BalanceMenuItem rendering badge + subtext + tooltip.
  • client/src/locales/en/translation.json — 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.

Tests

  • New unit tests for maybeAutoRefill (10 cases).
  • New controller tests for the eager-refill path in balanceController (5 cases).
  • Updated checkBalance and createSetBalanceConfig tests to reflect the change to lastRefill seeding 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.

pr-state1-fresh-user pr-state2b-stacked-badge pr-state2c-tooltip pr-state3-zero-not-eligible

Documentation

Docs draft (to be merged after this PR ships): LibreChat-AI/librechat.ai#583.

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.
@lkiesow
Copy link
Copy Markdown
Contributor

lkiesow commented May 21, 2026

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.

  1. Refill state surfaced in the account-settings popover.

    • Right after a refill (within 24h of lastRefill), the popover shows a green (+refillAmount) badge under the balance, with a TooltipAnchor tooltip showing the exact refill timestamp (works on touch as well as hover).
    • Otherwise it shows a muted "Next refill in X days" subtext so the user always knows when their next refill arrives.

Especially the time when the green +refillAmount is shown seems weird:

Right after a refill (within 24h of lastRefill), the popover shows a green (+refillAmount) badge under the balance…

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 +refillAmount whenever a refill is available for me since my available balance is literary remainingBalance + refillAmount.

So I would suggest:

  • Show a green +refillAmount when a refill is available for me. This would be the most important thing since this means that users can easily judge their available balance at any time.
  • Show just the current balance if my balance isn't 0, but no refill is available for me
  • Show a gray “Next refill in X days” if my balance is 0 and no refill is available right now

If that's too complex we could also always show either the green +refillAmount or the gray “Next refill in X days”. Though I would probably prefer the three states described above.

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).
@Odrec
Copy link
Copy Markdown
Contributor Author

Odrec commented May 21, 2026

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 2743c94 which switches to availability semantics, matching your proposed three states:

State Display
tokenCredits > 0 + refill available (eligible or never refilled) Green (+refillAmount) badge — your effective headroom is balance + refillAmount
tokenCredits > 0 + refill not yet available Just the current balance
tokenCredits ≤ 0 + refill not yet available Gray Next refill in X days

The tokenCredits ≤ 0 + eligible state is handled by the eager refill on the balance endpoint — it transitions through that state on read, so users never see "Balance: 0 + refill available" sitting there.

Implementation details: dropped the 24h lastRefill window and the timestamp tooltip; renamed the i18n keys com_nav_balance_just_refilled[_info]com_nav_balance_refill_available[_info] with copy reflecting the new meaning. Re-verified all four states against a live dev instance.

pr-v2-state-b-positive-not-eligible pr-v2-state-c-positive-eligible pr-v2-state-c-tooltip pr-v2-state-d-zero-not-eligible

Odrec pushed a commit to Odrec/librechat.ai that referenced this pull request May 21, 2026
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).
@Odrec Odrec marked this pull request as ready for review May 30, 2026 14:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants