from install to first receipt
From an empty project to a receipt you can verify, in under 30 seconds.
install it
One dependency beside the provider SDK you already use. usertrust itself needs no account, no key, no service in the path.
create the vault
Writes .usertrust/ — config, policy rules, an empty chain.
wrap the client you already have
Prompts and control flow untouched. The signature gains what it cost.
import Anthropic from "@anthropic-ai/sdk"; import { trust } from "usertrust"; const client = await trust(new Anthropic(), { budget: 50_000, dryRun: true, // $5.00 · ledger deferred }); const { response, receipt } = await client.messages.create({ model: "claude-fable-5", max_tokens: 1024, messages: [{ role: "user", content: "Analyze this contract" }], }); await client.destroy(); // required
{
transferId: "tx_msm19z93_e603a1a2",
cost: 104, budgetRemaining: 49896, settled: true,
auditHash: "08c22f6127aa76d3…ccabf030f8622",
chainPath: ".usertrust/audit",
model: "claude-fable-5", provider: "anthropic",
usage: {
inputTokens: 317, outputTokens: 131,
cacheReadTokens: 87, cacheWriteTokens: 43
},
pricing: { appliedRates: {
inputPer1k: 100, outputPer1k: 500,
cacheReadPer1k: 10, cacheWritePer1k: 125
}, tableVersion: "2026-08-09" }
}
- TransactiontransferId
- tx_msm19z93_e603a1a2This call’s own reference, the way a line on a bank statement has one. Nothing else can carry it.
- What it costcost
- 104 usertokens · $0.0104Just over a penny. Not an estimate and not a share of a monthly bill — what this one call took.
- What is leftbudgetRemaining
- 49,896 of 50,000Read after the cost settled, so it is an observation rather than a promise about what happens next.
- Tamper sealauditHash
- 08c22f61…f030f8622The hash of this call’s llm_call event in the chain — model, tokens, cost, the rates that priced them, the transaction — linked to the event before it. Alter one character of that event and the fingerprint stops matching. It seals the chain entry; it is not a hash of the fields of this receipt.
- Where it is keptchainPath
- .usertrust/auditA folder inside your own project; the chain itself is an append-only events.jsonl inside it. Not our servers, not a dashboard you rent — you hold the book.
- Settledsettled · true
- Settled against the budgetThe cost is held before the call and settled after it; a call that failed would have voided the hold instead. With dryRun the ledger is skipped and only the session budget moves.
- Who did the workmodel · provider
- claude-fable-5 · anthropicWhich model answered, and whose. Switch either one and the next receipt says so.
- The mathsusage · pricing
- 4 token counts × 4 ratesThe receipt carries both halves, so the cost can be recomputed from the record alone — see below.
| input | 317 | × | 100 | ÷1k | 31.700 |
| output | 131 | × | 500 | ÷1k | 65.500 |
| cache read | 87 | × | 10 | ÷1k | 0.870 |
| cache write | 43 | × | 125 | ÷1k | 5.375 |
| sum | 103.445 | ||||
| → | cost 104 | ||||
104 usertokens, 1 = $0.0001 — just over a cent. Counts and rates are both the receipt’s own, so the number is checkable from the record alone. That is the difference between a log and an account.
the line nobody could explain
Overnight, an agent retried the same call 47 times. The provider billed it. Your logs didn’t see it. Finance asks what the $500 was for — and there is nothing to dispute, nothing to replay, and nothing that would have stopped night two.
{
agent: —,
task: —,
budget hold: none,
auditHash: —,
cost: $500.00,
verifiable: no
}
every governed call returns evidence
three frontier models, three receipts, one ledger. usertrust sits in front of the call, not inside the model — so the receipt shape does not change when the provider does.
the quickstart above showed one anthropic call settle end to end. that was not a special case. route the same governed call through openai or google and the SDK hands back the same object — model, cost, settled, auditHash — written to the same ledger. one contract, multiple providers.
// → { response, receipt } model: "claude-opus-5", cost: 212, budgetRemaining: 49684, settled: true, auditHash: "3b7e91a4c05d8e12…5d02c8f61b9a4"
// same shape model: "gpt-5.6-sol", cost: 187, budgetRemaining: 49497, settled: true, auditHash: "a41c07d2e8b35f96…9e3b6f1a8c0d7"
// same shape model: "gemini-3.1-pro", cost: 96, budgetRemaining: 49401, settled: true, auditHash: "7f20e5b8d1a93c46…c14a90d3e7f25"
budget carried on from the first receipt (49,896 left) · one chain, three providers
priced across 26+ models · packages/core/src/ledger/pricing.ts — PRICING_TABLE keys
hold. settle. or void.
the banking pattern: held, then settled or voided. never lost.
every governed call opens a two-phase hold against the budget before a token moves — available = budget − settled − Σ(holds). without holds, concurrent agents each see the full budget and settle past it. with holds, the first hold that would exceed what is actually available throws.
// illustrative — the sdk opens the hold itself, inside trust() // budget: 50,000 usertokens ($5.00) const budget = 50000 agent1.hold(30000) // available = 50,000 − 30,000 = 20,000 agent2.hold(25000) // 25,000 > available (20,000) — the ledger refuses the hold throw new InsufficientBalanceError("need 25000, have 20000")
budget 50,000 usertokens ($5.00) · packages/core/src/shared/constants.ts — DEFAULT_BUDGET.
BLOCKED is a feature
the gate runs before the provider is ever called. a denial throws, the provider is never reached, and no receipt is returned — but the refusal is not silent. denials don't get receipts. they get chain events.
name: block-wire-transfers effect: deny enforcement: hard conditions: - field: "action_name" operator: "eq" value: "wire_transfer"
one of 12 policy field operators · packages/core/src/shared/types.ts — FieldOperator union members
// thrown before the call, not returned after it throw PolicyDeniedError receipt: none provider: never called
kind: "policy_denied" decision: "deny" denialClass: "policy" policyRules: [{ name: "block-wire-transfers" }]
no receipt means no spend, no ledger transfer, no line in the cost total — only the denial itself is written to the chain.
tamper with one byte. break the whole chain.
every audit entry carries the hash of the one before it. change a single byte anywhere in the chain and the verifier finds exactly where it broke.
the chain verifies itself
each entry in the audit chain stores the hash of the entry before it. a verifier recomputes every hash and walks the chain in order — each mismatch names its broken link — entry number and event id — and the walk keeps going, so every break is reported, not just the first.
and can be anchored beyond your infra
the same chain hash can be published somewhere outside your own systems, so a tamper attempt has to beat not just your database but that outside record too.
# the vault: three entries, each carrying the hash of the one before it # entry 1 hash 08c22f61… previousHash 00000000… # entry 2 hash 71e4a9c0… previousHash 08c22f61… # entry 3 hash c4d18f77… previousHash 71e4a9c0… $ npx usertrust-verify .usertrust Vault integrity: VERIFIED (UNANCHORED — internal consistency only) Chain length: 3 events Merkle root: 5d0b3c9e… Hash algorithm: SHA-256 All hashes: valid (3/3) # one byte changed inside entry 2 (amount 104 → 904), and entry 2 re-hashed to 9b21d5e3… # entry 3 still points at 71e4a9c0… — the link is what broke $ npx usertrust-verify .usertrust Vault integrity: FAILED - Event 3 (3f9c1a7e-…): previousHash mismatch. Expected 9b21d5e3…, got 71e4a9c0… Chain length: 3 events Merkle root: 9e47a1d2… Hash algorithm: SHA-256 All hashes: valid (3/3)
tamper-evident, not tamper-proof — detection, not recovery.
chain path .usertrust/audit · hash prefix 08c22f61…
don't take our word for it
the verifier is a separate, minimal program. these are the two numbers that make it trustworthy on its own terms.
the first zero is a dependency audit — nothing to compromise because nothing is installed. the second is the one that's load-bearing: 0 imports from packages/core anywhere in packages/verify. core is what produces the hashes in the first place; verify recomputes them from the same audit file on its own, without importing core to do it. if the two shared code, the verifier would just be checking its own arithmetic. because they don't, it's checking usertrust's.
$ npx usertrust-verify .usertrust # recomputing every hash — 0 imports from core Vault integrity: VERIFIED (UNANCHORED — internal consistency only) Chain length: 8 events Merkle root: 4f7ad669…2f4514c8 Hash algorithm: SHA-256 First event: 2026-08-09T16:45:13.602Z Last event: 2026-08-09T16:45:16.565Z All hashes: valid (8/8)
transcript as captured by the SDK's scripts/capture-evidence.mts · site/app/evidence/verify-transcript.json — 8-event vault, ledger mode
every way we know to forge a ledger
every commit in this repo tries to forge the ledger on purpose — then hands the attempt to a verifier that owes core nothing.
every forgery fails. every legitimate operation verifies.
harden suites: 65 · packages/core/tests/harden/**/*.test.ts — test-file count
test cases: 3,706 · packages/**/*.test.ts — count of it()/test() openers
assertions: 8,843 · packages/**/*.test.ts — count of expect( calls
invariants: 37 · AGENTS.md — count of "Prevents:" clauses
all four counted at usertrust v3.4.0 (a1bce25) on 2026-09-05 with the SDK's scripts/capture-evidence.mts rules — its shipped facts.json lags at 3.2.0
don't trust us — recompute us.
open your ledger
two commands, and the next call your agent makes comes back with something you can check.
npm install usertrust
npx usertrust init
your keys. your billing. your evidence.
start in dry-run — TigerBeetle skipped, audit chain and policy gates still run.
total surprises ··· 0
setup — docs quickstart, stated duration on the dry-run path
runtime deps — packages/verify/package.json, dependencies key count
licence — packages/core/package.json, license: Apache-2.0
run it where you need it
npm install usertrust
npx usertrust-server