How it works
The ledger, the seal, and the join
Assay is not in the request path. It cannot slow a turn down and cannot break one — it reads what already happened, prices it against a versioned table, and writes it once.
Step one
What gets recorded
Every unit of AI work becomes one usage span: token counts broken out by phase, cost in integer micro-USD, and the full chain from principal to session to task to tool call. Ingestion is idempotent on the producer reference, so re-delivery of a replayed event returns the existing span flagged as not-created.
That flag is the exactly-once signal a wallet debit keys on. It is what makes the meter safe to feed from an at-least-once stream and safe to charge from.
A span is written once. The storage port exposes no update and no delete — there is no destructive method to call.
Token phases, priced separately
Step two
What gets sealed
Conversation text reaches a store through exactly one pipeline: scrub twelve secret classes from the full text, derive the honest features, truncate, then seal with AES-256-GCM under that principal's own data key. It never lands in a metadata column, never in a sync batch, and never in an annotation note.
Twelve secret classes
Removed from the full text before anything downstream sees it — not from a preview, from the source.
Features first
Token phases, ratios, tool name, timing. Computed before the seal, which is why every derived number survives a lost key.
Bounded at rest
Content is cut to a bound, so a runaway turn cannot silently become a runaway store.
AES-256-GCM
Under that principal's own data key. Destroy the key and their history is permanently unreadable while every row is retained.
A remote model scoring a span sees derived features only — and for the raw payload, a shape fingerprint: key count and depth, never a key name and never a value. Full-content judging must run on a local model, or be opted into at the command line.
Step three
What gets joined
Said and spent, in one record. That join is the asset the rest of the product is built on — it is what lets Assay answer what a session was for, what a product cost to build, where money is being wasted, and what a result cost.
- What was this for? A closed, fixture-pinned goal vocabulary enforced at the parse boundary, so labels aggregate instead of splitting one real goal across a dozen rows.
- What did this product cost to build? Derived from path references in the harness's own session documents, reported in both honest readings, with every population of spend that reached no product printed beside it.
- Where is money being wasted? Four detectors over the classified spans, each drilling into the conversation that spent the money — and each carrying the count and share it excluded.
- What did a result cost? Outcomes bound to the spend that produced them under a recorded method, with measured zeros kept in the denominator and only genuine unknowns excluded.
The surface
The console
The read-only console serves seven panels over your store and prints its URL on start. Goals, waste and ROI run the same reports the CLI runs, so the console and the command line cannot report different numbers. A panel appears only if this store can answer it, and anything turned off is stated on the page.
Captured from assay dashboard over a deterministic synthetic corpus — assay seed for the spans, a synthetic transcript fixture for the conversations — never the live store. Its --snapshot mode stamps every static page with a fidelity stamp that ends in these numbers do not update.
The console binds loopback, and a routable bind is refused unless you name it unsafe — because binding one would expose unauthenticated read access to spend data and decrypted conversations. Decryption is server-side, on the machine where the key already lives. To browse a containerised installation's conversations you run the console inside that container. The key never travels; the console does.
Getting started
Install
One command makes a machine ready: a config file so there are no repeated flags, an empty schema-applied store, and a 0600 content key. Then it tells you to back the key up. Lose it and conversation content is unreadable; every derived number survives, because features are computed before the seal.
SQLite is the default store. Postgres is served through an injected executor, so the library carries no database dependency of its own — and both backends run one shared contract suite that proves byte-identical ordering, grouping and idempotency.
The repository is private today. The MIT grant on the substrate is standing, so publication would be a visibility flip rather than a relicensing — but it has not happened.
$ assay init
Assay is ready.
created:
config ./assay.config.json
store ./assay.sqlite (empty)
key ~/.config/assay/transcript.key (0600)
⚠ BACK UP THE CONTENT KEY …
next:
assay capture claude-code # logs → priced spans + turns
assay goals # spend + valence per goal
Or embed it
// the substrate — MIT, no database dependency
import { createMeter, SqliteSpanStore } from 'assay';
const store = new SqliteSpanStore('./assay.sqlite');
const meter = createMeter({ store });
// idempotent on producerRef — re-delivery is a no-op
await meter.record({ producerRef: 'gateway:sess-1:1', ... });
Deletion
Erasure
Each principal's content is sealed under its own data key, so destroying that key makes one party's history permanently unreadable while every row is retained. It censuses before it acts and exits non-zero on a refusal or a partial — a refused erasure is never reported as a completed one.
Quote the window, not the exit code: erasure is immediate in the live store and irreversible only once backups rotate — no more than 35 days in our own deployment, because the master key is inside the backup set by design.
The network
One verb reaches the network
assay pricing watch fetches public rate documents. It never opens the database, presents no credential, and transmits nothing. No other verb calls it; it is never ambient.
The sync verb, when you choose to run it, sends an enumerated export whose raw-payload field does not exist in the type — not blanked, absent — and the receiving end rejects any record carrying one.
Assay your spend.
It runs on your machine, against your own store. Exactly one of its verbs reaches the network, and that verb fetches public rate cards.