Credit top-up
ProA card for buying AI credits. It shows the balance with a 14 day usage chart and how many days are left, three preset packs with bonus badges, a custom amount slider with a live price, auto top-up with a threshold and the saved Visa or Mastercard. When you buy, the new balance rolls in like an odometer.
- Selling prepaid AI credits or tokens with volume bonuses.
- A billing settings section where people also manage auto top-up and their card.
- An out of credits moment where the balance, burn rate and a one click refill belong together.
- Subscription plan changes with seats and billing cycles: use a plan picker or checkout-live.
- Collecting a new card number: use a card form or a hosted payment element, then pass the saved card here.
- Read-only usage reporting: use a usage meter or a chart.
Installation
Pro source and install commands unlock with a Pro plan.
Usage
Drop it into a billing page, an out of credits dialog or a top-up drawer. Pass `balance` controlled, plus `usage` (daily credits, oldest first) and `paymentMethods`. Wire `onPurchase(order, signal)` to charge a saved card: resolve when the credits are added, or throw an Error with a short message to show the error state with Try again. `pricing` ({ unitPrice, currency, bonusTiers }) drives every price and bonus. Price orders on the server with the exported `quoteCredits` and the same pricing object. `onAutoTopupChange` and `onPaymentMethodChange` report settings changes. With no props it runs a simulated purchase for a workspace at 840 credits.
import { CreditTopup } from "@/components/arc/blocks/credit-topup/credit-topup"; export function BillingCredits({ workspace }) { return ( <CreditTopup balance={workspace.credits} usage={workspace.dailyUsage} paymentMethods={workspace.cards} onPurchase={async (order, signal) => { const res = await fetch("/api/credits/purchase", { method: "POST", body: JSON.stringify(order), signal }); if (!res.ok) throw new Error((await res.json()).message ?? "Payment failed. Try again or change card."); }} onAutoTopupChange={settings => fetch("/api/credits/auto-topup", { method: "PUT", body: JSON.stringify(settings) })} /> );}API reference
2 parts. The first is the root.
CreditTopup
A card for buying AI credits: the balance with a 14 day usage chart and days left, preset packs with bonus badges, a custom amount slider with a live price, auto top-up with a threshold, the saved card, and a checkout that rolls the new balance in. With no props it plays a simulated purchase for a workspace at 840 credits.
balancenumber–The current balance in credits, controlled. Any change rolls the numerals. Leave it out and the card keeps its own balance and adds each purchase to it.defaultBalancenumber840The starting balance when `balance` is left out.usageCreditUsageDay[]Oct 14 to Oct 27, 2026 sampleDaily usage, oldest first: { date: ISO date, credits }. The last day is today. Drives the bar chart, its hover readout, and the days left estimate (average of the full days).packsnumber[][1000, 5000, 20000]Preset pack sizes in credits, shown as a radio group. Keep it to three for the layout.pricingCreditPricing$0.01 a credit, +10% from 5,000, +15% from 20,000{ unitPrice, currency, bonusTiers: { min, percent }[] }. Prices, bonus badges, the total and the per 1,000 rate all derive from it.range{ min: number; max: number; step: number }{ min: 500, max: 30000, step: 500 }The custom amount slider's range in credits. A slider value equal to a pack selects that pack.defaultCreditsnumberthe second packThe amount selected on mount.paymentMethodsCreditPaymentMethod[]Visa 4242, Mastercard 5454Saved cards: { id, brand: "visa" | "mastercard", last4, expires: "MM/YYYY" }. Change cycles through them.thresholdsnumber[][500, 1000, 2500]Choices for the auto top-up threshold. The balance reads as running low below the selected one.defaultAutoTopup{ enabled: boolean; threshold: number }{ enabled: false, threshold: 1000 }Auto top-up settings on mount.onAutoTopupChange(settings: { enabled, threshold, credits }) => void–Called when auto top-up is switched or its threshold changes. credits is the selected amount including bonus.onPaymentMethodChange(id: string) => void–Called when another saved card is chosen.onPurchase(order: CreditOrder, signal: AbortSignal) => Promise<void>–Charges the order { credits, bonus, total, price, currency, paymentMethodId }. Resolve once the credits are added; reject with an Error and its message shows with a Try again button. Aborted on unmount. Leave it out to simulate a 1.2s charge.onPurchased(order: CreditOrder, balance: number) => void–Called after a successful purchase with the new balance.classNamestring–Extra class on the card.quoteCredits
The pricing helper the card uses: quoteCredits(credits, pricing) returns { credits, bonus, total, price, currency }. Use it on the server to price the same order.
No props.
- Tab
- Moves through the packs, slider, auto top-up switch, threshold, Change and Buy credits.
- ArrowLeftorArrowRight
- Moves between packs or thresholds and selects as it goes. Home and End jump to the first and last.
- ArrowLeftorArrowRight on the slider
- Steps the custom amount by 500 credits. PageUp and PageDown take larger steps.
- Space
- Switches auto top-up.
- EnterorSpace
- Buys the selected credits or changes the card.
- Packs and thresholds are radio groups with roving focus and labels that read the amount, price and bonus, such as "5,000 credits, $50, 10% bonus".
- Rolling numerals are aria-hidden; a visually hidden copy of each value is read instead, so screen readers never hear partial digits.
- A polite live region announces the charge, the result ("Added 5,500 credits. Nothing was charged.") and errors. Without onPurchase it says the checkout is simulated.
- Balance health is an icon plus a label (Running low, Healthy), never color alone. The usage chart is one image with a spoken summary.
- The Buy button stays focusable while processing (aria-busy) so keyboard focus is not lost.
- After a purchase the balance rolls up odometer style over 0.9s: each digit rolls one way to its target and the lower places take one extra lap, so it reads as counting. New leading places open their width on a bounce-free spring.
- The total and price roll in 0.24s as the slider moves, in tabular numerals so nothing shifts.
- The pack selection ring and threshold pill glide between options on a 0.24s spring with no bounce. Status, health, days left and the saved card crossfade (100ms out, 160ms in, 2px rise).
- The card never changes size between idle, processing, done and error.
- Reduced motion: numbers and highlights jump to their end state; crossfades stay as plain fades.
- Fluid up to 480px. Below about 450px container width the usage chart moves under the balance and stretches full width, and padding drops to 16px.
- Packs stay three across down to 320px; the bonus badge sits on the price line so amounts never truncate.
- Container queries, not viewport queries, so it adapts inside dialogs and sidebars.
- Each digit is one transformed strip driven by a motion value: no re-renders while numbers roll.
- No timers or loops while idle; the only timeout clears the done message.
- Brand marks are inline SVG, about 1 KB.
Notes for AI
Give your coding assistant the Markdown reference instead of screenshots.
- Use for usage-based AI products that sell prepaid credits: a billing page section, an out of credits dialog body, or a top-up drawer.
- Keep pricing on the server authoritative: price orders with the exported quoteCredits and the same pricing object, never trust the client total.
- Pass balance controlled when credits change elsewhere (live usage); the numerals roll on every change, so it doubles as a live balance.
- Wire onPurchase to a saved payment method charge (for example a Stripe PaymentIntent with off_session) and throw an Error with a short human message on decline.
The full library index for assistants is at /llms.txt.