Knowing what an answer costs before and after you send it
October 13, 2026
By Bike4Mind Engineering · also on app.bike4mind.com
A chat assistant that bills by usage has an obvious problem: the number that matters, what this answer cost you, is the one you usually learn about last. Over the past few weeks we shipped a set of changes in Bike4Mind so that the cost of a message is visible before you send it, right after it arrives, and in aggregate on your balance. This post covers what you will see in the app, and then how the numbers are produced in the open-source core.
Before you send: credits per 1M tokens in the model picker
The model picker already marked each model with a rough cost tier (lower, medium, premium). That told you which model was relatively expensive but not what a message would cost. The tooltip on the cost icon now adds the model's rate in credits per 1M input tokens and per 1M output tokens, at the model's base pricing tier. It uses the same unit as the pricing table under Profile > Usage and the models API, so the three places agree to the last digit.
A few edge cases are handled on purpose. A model marked free to run shows 0 credits rather than a tier label. A model with no usable price, and image models, keep the tier-only text, so "unpriced" never reads as "free". The tooltip also closes when you tap elsewhere on a touch screen.
One caveat the code itself states: the figure is the list rate. Prompt caching can make a real turn cheaper than it implies.
After you send: the cost on each answer
Each finished answer carries a small chip with what it cost, for example "15 credits". Very cheap answers can cost a fraction of a credit, and the chip then reads "<1 credit" instead of rounding down to a misleading zero. The chip is on by default for new accounts. If you would rather not see it, there is a toggle in your profile. For existing accounts, where older sign-ups had stored the setting as off, we ran a dry-run-first backfill that turns it on where the value was missing.
Always: a balance in the composer
The composer toolbar now has a labeled balance chip, such as "9,982 credits". It turns amber when you are running low and red when you are out, and clicking it opens the credits modal with packages and transaction history. It shows nothing when credit enforcement is off for the deployment, since nothing is being deducted and a balance would only mislead.
One detail worth knowing: while a turn is streaming, the server holds back a worst-case amount of your balance, then gives back the difference when the turn settles. Shown raw, that looks like the balance dipping and bouncing. The chip holds the pre-turn value while any turn is streaming and switches to the live number once it ends. The checks that decide whether you can send still use the live balance, so a real mid-turn shortfall is not hidden.
New accounts with no notebooks yet also see a short welcome on the empty chat screen: the starting credit balance (or the pending grant, if email verification is still outstanding) and three starter prompts. It is dismissible and returning users never see it.
Before you run out: offers that do not block you
When the balance drops under a low-credit threshold, a notice appears above the message box. It does not block sending, and dismissing it snoozes it for 24 hours in that browser, keyed to the signed-in user so a shared machine does not hide it from the next person. When the balance reaches zero, the notice replaces the message box and send is disabled.
Both states offer the next step. Users without a plan see a subscribe action as the primary choice and a credit pack as the secondary one. Users who already have a plan are offered only the credit pack. Members of an organization cannot buy credits themselves, so they get an "Ask your admin" action that copies a ready-made request message.
Plans and credits you can link to
The credits and plans screens now have URLs. /billing/credits and /billing/plans redirect to the app with a ?billing= parameter, and a layout-level component opens the matching modal on any app page. Signed-out visitors go through the normal login redirect first, which is why the short /billing/<view> form is the one to share. The profile menu opens the same modals through the same URL.
Checkout failures are also visible now. Previously a failed or invalid checkout link quietly sent you to the home screen. Now the checkout page shows an error card with "See plans" and "Contact support", and the subscribe endpoint rejects a plan that is not in the configured list with a 400 and "Unknown plan".
How the numbers are produced
All of the pricing math lives in b4m-core/common/src/pricing.ts, so the client, the API and the server settle on the same numbers.
- Provider cost in USD. After a chat turn,
getTextModelCostinb4m-core/common/src/models.tsprices the tokens from the model's published per-token rates. Where the provider reports usage, that is used. If it does not, the server falls back to its own token estimate. Cache reads and cache writes are priced with their own multipliers unless the model publishes explicit cache rates. - USD to credits. A single constant,
CREDITS_PER_USD_COST, converts provider cost to credits. It is derived from a margin and a per-credit price, both overridable by environment variable in self-hosted deployments, and every purchase route grants credits at the same rate. - Settlement uses unbiased rounding.
usdToCreditsStochasticalways charges the whole part of the cost and charges one extra credit with probability equal to the fractional part. On average you pay exactly the fractional cost, with no round-up penalty on small answers and no free tier hiding below one credit. It draws from the platform CSPRNG so the rounding cannot be predicted and gamed. This is why the same answer can show a slightly different charge from one run to the next, and why a cheap one can show "<1 credit". - The number on the chip is the number charged.
ChatCompletionProcessmakes one settlement draw and reuses it for the answer's recorded cost, the usage event and the ledger deduction, so they cannot disagree. The chip simply readscreditsUsedfrom that record. - Estimates round up instead.
usdToCreditsuses a ceiling and is meant for holds and eligibility checks, where a conservative figure is the point. The model picker's per-1M figure is not that one.getModelCreditRatesinapps/client/app/utils/aiSettingsUtils.tsmultiplies the per-token USD rate by the sameCREDITS_PER_USD_COSTthat settlement charges in expectation, and pre-rounds to four decimals so the tooltip and the pricing table never differ in the last digit.
Because the pricing table is only as good as the model catalog behind it, a model with no price entry would settle at zero. The server logs an alarm when real usage prices out to $0 on a model not marked free to run. A recent fix is an example of why that matters: a native Opus model had no price literal in the direct provider table, and a Bedrock rate had been copied from an earlier model. Both are corrected, and the new price also flows through to the tooltip.
What we left alone
Credit amounts, plan prices and the starting grant are configuration, not constants, so this post does not quote them. The in-app screens read them live. If you run Bike4Mind yourself, the grant and the low-balance threshold are admin settings, and with credit enforcement off none of the balance or offer UI appears at all.