nvoken
DocumentationCredits & spending limits

Operate

Credits & spending limits

Fund a customer's spending capacity, decide whether nvoken enforces it, and relate tenant credits to per-Turn limits.

Two different things cap what an agent can spend, and confusing them is the usual source of a surprise.

  • Credits are a customer's spending capacity. They live on a tenant, they persist across every turn that tenant runs, and nvoken can refuse work when they run out.
  • Limits are one turn's ceiling. limits.max_estimated_cost_usd and the iteration, output-token, and deadline caps belong to a single Turn and reset with the next one.

Credits answer "has this customer paid for this?" Limits answer "how far can this one turn go?" You will usually want both.

Tenant credits are spending counters

The tenant credits on this page do not process payments or issue invoices. Allocating them does not charge a card or transfer money; it tells nvoken what your own billing system collected or authorized for one customer.

If you use nvoken-funded model access, the credits you purchase for your nvoken account are separate. Account credits fund model use. Tenant credits divide spending capacity among the customers inside your App.

That division is deliberate: your billing system knows your prices, your currencies, your refund policy, and your customers. nvoken knows, exactly and transactionally, what a turn cost while it was running — which is the one thing a billing system on the outside cannot see in time to stop anything.

POST /v1/credits/allocations
{
  "tenant_key": "acme",
  "amount": { "amount": "50.000000", "currency": "USD" },
  "reference": "stripe_pi_3Q…",
  "idempotency_key": "topup-2026-08-15-acme-1"
}

idempotency_key is required and the retry is safe: an identical replay returns the original allocation and the current account rather than adding the money twice. Call this from your Stripe webhook handler and stop worrying about double-delivery.

reference is yours — correlation text so you can trace a top-up back to the payment that caused it. nvoken stores no payment-provider state and attaches no meaning to the string.

Allocations are append-only. There is no negative allocation and no edit; the history of what was added stays intact.

The four numbers on an account

{
  "tenant_key": "acme",
  "allocated": { "amount": "50.000000", "currency": "USD" },
  "used":      { "amount": "12.480000", "currency": "USD" },
  "held":      { "amount": "0.310000",  "currency": "USD" },
  "available": { "amount": "37.210000", "currency": "USD" },
  "budget_hold_turns": 0
}

allocated is everything ever added. used is settled spend. held is money reserved for provider calls in flight — nvoken takes a hold before each attempt and settles it against what the call actually cost, which is why concurrent turns cannot overspend a balance between them. available is max(0, allocated − used − held).

Amounts are exact decimal strings with six fractional digits, never floats.

Credits are enforcement facts, not an invoice. Deleting Conversations does not restore used — the spend happened, and the accounting evidence outlives the transcript.

Deciding whether nvoken enforces them

credit_policy on the App, set from the dashboard:

ValueWhat happens when a turn starts
off (default)Starts the turn without consulting the account. Credits are still counted.
requiredRefuses the turn — and each later provider call — unless the tenant has available credits.

The default changed. credit_policy used to be on; it now defaults to off. The reasoning is about who is at risk: when the provider account paying for the call is yours, nvoken is not the party exposed, and your own billing system already meters your customer. Turning enforcement on is you asking nvoken to hold the line per tenant.

One case ignores the setting. When nvoken's own provider key pays for the call, credits are always required, whatever credit_policy says.

What running out looks like

Before the turn starts, a tenant with no available credits is refused with insufficient_credits, and the error carries the tenant key and the exact amounts. Nothing runs and nothing is charged.

Mid-turn is more interesting, because the Turn already exists. A Turn that cannot fund its next provider attempt carries a credit_block naming the account that ran dry.

What happens next is your choice when starting the turn:

  • on_budget_exhausted: "stop" (the default) ends the turn as incomplete.
  • on_budget_exhausted: "hold" keeps it in budget_hold with stop_reason: "insufficient_credits". No worker or model call stays active, its deadlines are on hold, and it is waiting for money.

A held turn resumes on its own once the account is funded. Allocate credits and it picks up where it stopped; the work already done is not repeated and not thrown away. budget_hold_turns on the account tells you how many turns are waiting.

This is the part worth designing for. A turn that waits a day for a top-up, and then finishes, is a different product experience from one that fails and asks the customer to start over.

Holding on a per-turn limit instead

on_budget_exhausted: "hold" covers the turn's own limits too — iteration, output-token, and per-turn cost. A turn held on max_estimated_cost is resumed differently: raise that limit and continue it explicitly.

POST /v1/turns/{turn_id}/resume
{ "limits": { "max_estimated_cost_usd": 4.0 } }

Send only the limit that ran out, raised above both its old value and what the turn has already spent. A deadline cannot create a budget hold, so a turn that ran out of time never arrives here.

Seeing which customers you have

GET /v1/tenants

Each row is one stored tenant key, with its credit position and the last time it ran a turn. The row makes the key referenceable; nvoken holds no customer profile, configuration, or entitlement.

A key that has only ever been refused is not here, because it was never stored. Those requests live in the turn-start log. Comparing the two lists shows which customer keys have requested work but have never run it successfully.

DELETE /v1/tenants/{tenant_id} exists to undo a mistake — a key funded with a typo in it. It is deliberately narrow: a tenant with any Conversation, Turn, retained usage fact, memory, or provider key is refused with tenant_in_use and the counts that show why it is still in use. Usage facts have to outlive the transcripts they describe, so deleting a tenant that has run work would destroy accounting evidence.

Credits and per-Turn limits stay separate

Tenant credits answer how much one customer may spend across all work. limits.max_estimated_cost_usd and the other execution limits constrain only one Turn. A budget_exceeded error therefore describes one Turn's limit, while insufficient_credits describes the tenant account.

One thing to know before turning enforcement on: nvoken cannot enforce credits for a model it has no price for. Such a call fails with cost_estimate_unavailable, which does not mean you overspent — it means the ceiling could not be checked at all.