---
title: 'End-user credits: charge your users for agent spend on camelRun'
sidebarTitle: 'End-user credits'
description: 'Give each of your users a balance and daily free allowances, enforced on every model call by camelRun, with webhooks when someone runs low or runs out.'
---
[View as Markdown](https://camelai.com/docs/camelrun/end-user-credits.md) · [Docs index for coding agents](https://run.camelai.com/llms.txt)

End-user credits let you charge your own users for what their agents spend.
camelRun keeps a balance for each person your app serves, gives each a daily
free allowance per part of your product, enforces both on every model call, and
tells you when someone runs low or runs out. You keep the money side: your
payment provider, your pricing page, your emails. After a payment, you tell
camelRun "add $10 to this person".

Everything a person causes counts against one balance: every agent that acts
for them, their stateless runs, transcriptions, images, web searches and the
rest. You still pay camelRun for raw usage as before (see
[pricing](/docs/camelrun/pricing)). End-user balances are your ledger of your users,
and camelRun never moves money for them.

## The model

* **End user**: the `subject` of an agent (set when you upsert it, see
  [multi-user apps](/docs/camelrun/multi-user)) or of a call (`POST /v1/runs`,
  `/v1/transcriptions`, `/v1/images`). An agent's sub-agents act for its subject
  too. Work with no subject is not metered: you pay for it, as today.
* **Bucket**: the part of your product that spends, such as `builder` or
  `bot:42`. An agent spends from its `bucket`; a stateless run or call names its
  own. A prompt sent with your API key (`POST /v1/agents/{id}/prompt`) may name
  another; an agent's own token can't. A run that names none (a schedule's, a
  sub-agent's notice, a channel's message, a voice session's `ask_agent`, or one
  you send without) spends from the agent's bucket, else the bucket your latest
  run of the agent named, else `default`.
* **Daily allowance**: free spend per bucket per UTC day, spent first. Set it by
  bucket name, by prefix (`"bot:*"`) or for every bucket (`"*"`). A bucket takes
  its own name, else the longest prefix it starts with, else `*`. You can
  override allowances per person. Allowances count **raw cost**: what the call
  costs you.
* **Balance**: paid credit, spent once the allowance is used. Grants (after a
  payment) add to it; refunds and adjustments take from it.
* **Markup**: added to the raw cost of paid spend before it is charged
  (`markupBps`: 2500 is +25%). The ledger records both.
* **Daily limit**: the most a person's paid spend may be per UTC day, for
  everyone or per person.
* **Bucket limits**: the most one bucket may charge a person's balance per UTC
  day, keyed like allowances, for everyone or per person. A bot at
  `"bot:*": 2_000_000` spends its allowance, then at most $2 of the balance a
  day, however much the person holds. At `0` it spends its allowance only. To
  list the only buckets that may spend paid credit, give `"*": 0` and a limit
  for each: `{ "*": 0, builder: 20_000_000, "bot:*": 2_000_000 }`. No other
  bucket, `default` included, then ever charges anyone's balance.

All amounts are integer **micro-USD**: 1 USD = 1,000,000.

A call costs a person what it cost you. On camelRun's keys that is what you are
charged for it: the provider's price, plus the cost of funding OpenRouter credit
on OpenRouter calls (about 5.5%). On your own keys it is the provider's reported
cost, else the catalog's price for its tokens (cache reads and writes included),
as `usage.recorded` shows. The markup is on top. A model with no price (one of
your [own models](/docs/camelrun/custom-models) declared without `pricing`) is never
free to them: it is charged at the catalog's price for the model, else a
mid-range price ($3 in, $15 out per million tokens). Declare its pricing to
charge your own.

Changing an allowance or a limit never resets what was spent today. Spend is
kept per day and bucket, and the allowance is only the cap: raise a bucket from
$40 to $60 mid-day and the person has $20 more, not $60.

## Turn it on

```ts TypeScript
import { Agents } from "@camelai/run";
const agents = new Agents();

await agents.endUsers.configure({
  enabled: true,
  allowances: { builder: 50_000_000, "bot:*": 1_000_000 }, // $50 a day for the builder, $1 a day per bot
  markupBps: 2_500,                                          // paid spend at cost +25%
  dailyLimit: 20_000_000,                                    // at most $20 of paid spend a day
  bucketLimits: { "bot:*": 2_000_000 },                      // and at most $2 of it per bot
  lowBalance: 2_000_000,                                     // end_user.balance.low below $2
  allowanceWarningPercent: 80,                               // end_user.allowance.low at 80% of a bucket's allowance
});
```

```python Python
await agents.end_users.configure(enabled=True, allowances={"builder": 50_000_000, "bot:*": 1_000_000},
                                  markup_bps=2_500, daily_limit=20_000_000, bucket_limits={"bot:*": 2_000_000},
                                  low_balance=2_000_000)
```

The REST call is `PUT /v1/end-user-settings`. Fields left out keep their values,
but `allowances` and `bucketLimits`, when sent, replace the whole map. To change
some keys and keep the rest, use `PATCH /v1/end-user-settings`
(`endUsers.amend`, Python `end_users.amend`): only what you send changes, maps
merge key by key, and `null` removes a key. A person's own overrides merge the
same way (`endUsers.update`).

Several writers can change the settings at once (each server of yours as it
starts, an admin tool, a script). Each write changes only the fields it sends,
and a `PATCH` merges its keys into the map as it stands, so writes to different
fields or keys all keep their changes. Of two writes to the same field or key,
the later wins. Give each writer its own fields and keys and have it `PATCH`
only those.

## Example: pay as you go

A person buys credit with Stripe; their builder agent and their bots spend it.

```ts
// 1. Their agents act for them, each from its bucket.
const builder = await agents.upsert(`builder-${user.id}`, { subject: user.id, bucket: "builder", definition: BUILDER });
const helper = await agents.upsert(`bot-${botId}`, { subject: user.id, bucket: `bot:${botId}`, definition: BOT });

// 2. One-off model calls in their bot's code spend from the same bucket.
const answer = await agents.run({ input: prompt, subject: user.id, bucket: `bot:${botId}` });

// 3. A payment lands: credit it, once per payment (a retried webhook posts nothing twice).
app.post("/stripe/webhook", async (req, res) => {
  const event = stripe.webhooks.constructEvent(req.body, req.headers["stripe-signature"], secret);
  if (event.type === "payment_intent.succeeded") {
    const payment = event.data.object;
    await agents.endUsers.grant(payment.metadata.userId, { amount: payment.amount * 10_000, key: payment.id, reference: `Stripe ${payment.id}` });
  }
  res.sendStatus(200);
});

// 4. Their credit page: where they stand, what they bought, and what their credit went to each day.
const person = await agents.endUsers.get(user.id, { buckets: ["builder"] });
// person.balance, person.available, person.today.buckets[0].allowanceLeft
const purchases = await agents.endUsers.ledger(user.id, { kind: ["grant", "refund", "adjustment"], limit: 20 });
const { days, next } = await agents.endUsers.days(user.id, { limit: 7 });
// days[0]: { day: "…", freeRaw, paidCharged, writtenOff, granted, refunded, adjusted, entries, buckets: [{ bucket: "bot:42", freeRaw, paidCharged, writtenOff }] }
```

In Python:

```python
await agents.end_users.grant(user_id, amount=amount_cents * 10_000, key=payment_id, reference=f"Stripe {payment_id}")
person = await agents.end_users.get(user_id, buckets=["builder"])
purchases = await agents.end_users.ledger(user_id, kind=["grant", "refund", "adjustment"], limit=20)
days = await agents.end_users.days(user_id, limit=7)
```

A credit page reads no usage rows: the ledger's `kind` filter leaves them out,
and `days` gives each UTC day's totals by bucket in one request.

For a refund you make in Stripe, `endUsers.refund(id, { amount, key })` takes the
credit back. It can take at most the balance that calls in flight don't hold;
past that it is a 409 `INSUFFICIENT_BALANCE`, with `refundable`. For anything
else, `endUsers.adjust(id, { amount, key })` takes a signed amount. Grants,
refunds and adjustments are idempotent by `key`: the same key and entry again
returns the first, and the same key for a different entry is a 409.

To charge a saved card when someone runs low, see
[auto top-up](/docs/camelrun/end-user-auto-top-up).

## When a person runs out

camelRun holds credit for each model call before making it: its input and up to
2,048 tokens of output, from the bucket's allowance first, then the balance. A
longer reply holds more as it streams. Holds are taken one at a time per person,
so calls in parallel (sub-agents, several of their agents at once) never each
see the whole balance.

When nothing more can be held:

* **A new run** is refused with `402` and code `END_USER_CREDIT`, with
  `endUser`, `bucket` and `reason` in the body. The SDKs throw `AgentError` with
  the same fields (`reason`, `bucket`, `endUser`; Python `end_user`).
* **A queued run** refused as it starts fails with code `end_user_credit`,
  `reason`, `bucket` and `endUser`.
* **A run already going** stops where the credit ends, even mid-reply. What
  streamed is kept, and the run fails with code `end_user_credit`,
  `stopped: "spend_limit"`, `limit: "end_user"`, a `reason`, the `bucket` and
  the `endUser`. It is never retried.
* The `run.failed` webhook carries `code`, `limit`, `reason`, `bucket` and
  `endUser` too, and `voice.session.ended` its `endUserReason` and `bucket`.
* Transcriptions, speech and images are refused before they are made, with the
  same 402.

`reason` is one of:

| `reason`       | Meaning                                                                                                                                               |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `balance`      | No allowance left in the bucket today, and no balance                                                                                                 |
| `daily_limit`  | The person's paid spend reached the daily limit                                                                                                       |
| `bucket_limit` | The bucket's paid spend reached its own limit. The person may still have a balance for other buckets; a bucket limited to 0 spends only its allowance |

`endUserCredit` (Python `end_user_credit`) reads these from a thrown error, a
failed run or its error, so one place handles every path:

```ts
import { endUserCredit } from "@camelai/run";

try {
  const run = await agent.run(text, { throwOnError: false });
  const credit = endUserCredit(run);
  if (credit) notify(user, credit.reason);
} catch (error) {
  const credit = endUserCredit(error); // a 402 at submit
  if (!credit) throw error;
  notify(user, credit.reason);
}

function notify(user: string, reason?: string) {
  // "daily_limit" | "bucket_limit" | "balance"; a reason added later: a credit refusal of no particular kind.
}
```

Branch on `code` and `reason`, never on the message: its words are for people
and may change.

### Costs past a call's hold are written off

Usage never takes a balance below zero, and never takes paid spend past the
daily limit or a bucket's limit. A call can still cost more than it held:

* output a provider bills without streaming it (hidden reasoning tokens);
* what streams while the last credit check is answered (milliseconds of output);
* the gap between the input estimate and the real input (a provider cache that
  expired, a provider's own price at the time);
* a call cut off by a failure: its hold lapses after 10 minutes, and if the
  provider reports its cost later, it is settled then.

When it settles, the person is charged at most what is left for it: the balance,
the daily limit and the bucket's limit, less what their other calls in flight
hold. The rest is **written off**. You bear it (your camelRun usage is the
call's full cost, as always), and it is never charged to the person later, even
after they buy credit. Each usage entry shows it as `writtenOff`,
`GET /v1/end-users/{id}` totals it per bucket for today
(`today.buckets[].writtenOff`), and `GET /v1/end-users/{id}/days` per bucket and
day, all in raw micro-USD. The limit that capped the charge fires
`end_user.credit.exhausted` as a refused call would, and the person's next call
is refused.

A cost corrected down later takes back the write-off first, then refunds what
the balance paid, then the allowance. A cost corrected up is capped the same
way, by the limits of the day the call began and the balance as it is then.

## Webhooks

Subscribe an endpoint (`POST /v1/webhooks`, see [webhooks](/docs/camelrun/webhooks)) to:

| Type                                                | When                                                                                                                                                                                                                                                                                                                                                  |
| --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `end_user.balance.low`                              | A balance went below the person's threshold: their own `lowBalance` (`PATCH /v1/end-users/{id}`), else yours. See [when balance events fire](#when-balance-events-fire)                                                                                                                                                                               |
| `end_user.credit.exhausted`                         | `reason: "allowance"`: a bucket's allowance for the day is used up (the balance may still pay; once per person, bucket and day). `"balance"`: the balance reached zero, or a call was refused for it. `"daily_limit"`: the daily limit was reached (once a day). `"bucket_limit"`: a bucket's own limit was reached (once per person, bucket and day) |
| `end_user.credit.granted`, `.refunded`, `.adjusted` | You posted a grant, refund or adjustment, once per entry (a replayed `key` sends nothing): `{endUser, entryId, kind, amount, key, reference, metadata, balance, previousBalance}`, with `amount` signed as in the ledger. For receipts, and for a second service or a mirror of balances                                                              |
| `end_user.allowance.low`                            | A person's spend in a bucket passed `allowanceWarningPercent` (1 to 100, off by default) of its daily allowance, once per person, bucket and day: `{endUser, bucket, day, allowance, allowanceSpent, percent}`                                                                                                                                        |

They are signed and retried like every webhook, and written in the same
transaction as the change that caused them.

### When balance events fire

`end_user.balance.low` and `end_user.credit.exhausted` with `reason: "balance"`
each carry a `cause`: what moved the balance.

```jsonc
"cause": {
  "kind": "usage",       // usage | usage_correction | grant | refund | adjustment | refusal | threshold_changed
  "entryId": 1234,       // the ledger entry; none for refusal and threshold_changed
  "entryKey": "dp_123",  // your key, for grant, refund and adjustment
  "requestId": "…",      // usage: the run's request, when there is one
  "bucket": "bot:42"
}
```

* `usage_correction` is a call's cost posted again, corrected: a provider's
  final figure, which can come long after the call.
* `refusal` is a call refused with no balance left. It has no ledger entry.
* A person's entry ids grow, so `entryId` puts their events in order, which
  delivery may not.

The rules:

* **`balance.low` fires on a debit** (usage or a negative adjustment) that takes
  the balance from at or above the threshold to below it. Staying below fires
  nothing more until one of the next rules applies.
* **`credit.exhausted` (`balance`) fires on a debit** that takes the balance
  from above zero to zero or below, and on a call refused with no balance left
  (`cause.kind: "refusal"`), once until the balance is back above zero or
  re-armed.
* **A grant or positive adjustment that leaves the balance short re-arms the
  event**: the next debit fires it again. With a $10 threshold, a $5 top-up that
  leaves someone at $6 makes their next debit fire `balance.low` again. A grant
  that leaves someone at or below zero makes their next debit or refused call
  fire `credit.exhausted` again.
* **Any move that leaves the balance past the line** (at or above the
  threshold, above zero) resets the event, including a cost corrected down: the
  next crossing fires.
* **Setting a threshold** fires `balance.low` once, with
  `cause.kind: "threshold_changed"`, for each person under it who hasn't been
  told since they were last above it (or was re-armed since). A person's own
  threshold applies to them alone. Your account-wide `lowBalance` applies to
  everyone without their own who has ever been credited. Setting the same value
  again fires nothing.
* **Refunds are silent.** A refund fires neither event (no one should be topped
  up because you gave money back), and counts as told for each line the balance
  is short of, so neither fires until a grant or a positive adjustment.
* **Someone never credited** (who only spends free allowances) isn't re-armed by
  anything, and your account-wide threshold never tells them their balance is
  low.

## The ledger

Every movement of a person's money is an entry, newest first:

| `kind`                          | `amount`                                                              | Also                                                                                                                                                                                                                                                               |
| ------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `grant`, `refund`, `adjustment` | What you posted                                                       | Your `key`, `reference` and `metadata`                                                                                                                                                                                                                             |
| `usage`                         | What the balance paid (negative); 0 when the allowance covered it all | `raw` (the call's cost), `allowance` (what the allowance covered), `writtenOff` (the cost past what was left for it, not charged), `bucket`, and `metadata` naming the call (`response`, `compaction`, `transcription`, `image`, `web_search`…), its agent and run |

A call whose cost is corrected later has another usage entry for the
difference. `balanceAfter` is the balance after each entry.

Narrow it with `kind` (any of `grant`, `refund`, `adjustment`, `usage`;
comma-separated in the API), `from` and `to` (a time range on `createdAt`, `to`
exclusive: epoch milliseconds, a UTC day or an ISO 8601 time), `bucket`, and
`moved=true` (only entries that moved the balance, leaving out usage the
allowance paid in full). They combine with each other and with `before` and
`limit`.

### Per day

`GET /v1/end-users/{id}/days` (`endUsers.days(id, { from, to, bucket, limit })`)
gives a person's UTC days, newest first, `limit` days a page (default 31, at most
100; pass `next` as `to` for the next page). `from` and `to` are UTC days
(`YYYY-MM-DD`, `to` exclusive), at most 366 apart, by default the 366 days up to
today. Days with no spend and no entries are left out. `bucket` keeps that
bucket's spend alone, without entries (entries belong to no bucket).

A call counts on the UTC day it began (its usage entry's `metadata.day`), so near
midnight that can differ from its entry's `createdAt`. An entry you post counts
on the day you posted it.

### Units

Amounts are integer micro-USD. Some are raw cost (what a call cost you); others
are charged, in the balance's units (raw cost with your markup).

| Field                                                                                   | Unit    |
| --------------------------------------------------------------------------------------- | ------- |
| `allowances`, `allowance`, `allowanceSpent`, `allowanceLeft`                            | Raw     |
| A usage entry's `raw`, `allowance` and `writtenOff`; a bucket's or a day's `writtenOff` | Raw     |
| `balance`, `held`, `available`, `dailyLimit`, `bucketLimits`, `paidLimit`, `lowBalance` | Charged |
| `paidSpent`, any entry's `amount` and `balanceAfter`                                    | Charged |
| A day's `freeRaw` (what allowances paid)                                                | Raw     |
| A day's `paidCharged` (what the balance paid), `granted`, `refunded`, `adjusted`        | Charged |

A bucket's `held` today adds the allowance's part of holds (raw) to the
balance's (charged). Read `available` and `allowanceLeft` for what is left.

## API

| Call                                                           | SDK (TypeScript / Python)                                                                                                                                                                                    |
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `GET`, `PUT`, `PATCH /v1/end-user-settings`                    | `endUsers.settings()`, `endUsers.configure({...})`, `endUsers.amend({...})` / `end_users.settings()`, `end_users.configure(...)`, `end_users.amend(...)`                                                     |
| `GET /v1/end-users`                                            | `endUsers.list({ after, limit, belowLowBalance })`: `belowLowBalance` lists only those under their threshold                                                                                                 |
| `GET /v1/end-users/{id}?buckets=`                              | `endUsers.get(id, { buckets })`                                                                                                                                                                              |
| `PATCH /v1/end-users/{id}`                                     | `endUsers.update(id, { allowances, bucketLimits, dailyLimit, lowBalance })`: a person's own allowances and bucket limits (merged; `null` removes one), daily limit and low-balance threshold (`null`: yours) |
| `GET /v1/end-users/{id}/ledger?kind=&from=&to=&bucket=&moved=` | `endUsers.ledger(id, { before, limit, kind, from, to, bucket, moved })` / `end_users.ledger(id, before=, limit=, kind=, from_=, to=, bucket=, moved=)`                                                       |
| `GET /v1/end-users/{id}/days?from=&to=&bucket=&limit=`         | `endUsers.days(id, { from, to, bucket, limit })` / `end_users.days(id, from_=, to=, bucket=, limit=)`                                                                                                        |
| `POST /v1/end-users/{id}/entries`                              | `endUsers.grant`, `refund`, `adjust`                                                                                                                                                                         |

`bucket` is taken by agent upserts and configuration, prompts sent with your API
key, `POST /v1/runs`, `/v1/transcriptions` and `/v1/images`.
