usertrust
API Reference

Errors

Seven domain error classes with structured hints and documentation URLs.

usertrust defines seven domain-specific error classes. All extend Error and include structured hint and docsUrl properties for actionable diagnostics.

Catching Errors

import {
  InsufficientBalanceError,
  PolicyDeniedError,
  LedgerUnavailableError,
} from "usertrust";

try {
  const { response, receipt } = await client.messages.create({
    model: "claude-fable-5",
    max_tokens: 1024,
    messages: [{ role: "user", content: "Hello" }],
  });
} catch (err) {
  if (err instanceof InsufficientBalanceError) {
    console.log(`Need ${err.required} UT, have ${err.available} UT`);
    console.log(err.hint);
  }
  if (err instanceof PolicyDeniedError) {
    console.log(`Blocked: ${err.reason}`);
  }
  if (err instanceof LedgerUnavailableError) {
    console.log(`TigerBeetle down: ${err.cause_message}`);
  }
}

Every error class follows the same pattern: a descriptive message, a hint with a suggested fix, and a docsUrl pointing to detailed documentation.


InsufficientBalanceError

Thrown when the requested LLM call would exceed the remaining budget.

class InsufficientBalanceError extends Error {
  readonly userId: string;
  readonly required: number;    // usertokens needed
  readonly available: number;   // usertokens remaining
  readonly hint: string;
  readonly docsUrl: string;
  readonly auditEventHash?: string;   // the `ledger_rejected` event's hash
  readonly auditDegraded?: boolean;   // the append failed and was dead-lettered
}

When it fires: During the PENDING hold phase, before the LLM call is forwarded to the provider. No tokens are consumed.

Hint: "Increase the budget in trust() options or add funds via the ledger."


PolicyDeniedError

Thrown when a policy rule or PII detector blocks the request.

class PolicyDeniedError extends Error {
  readonly reason: string;
  readonly hint: string;
  readonly docsUrl: string;
  readonly auditEventHash?: string;   // the `policy_denied` event's hash
  readonly auditDegraded?: boolean;   // the append failed and was dead-lettered
}

When it fires: During policy evaluation, before the LLM call. The reason field describes which rule or PII type triggered the denial.

Hint: 'Check your policy rules in .usertrust/policies/ or use { pii: "warn" } to downgrade PII enforcement.'


The denial correlation handle

Both denial errors above carry auditEventHash — the hash of the chain event the governor wrote when it refused the call. It is the join between the exception your code logged and the record an auditor reads: policy_denied for a governor decision (a policy rule, a budget-family rule, PII, injection, or an unpriceable model), ledger_rejected for the ledger's own atomic refusal of the hold.

try {
  await client.messages.create({ /* ... */ });
} catch (err) {
  if (err instanceof PolicyDeniedError) {
    // Quote this in your incident ticket; `usertrust verify` can find it.
    console.log(`denied, chain event ${err.auditEventHash ?? "(none)"}`);
  }
}

The two fields answer different questions, and you need both:

auditEventHashauditDegradedWhat happened
setabsentThe denial is on the chain, at that hash.
settrueThe event is on the chain at that hash, but the write reported failure — the log was fsync'd and the .meta sidecar was not. The handle is usable; run usertrust verify on the vault.
absenttrueThe append was attempted, nothing durable landed, and the payload was dead-lettered to .usertrust/dlq/. There is no chain record.
absentabsentNo append was attempted — an error built by hand, or thrown by a version before denial events existed.

A failed denial-append never changes the error you receive. audit.failClosed exists to stop an unaudited spend from settling; a denial already refused the call and moved no money, so escalating here would only replace an actionable, typed denial with an AuditDegradedError and hide why the call was refused.

Indirect callers do not receive the handle yet. usertrust-server and the ACS adapter convert the error to a wire response and do not forward auditEventHash today; the field is on the in-process error object only.


AccountNotFoundError

Thrown when the TigerBeetle account for the given user does not exist.

class AccountNotFoundError extends Error {
  readonly userId: string;
  readonly hint: string;
  readonly docsUrl: string;
}

When it fires: During ledger initialization or the first spend attempt.

Hint: 'Run "npx usertrust init" to create accounts, or verify the userId matches your config.'


IdempotencyConflictError

Thrown when a transfer with the same ID has already been submitted to TigerBeetle.

class IdempotencyConflictError extends Error {
  readonly key: string;
  readonly hint: string;
  readonly docsUrl: string;
}

When it fires: During the PENDING hold phase, if the generated transferId collides with an existing transfer.

Hint: "This transfer was already submitted. Use a unique transferId for retries."


LedgerUnavailableError

Thrown when TigerBeetle is unreachable. The LLM call is not forwarded to the provider.

class LedgerUnavailableError extends Error {
  readonly cause_message: string;
  readonly hint: string;
  readonly docsUrl: string;
}

When it fires: Before any LLM call, during the connection check or PENDING hold attempt. This is failure mode 15.4 -- no tokens are at risk because no hold was placed.

Hint: 'Start TigerBeetle with "npx usertrust tb start" or use { dryRun: true } to skip the ledger.'

That hint is the error's own text, quoted verbatim — and usertrust tb start is a stub that prints a manual command rather than starting anything. Follow the CLI page's usertrust tb section for the command that actually starts a node, or pass { dryRun: true } to skip the ledger entirely.

When TigerBeetle is unreachable, usertrust refuses to forward the request. This is by design -- without a ledger, budget enforcement is impossible.


AuditDegradedError

Thrown (or flagged) when the audit chain write fails after the LLM call and ledger settlement both succeed.

class AuditDegradedError extends Error {
  readonly cause_message: string;
  readonly hint: string;
  readonly docsUrl: string;
}

When it fires: After the LLM call succeeds and the ledger POST settles. This is failure mode 15.3. The response is still returned to the caller with auditDegraded: true on the receipt. The failed event is written to the dead-letter queue.

Hint: "Check disk space and permissions on the .usertrust/audit/ directory."

usertrust never fails the user on audit degradation. If the LLM call succeeded and the ledger settled, the response is always returned.


VaultNotInitializedError

Thrown when the .usertrust/ vault directory does not exist.

class VaultNotInitializedError extends Error {
  readonly path: string;
  readonly hint: string;
  readonly docsUrl: string;
}

When it fires: During trust() initialization, before any LLM calls.

Hint: 'Run "npx usertrust init" to create the vault directory.'


Error Summary

ErrorFailure ModeLLM Call Forwarded?Tokens At Risk?
InsufficientBalanceErrorPre-callNoNo
PolicyDeniedErrorPre-callNoNo
AccountNotFoundErrorPre-callNoNo
IdempotencyConflictErrorPre-callNoNo
LedgerUnavailableError15.4NoNo
AuditDegradedError15.3Yes (succeeded)No (settled)
VaultNotInitializedErrorPre-callNoNo

All pre-call errors are thrown before any interaction with the LLM provider. No tokens are consumed and no budget is affected.