usertrust
API Reference

Pricing

Usertoken costs per model across all four tiers, estimation functions, and the fallback rate.

All costs in the usertrust SDK are denominated in usertokens (UT).

1 UT = $0.0001 (one basis point of a cent).

A budget of 100_000 UT equals $10.00. A budget of 1_000_000 UT equals $100.00.

Pricing Table

The SDK includes a 23-model pricing table in ledger/pricing.ts, current as of PRICING_TABLE_VERSION = "2026-08-09" (see PRICING_TABLE_VERSION). Rates are in usertokens per 1,000 LLM tokens, across four tiers: input (fresh prompt tokens), output, cache-read (cache-hit prompt tokens), and cache-write (cache-creation prompt tokens). A means the provider publishes no rate for that tier — it is not free; see Cache tiers and the fallback invariant below.

Anthropic

ModelInputOutputCache ReadCache Write
claude-sonnet-4-630150337.5
claude-haiku-4-51050112.5
claude-opus-4-650250562.5
claude-fable-510050010125

Anthropic publishes cache-read at 0.1x base input and 5-minute cache-write at 1.25x. The 1-hour write premium (2x) collapses to the 5-minute rate — see Documented approximations — operators with 1h-heavy workloads should override via customRates.

OpenAI

ModelInputOutputCache ReadCache Write
gpt-4o2510012.5
gpt-4o-mini1.560.75
gpt-5.4251502.5
o320805
o4-mini11442.75
gpt-5.6-sol50300562.5

The cached-input discount varies within the OpenAI family — gpt-5.4 reads at 0.1x, o3/ o4-mini at 0.25x — so there is no single family-level multiplier. OpenAI publishes no separate cache-write rate for the models above; writes bill at the standard input price, which is exactly what the fallback invariant reproduces by leaving cacheWritePer1k unset. gpt-5.6-sol is the exception and carries an explicit rate: from gpt-5.6 onward OpenAI bills cache writes at 1.25x the uncached input price, so leaving it unset would misprice them.

Moonshot AI

ModelInputOutputCache ReadCache Write
kimi-k3301503

Moonshot's price list has a cache-hit and a cache-miss input column and no cache-creation column at all, so cacheWritePer1k is unset and writes price at the input rate through the fallback invariant. Kimi speaks the OpenAI wire protocol, so detectClientKind reports openai for a Kimi client — that is the client SHAPE, and it does not change which rates apply.

Google Gemini

ModelInputOutputCache ReadCache Write
gemini-2.5-flash3250.3
gemini-2.5-pro12.51001.25
gemini-3.1-pro201202

Context-cache reads are 0.1x base input. Cache creation bills as ordinary input plus an hourly storage charge this per-token model doesn't carry, so cacheWritePer1k is omitted rather than guessed — see Documented approximations.

Other Providers

No cache-tier pricing is currently published for these models; both cache columns fall back to the input rate.

ModelProviderInputOutput
mistral-largeMistral515
deepseek-chatDeepSeek2.84.2
deepseek-reasonerDeepSeek2.84.2
grok-3xAI30150
llama-4-maverickMeta (Bedrock)2.49.7
command-aCohere25100
sonar-proPerplexity30150
qwen-72bAlibaba2.93.9
nova-proAmazon832

Rates reflect the SDK's built-in table. Check packages/core/src/ledger/pricing.ts for the canonical source.

Cache tiers and the fallback invariant

ModelRates carries two optional fields beyond inputPer1k/outputPer1k:

interface ModelRates {
  inputPer1k: number;
  outputPer1k: number;
  cacheReadPer1k?: number;  // absent ⇒ prices at inputPer1k, never 0
  cacheWritePer1k?: number; // absent ⇒ prices at inputPer1k, never 0
}

Absent means "no published rate," not "free." costFromRates resolves an absent (or non-finite/negative) cache rate to the model's inputPer1k — this is the one and only rate resolution site in the SDK. A model that omits cacheWritePer1k still bills cache-write tokens, at the same rate as fresh input.

This is deliberately fail-safe: understating a bill is the dangerous direction (a budget that looks fuller than the real invoice), so an unmodeled cache tier defaults to the conservative price, never to zero. Never treat an absent field as a discount.

Fallback Rate

Models not in the pricing table use sonnet-class pricing as a conservative fallback:

DirectionRate (per 1K tokens)
Input30 UT
Output150 UT

This ensures unknown models are never free. The fallback is intentionally high to avoid under-billing, and is deliberately two-tier: an unrecognized model isn't known to be Anthropic-shaped, so attaching a cache discount here would silently under-bill it. Its cache tokens route through the same fallback invariant above and price at 30 UT/1k.

Model Matching

getModelRates() first attempts an exact match against the pricing table. If no exact match is found, it tries prefix matching (longest key first) to handle versioned model names. If neither matches, the fallback rate is used.

getModelRates("claude-fable-5")           // exact match → { 100, 500 }
getModelRates("claude-fable-5-20260301")  // prefix match → { 100, 500 }
getModelRates("unknown-model")              // fallback → { 30, 150 }

Functions

getModelRates()

function getModelRates(model: string): ModelRates

Look up the four-tier rates for a model. Falls back to prefix matching, then to the fallback rate.

costFromRates()

function costFromRates(
  rates: ModelRates,
  inputTokens: number,
  outputTokens: number,
  cacheReadTokens?: number,  // default 0
  cacheWriteTokens?: number, // default 0
): number

Compute usertoken cost across all four tiers from an explicit ModelRates. The four counts are expected to be disjoint — cached tokens must already be separated out of inputTokens before this is called, or they are double-counted. Absent cache rates resolve to inputPer1k (see Cache tiers and the fallback invariant); the result is floored at 1 usertoken, same as estimateCost().

// 1,000 fresh input + 500 output + 2,000 cache-read tokens on claude-fable-5
costFromRates(getModelRates("claude-fable-5"), 1000, 500, 2000, 0)
// → Math.max(1, Math.ceil((1000 * 100)/1000 + (500 * 500)/1000 + (2000 * 10)/1000))
// → Math.max(1, Math.ceil(100 + 250 + 20))
// → 370 UT

Each tier is computed as (tokens × ratePer1k) / 1000multiply before dividing. This is the same order the receipt schema publishes as ceil(sum(counts × rates / 1000)), and it is load-bearing rather than stylistic: in IEEE-754 the divide-first form (tokens / 1000) × ratePer1k can land a ulp above an integer (560 cache-write tokens at 12.5/1k give 7.000000000000001), which Math.ceil then rounds to a whole extra usertoken. Reconciling against a receipt with the published formula reproduces the charged cost exactly, on every input.

estimateCost()

function estimateCost(model: string, inputTokens: number, outputTokens: number): number

Two-tier convenience wrapper around costFromRates() for callers that don't have a cache split — cacheReadTokens/cacheWriteTokens default to 0. The result is always at least 1 (floor to prevent zero-amount transfers in TigerBeetle).

// 1,000 input tokens + 500 output tokens on claude-fable-5
estimateCost("claude-fable-5", 1000, 500)
// → Math.max(1, Math.ceil((1000 * 100)/1000 + (500 * 500)/1000))
// → Math.max(1, Math.ceil(100 + 250))
// → 350 UT

estimateInputTokens()

function estimateInputTokens(messages: unknown[]): number

Estimate the input token count from a messages array before the LLM call. Uses a heuristic of approximately 4 characters per token with a 1.5x safety margin so the PENDING hold exceeds actual cost in the majority of cases.

This function handles:

  • String content and multi-part content blocks
  • Nested arrays (tool result payloads)
  • Per-message overhead (role, structure)
  • Tool-call overhead

Returns at least 1. This estimate never models cache state — see Documented approximations.

ModelRates

interface ModelRates {
  inputPer1k: number;        // usertokens per 1,000 fresh input tokens
  outputPer1k: number;       // usertokens per 1,000 output tokens
  cacheReadPer1k?: number;   // usertokens per 1,000 cache-hit tokens; absent ⇒ inputPer1k
  cacheWritePer1k?: number;  // usertokens per 1,000 cache-creation tokens; absent ⇒ inputPer1k
}

PRICING_TABLE_VERSION

const PRICING_TABLE_VERSION: string  // e.g. "2026-08-09"

Date-stamped version of the built-in PRICING_TABLE, bumped whenever any entry's rates change. Recorded on every settled receipt (receipt.pricing.tableVersion) alongside the resolved rates (receipt.pricing.appliedRates) so a metered cost can be reproduced exactly against the table version that priced it, independent of the SDK version currently installed.

Hold sizing and the cache-write premium

The PENDING hold reserves the input leg at max(inputPer1k, effectiveCacheWriteRate(rates)), not inputPer1k alone. A cache write can price above plain input — Anthropic's 5-minute write is 1.25x, the 1-hour write 2x — so sizing the hold off inputPer1k alone understates the reserve for any call that turns out to write to cache. This applies at both hold-sizing sites (the governed authorize() path and the headless authorize() path).

Consequence: holds on cache-writing workloads run roughly 25% fatter than an input-only hold would size them. Warm (cache-hit-heavy) calls settle well below the hold and release the difference — this is conservative, not wasted budget. A caller supplying an exact estimatedInputTokens for a call that turns out cache-cold can still see the settle capped at the hold (settlement_shortfall, see AGENTS.md's Money invariants) if the actual write premium exceeds even the widened reserve.

Documented approximations

Reconciliation against a provider's own invoice is approximate, not exact — a receipt's cost exactly reproduces usertrust's own configured metered cost (counts × appliedRates, per-call ceil, 1-UT floor), but not necessarily the provider's bill line-for-line, because:

Per-TTL write premium collapsed (1h = 2× billed as 1.25×; customRates override for 1h-heavy workloads); long-context, service-tier, regional, modality, and cache-STORAGE charges (Gemini hourly storage, prompt-size-dependent rates; GPT-5.4 long-context uplifts) not modeled — fixed per-model rates by design; per-call ceil + 1-UT floor differs from provider-side aggregation. Estimates never model cache state.

Cost Estimation Flow

During the two-phase spend lifecycle, costs are estimated twice:

  1. Before the call (PENDING): estimateInputTokens() produces a conservative input estimate. Combined with max_tokens for the output estimate and the cache-write premium guard above, the SDK calculates the hold amount. The 1.5x safety margin (plus the write-premium guard) means the hold usually exceeds actual cost.

  2. After the call (POST): Actual token counts from the provider response (or accumulated from stream chunks), split into all four tiers, produce the real cost via costFromRates(). The difference between the hold and the actual cost is released back to the available budget.