financial governance for AI agents

Open-source ledgerfor agent spend

Keep the receipts

Apache-2.0· 0 runtime deps in the verifier· 2 commands to first receipt· runs on TigerBeetle
first receipt

from install to first receipt

From an empty project to a receipt you can verify, in under 30 seconds.

What you write
01

install it

One dependency beside the provider SDK you already use. usertrust itself needs no account, no key, no service in the path.

02

create the vault

Writes .usertrust/ — config, policy rules, an empty chain.

03

wrap the client you already have

Prompts and control flow untouched. The signature gains what it cost.

agent.tstypescript
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
What comes back
receipt
{
  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" }
}
input317×100÷1k31.700
output131×500÷1k65.500
cache read87×10÷1k0.870
cache write43×125÷1k5.375
 sum103.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 alternative

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.

receiptnone issued
{
  agent:        —,
  task:         —,
  budget hold:  none,
  auditHash:    —,
  cost:         $500.00,
  verifiable:   no
}
unreceipted. paid anyway.
exhibit a

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.

anthropicclaude-opus-5
// → { response, receipt }
model: "claude-opus-5",
cost: 212,
budgetRemaining: 49684,
settled: true,
auditHash: "3b7e91a4c05d8e12…5d02c8f61b9a4"
openaigpt-5.6-sol
// same shape
model: "gpt-5.6-sol",
cost: 187,
budgetRemaining: 49497,
settled: true,
auditHash: "a41c07d2e8b35f96…9e3b6f1a8c0d7"
googlegemini-3.1-pro
// 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

exhibit b

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.

held
settled
voided
the racetwo agents, one budget
// 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.

exhibit c

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.

policy denial
policy rule deny · hard
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

caller what the SDK sees
// thrown before the call, not returned after it
throw PolicyDeniedError
receipt: none
provider: never called
chain what the ledger records
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.

exhibit d

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.

verify.usertrust
# 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…

exhibit e

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.

0runtime dependencies in the verifier.
verifier runtime dependencies: 0 — packages/verify/package.json, dependencies key count
0the verifier owes us nothing.
imports the verifier takes from core: 0 — packages/verify parity contract (AGENTS.md), import audit

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.

usertrust-verify0 deps · 0 imports from core
$ 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

exhibit f

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.

65harden suites

harden suites: 65 · packages/core/tests/harden/**/*.test.ts — test-file count

3,706test cases

test cases: 3,706 · packages/**/*.test.ts — count of it()/test() openers

8,843assertions

assertions: 8,843 · packages/**/*.test.ts — count of expect( calls

37invariants enforced

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.

get started

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.

setup2 min
runtime deps in the verifier0
licenceApache 2.0

total surprises ··· 0

where these were counted

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

local SDK shipping today npm install usertrust
self-hosted control plane shipping today npx usertrust-server
managed proxy on request request access →