Assistant Billing

React Components

npm install @assistant-billing/react @assistant-billing/sdk

Headless React primitives for building customer-facing billing UIs. All components are composable, unstyled, and support asChild for Radix-style composition.

Setup

Wrap your app with the billing client:

"use client";
 
import { BillingClient } from "@assistant-billing/sdk";
import { GatewayClient, AuiSSR, AuiSSRClient } from "@assistant-billing/react";
import { AuiProvider, useAui } from "@assistant-ui/store";
 
const client = new BillingClient({
  getToken: () => fetchSessionToken(),
  baseURL: "https://api.assistant-billing.dev/v1/platform",
});
 
function BillingProvider({ children }) {
  const aui = useAui({
    billingApi: GatewayClient({ client }),
  });
 
  return <AuiProvider value={aui}>{children}</AuiProvider>;
}
 
// Server component (SSR)
export default function BillingLayout({ children }) {
  return (
    <AuiSSR>
      <AuiSSRClient>
        <BillingProvider>{children}</BillingProvider>
      </AuiSSRClient>
    </AuiSSR>
  );
}

GatewayClient connects all child primitives to your BillingClient instance and auto-registers sub-clients (wallet, keys, analytics, models) as needed.

Data Access

Use useAuiSuspense to read data directly from any scope. This is the recommended way to display data — no leaf components needed.

import { useAuiSuspense } from "@assistant-billing/react";
 
function WalletBalance() {
  const balance = useAuiSuspense((aui) => aui.wallet().getBalance());
  return <span>${Number(balance.amount).toFixed(2)}</span>;
}
 
function AnalyticsOverview() {
  const overview = useAuiSuspense((aui) =>
    aui.analytics().getOverview({ from: "2026-03-01", to: "2026-03-14" })
  );
  return <span>{overview.total_requests} requests</span>;
}
 
function CacheStats() {
  const stats = useAuiSuspense((aui) =>
    aui.analytics().getCacheStats({ from: "2026-03-01", to: "2026-03-14" })
  );
  return <span>{(stats.hit_rate * 100).toFixed(1)}% hit rate</span>;
}

Selectors should return the client's cached promise directly. For reads across multiple pages, use the client's paged method instead of wrapping reads in an async selector. This keeps the promise stable when React retries a suspended mount.

Available Scopes

ScopeMethods
aui.wallet()getBalance(), getTransactions(), getTransactionPages(), createCheckout()
aui.keys()getApiKeys(), createApiKey(), revokeApiKey(), getTempKeys(), createTempKey(), revokeTempKey()
aui.analytics()getOverview(), getTimeseries(), getTimeseriesByModel(), getSummary(), getCosts(), getRequests(), getRequestPages(), getRequestDetail(), getCacheStats()
aui.models()getModels(), getProviders(), setKey(), removeKey(), testKey()

Wallet

WalletPrimitive.AddBalanceForm

Form for adding funds via Stripe checkout.

import { WalletPrimitive } from "@assistant-billing/react";
 
<WalletPrimitive.AddBalanceForm.Root
  defaultAmount={25}
  successUrl="https://myapp.com/billing?checkout=success"
  cancelUrl="https://myapp.com/billing"
>
  <WalletPrimitive.AddBalanceForm.AmountInput />
  <WalletPrimitive.AddBalanceForm.Submit>
    Add Funds
  </WalletPrimitive.AddBalanceForm.Submit>
</WalletPrimitive.AddBalanceForm.Root>
Sub-componentElementDescription
Root<form>Manages amount state, handles checkout on submit. Props: defaultAmount, successUrl, cancelUrl, onCheckout.
AmountInput<input type="number">Controlled number input bound to the form amount.
Submit<button type="submit">Submits the form.

If onCheckout is provided, it receives the checkout URL instead of auto-redirecting. Otherwise, window.location.href is used.

WalletPrimitive.TransactionList

Render transaction history with a render prop.

<WalletPrimitive.TransactionList empty={<p>No transactions yet</p>}>
  {(tx) => (
    <div key={tx.id}>
      <span>{tx.description}</span>
      <span>{tx.amount_usd}</span>
      <span>{tx.type}</span>
      <span>{new Date(tx.created_at).toLocaleDateString()}</span>
    </div>
  )}
</WalletPrimitive.TransactionList>

The render prop receives the full Transaction object. Available fields: id, type, amount_usd, balance_after, description, reference_type, reference_id, created_at, metadata.

Keys

API keys and temporary keys are unified under KeysPrimitive.

List Keys

import { KeysPrimitive } from "@assistant-billing/react";
 
{/* API Keys */}
<KeysPrimitive.ApiKeyList empty={<p>No keys</p>}>
  {(key) => (
    <div key={key.id}>
      <span>{key.name}</span>
      <span>...{key.suffix}</span>
      <span>{key.lastAccessedAt ?? "Never"}</span>
      <KeysPrimitive.ApiKeyRevoke>Revoke</KeysPrimitive.ApiKeyRevoke>
    </div>
  )}
</KeysPrimitive.ApiKeyList>
 
{/* Temp Keys */}
<KeysPrimitive.TempKeyList empty={<p>No temp keys</p>}>
  {(key) => (
    <div key={key.id}>
      <span>{key.name}</span>
      <span>{key.expires_at}</span>
      <KeysPrimitive.TempKeyRevoke>Revoke</KeysPrimitive.TempKeyRevoke>
    </div>
  )}
</KeysPrimitive.TempKeyList>

The render prop gives you the full ApiKey or TempKey object — render whatever fields you need directly.

Create API Key

<KeysPrimitive.ApiKeyCreate.Root name="my-key">
  <KeysPrimitive.ApiKeyCreate.Trigger>Create</KeysPrimitive.ApiKeyCreate.Trigger>
  <KeysPrimitive.ApiKeyCreate.Key />
  <KeysPrimitive.ApiKeyCreate.Copy>Copy</KeysPrimitive.ApiKeyCreate.Copy>
  <KeysPrimitive.ApiKeyCreate.Dismiss>Done</KeysPrimitive.ApiKeyCreate.Dismiss>
</KeysPrimitive.ApiKeyCreate.Root>
Sub-componentElementDescription
RootProviderManages create state. Accepts name prop.
Trigger<button>Creates the key on click
KeytextRenders the full key string (shown once only)
Copy<button>Copies the key to clipboard. Sets data-copied="true".
Dismiss<button>Resets the state

Use KeysPrimitive.useKeyCreateContext() inside the Root to access state (key, isCopied, create, copyKey, dismiss).

Create Temp Key

<KeysPrimitive.TempKeyCreate.Root
  options={{
    name: "Trial Key",
    ttl: 3600,
    permissions: { models: ["gpt-4o-mini"] },
    monthly_budget_usd: 5,
  }}
>
  <KeysPrimitive.TempKeyCreate.Trigger>Create Trial Key</KeysPrimitive.TempKeyCreate.Trigger>
  <KeysPrimitive.TempKeyCreate.Key />
  <KeysPrimitive.TempKeyCreate.Copy>Copy</KeysPrimitive.TempKeyCreate.Copy>
  <KeysPrimitive.TempKeyCreate.Dismiss>Done</KeysPrimitive.TempKeyCreate.Dismiss>
</KeysPrimitive.TempKeyCreate.Root>

Same sub-components as ApiKeyCreate. The Root accepts options: CreateTempKeyOptions for TTL, permissions, and budget.

Models

Model catalog and provider key management are unified under ModelsPrimitive.

Model Catalog

import { ModelsPrimitive } from "@assistant-billing/react";
 
<ModelsPrimitive.CatalogList empty={<p>No models available</p>}>
  {(model) => (
    <div key={model.id}>
      <span>{model.id}</span>
      <span>{model.owned_by}</span>
    </div>
  )}
</ModelsPrimitive.CatalogList>

Provider Keys (BYOK)

<ModelsPrimitive.ProviderList>
  {(provider) => (
    <div key={provider.id}>
      <span>{provider.name}</span>
      <ModelsPrimitive.ProviderKeyStatus />
 
      {/* Set credentials form */}
      <ModelsPrimitive.ProviderSetKey.Root>
        <ModelsPrimitive.ProviderSetKey.Fields>
          {({ name }) => (
            <ModelsPrimitive.ProviderSetKey.FieldInput
              fieldName={name}
              type="password"
              placeholder={name}
            />
          )}
        </ModelsPrimitive.ProviderSetKey.Fields>
        <ModelsPrimitive.ProviderSetKey.Submit>Save</ModelsPrimitive.ProviderSetKey.Submit>
        <ModelsPrimitive.ProviderSetKey.Error />
      </ModelsPrimitive.ProviderSetKey.Root>
 
      {/* Test & remove */}
      <ModelsPrimitive.ProviderTestKey>Test</ModelsPrimitive.ProviderTestKey>
      <ModelsPrimitive.ProviderRemoveKey>Remove</ModelsPrimitive.ProviderRemoveKey>
    </div>
  )}
</ModelsPrimitive.ProviderList>

ProviderSetKey Sub-components

Sub-componentElementDescription
Root<form>Handles credential submission. Accepts onError callback.
Fieldsrender propIterates over provider env vars. Passes { name } to children.
FieldInput<input>Input for a specific env var. Props: fieldName.
Submit<button type="submit">Submits credentials. Disabled while pending.
Error<p>Renders error message. Accepts render prop: {(error) => <span>{error}</span>}.

Action Buttons

ComponentElementDescription
ProviderTestKey<button>Tests the saved credentials. Disabled if no key is set.
ProviderRemoveKey<button>Removes saved credentials.
ProviderKeyStatus<span>Sets data-has-key="true" if credentials are configured.

Analytics

AnalyticsPrimitive.TimeseriesList

Render daily analytics data with a render prop.

import { AnalyticsPrimitive } from "@assistant-billing/react";
 
<AnalyticsPrimitive.TimeseriesList
  from="2026-03-01T00:00:00Z"
  to="2026-03-31T23:59:59Z"
>
  {(day) => (
    <div key={day.date}>
      <span>{day.date}</span>
      <span>{day.request_count} requests</span>
      <span>${day.total_cost}</span>
      <span>{day.avg_latency}ms</span>
      <span>{day.error_count} errors</span>
    </div>
  )}
</AnalyticsPrimitive.TimeseriesList>

The render prop receives the full AnalyticsTimeseries object. Defaults to last 30 days if from/to are omitted.

For overview stats, cache stats, and other analytics data, use useAuiSuspense directly (see Data Access).

Component Reference

PrimitiveSub-components
WalletPrimitiveAddBalanceForm (Root, AmountInput, Submit), TransactionList
KeysPrimitiveApiKeyList, ApiKeyCreate (Root, Trigger, Key, Copy, Dismiss), ApiKeyRevoke, TempKeyList, TempKeyCreate (Root, Trigger, Key, Copy, Dismiss), TempKeyRevoke, useKeyCreateContext
ModelsPrimitiveCatalogList, ProviderList, ProviderSetKey (Root, Fields, FieldInput, Submit, Error), ProviderRemoveKey, ProviderTestKey, ProviderKeyStatus
AnalyticsPrimitiveTimeseriesList

Data Attributes

Components expose state via data-* attributes for CSS styling:

AttributeComponentValues
data-copiedApiKeyCreate.Copy, TempKeyCreate.Copy"true" after copy
data-has-keyModelsPrimitive.ProviderKeyStatus"true" if credentials are set
data-testingModelsPrimitive.ProviderTestKey"true" while testing
data-test-resultModelsPrimitive.ProviderTestKey"success" or "error"
data-test-errorModelsPrimitive.ProviderTestKeyError message string
data-pendingProviderSetKey.Submit"true" while submitting

All interactive components support the asChild prop for Radix-style composition with your own elements.