Prove what your
AI decided.

Give every AI decision you instrument a tamper evident record. Seal batches with your own Ed25519 key, then hand an auditor the records and your public key and let them prove nothing changed after sealing, on their machine, with no service to call.

$dotnet add package Invarix.Guard.Evidence --prerelease
How it works Pricing
0
prompt fields in the record
RFC 6962
Merkle inclusion proofs
Ed25519
signed batch commitments
1 file
plus your public key, verifies offline
What it records

An audit trail that shows
if a sealed record changed.

Ordinary application logs can be edited, and nothing in them shows whether they were. You seal decision records into signed batches instead, so a later edit is provable rather than deniable.

01

Append only ingest

Each decision you record captures the system and model that decided, when the call started and ended, and digests of the content, with detector results and oversight actions attached when they apply. It writes as CloudEvents 1.0 JSON, and the store interface is append and query, nothing that edits.

02

Digests, not content

The record has no prompt or completion field. What lands in the store is a SHA-256 digest over the canonicalized input and output, so you can prove which content ran without retaining it.

03

Merkle sealed, Ed25519 signed

Hand a batch of records to the signer. It builds an RFC 6962 Merkle tree, the Certificate Transparency construction, and signs the 32 byte root with a key that never leaves your infrastructure. Where you cut a batch is your call.

04

Retention with legal holds

Presets for 6 month, 3 year, and 4 year retention windows. Pass your hold registry to a prune run and matching records survive it, whatever the cutoff says. PruneAndCertifyAsync signs a certificate for the run, so a gap in the trail comes with a signed account of it.

05

Export endpoint

MapEvidenceExport serves the trail as NDJSON or a JSON array with a required time range and an optional tenant filter, written straight out of the store rather than buffered. Ships with no authentication on purpose, so you attach your own policy.

06

In process, no egress

Your app builds each record inside your own process, where the decision happens. The package makes no network calls at all, including model downloads. Verification runs offline against your public key.

Program.cs
.NET 8+ · ASP.NET Core
using Invarix.Guard.Evidence;
using Invarix.Guard.Evidence.Extensions;

builder.Services.AddInvarixGuardEvidence(options =>
{
    options.AISystemId = "invoice-classifier";
    options.AISystemVersion = "2.3.1";
    options.ModelId = "gpt-4o-mini";
    options.ModelVersion = "2024-07-18";
})
.UseJsonlStore("/var/evidence/decisions.jsonl");

// then, per decision:
var record = factory.NewBuilder()
    .WithStartTime(started)
    .WithEndTime(DateTimeOffset.UtcNow)
    .WithInputHashSha256(prompt.ToSha256Hex())
    .WithOutputHashSha256(completion.ToSha256Hex())
    .WithOutcome(DecisionOutcome.Allowed)
    .Build();

await sink.WriteAsync(record);
How it works

Four steps between
a decision and a proof.

The chain below is the whole cryptographic mechanism. It runs inside your process: no call to Invarix when a record is written, when a batch is signed, or when someone comes asking about it years later.

record 1record 2record 3record 4sha-256sha-256signed root · ed25519public key verifies
  1. 1

    Record

    Your app builds a decision record at the moment it decides. You hash the input and output yourself with ContentHasher, which canonicalizes before digesting so the same content always reproduces the same hash. The record has no field for the raw prompt.

  2. 2

    Seal

    You hand a batch of records to the signer, which builds a Merkle tree over them. You can compute an inclusion proof for any record, tying it to a single 32 byte root.

  3. 3

    Sign

    The root is signed with an Ed25519 key held on your own infrastructure. The signature covers the root, the tree size, the batch ID, the key ID, and the timestamp you assert.

  4. 4

    Verify

    Give an auditor the record, its proof, the signed commitment, and your public key. The check then runs entirely on their machine.

Pricing

Every feature ships
in the free package.

There is no paid tier of Invarix.Guard.Evidence. Signing, sealing, retention presets, and the export endpoint are all in the package you install.

FAQ

What to check
before you install.

Can I install it today?
Yes. It is on NuGet as Invarix.Guard.Evidence at 1.0.0-rc.1, targeting .NET 8 and .NET 10. Because the only published version is a prerelease, you need the flag: dotnet add package Invarix.Guard.Evidence --prerelease. Without it NuGet refuses to resolve a prerelease and tells you there are no stable versions available. The release candidate tag means the public API and the signed wire formats are frozen, not that coverage is thin: over 440 tests pass, plus a separate harness that exercises the packed package itself.
Do I need Invarix.Guard to use this?
No. Invarix.Guard.Evidence is a standalone package with no dependency on the guardrail, and there is no integration code in either direction. It works with any .NET 8 or .NET 10 app, whatever you use to call your model. The two pair well because Guard's detector verdicts and oversight actions map onto fields a decision record already carries, but the few lines that copy them across are yours to write. See Invarix.Guard.
What exactly gets signed, and how does someone verify it?
You hand a batch of records to the signer. It builds an RFC 6962 Merkle tree, the same construction Certificate Transparency uses, and signs the root with your Ed25519 key. The signature covers the root, the tree size, the batch ID, the key ID, and the timestamp you supply. A verifier needs two things from you: one JSON file and your public key. EvidenceBundle.Create packs the record's exact bytes, its inclusion proof, and the signed commitment into that file. The key travels out of band, deliberately: a key carried inside the bundle could be swapped alongside a re-signed commitment, and the tampering would certify itself. EvidenceBundleVerifier.Verify checks the signature, then folds the record's bytes through the proof up to the signed root, and reports which check failed rather than a bare true or false. The proof covers exact bytes, so one altered byte fails the check. Because the tree follows RFC 6962, the construction checks out against the published Certificate Transparency test vectors. The package ships docs/FORMAT-SPEC.md, which specifies the prefix bytes, both signed payload layouts, and the numbered verification algorithm, each with test vectors.
What happens to our evidence if Invarix stops existing?
It keeps verifying, and you can check that before you install anything. The format specification ships inside the package as docs/FORMAT-SPEC.md, and a nupkg is a zip: pull it off NuGet, unzip it, read the file. It gives the canonical leaf encoding byte for byte, the RFC 6962 tree parameters, both signed payload layouts field by field with hex test vectors, the bundle schema, and a numbered five step verification algorithm that stops at the first failure. Then it works one batch of three records all the way through: every leaf hash, the intermediate node, the root, the signing payload, the signature, and the finished bundle. Two of those values come from outside Invarix: the tree vectors are the standard Certificate Transparency reference values, and the worked example is signed with the RFC 8032 section 7.1 test key, published in the RFC, so any Ed25519 implementation will tell you whether our signature is the right one. The document also states what a passing check does not prove. So an auditor can reimplement verification in any language with SHA-256 and Ed25519, without our library and without Invarix existing. That is the trade for shipping unsupported, so the formats are written to the level where you do not need us. The spec will not answer questions about your code. It covers the formats, which is the part that has to outlive us.
Do you store our prompts?
No. There is no prompt or completion field in the schema to put them in. You store SHA-256 hex digests over canonicalized input and output instead, so you can prove which content ran by hashing it again and comparing. Two free text fields do exist: the detector preview, which is null by default and is intended for a short snippet you have already redacted, and the oversight reason. Nothing in the package validates or scrubs what you put in either one.
Does this make us EU AI Act compliant?
No, and be wary of anything that claims otherwise. This package produces evidence. Whether that evidence satisfies a given obligation is a question for your counsel, not a library. We built this for security reviews, auditors, and discovery, the demands that are live today. Regulatory fit is a side effect.
Where do the records actually get stored?
Two stores ship in the box: an in memory one for tests and local development, and a single file JSONL store for low volume single instance deployments, where one process owns the file and queries scan it end to end. Neither will carry a high throughput service. The persistence boundary is a two method interface, append and query, so the intended path for production is your own adapter over Postgres, blob storage, or whatever your compliance team already approved for regulated data. The Merkle and Ed25519 primitives take byte arrays rather than store handles, so they carry no dependency on where your records live: you read records out, encode them to bytes, and hand them to the signer.
What happens when records need to be deleted?
RetentionPolicy computes a cutoff from a retention window, with presets for 6 months, 3 years, and 4 years, and RetentionEngine permanently deletes records that fall before it. Legal holds are an argument you pass to the prune call rather than state stored on the record, so records matching a hold you pass in survive that run whatever the cutoff says, and keeping the registry alive across restarts is your job. Two prune methods exist: PruneAsync deletes and reports, while PruneAndCertifyAsync also signs an Ed25519 deletion certificate recording when it ran, the cutoff and tenant scope, how many records it deleted, and the ID of every hold consulted. Use the certifying one if you want an auditor to check a gap in the trail against a signed record of the run rather than taking your word for it.
Will evidence sealed today still verify in four years?
No package can promise the company behind it will still be here, so the thing that has to last is the format. docs/COMPATIBILITY.md packs into the nupkg, so the compatibility promises ship inside the version you install. It lists what is frozen at 1.0: both signing payload layouts, the canonical leaf encoding, the bundle JSON shape, the decision record property names, the JSONL line format, the content hash canonicalization rule, and which algorithms belong to which payload version. Frozen means superseded rather than mutated. A change to what gets signed takes a new payload version byte, a change to the bundle layout takes a new formatVersion, and verifiers keep decoding the older ones indefinitely and reject versions they do not know rather than guessing. The leaf encoding is pinned by a golden byte vector test, so a serializer change in a .NET update fails our build instead of shipping. Version 1 payloads stay decodable forever, so commitments and certificates the betas signed still verify without being re-signed. One exception: batches sealed with 0.1.0-beta.1 at sizes that were not a power of two predate the tree correction in beta.2 and need re-sealing. Everything from beta.2 onward verifies as it stands. A bundle never carries the public key by design, so keeping your keyring for as long as you keep the evidence is your job. And because docs/FORMAT-SPEC.md specifies verification completely, with test vectors, an auditor in year four can check a bundle in any language that has SHA-256 and Ed25519, without the library or us.
Is the export endpoint safe to expose?
Not until you put a policy in front of it. MapEvidenceExport deliberately ships with no authentication, because guessing at your auth model would be worse than making the decision explicit. Chain RequireAuthorization or an endpoint filter onto it, put mTLS in front of it, or use whatever your host app already does. It takes a required time range plus an optional tenant filter, returns NDJSON or a JSON array, and writes straight out of the store rather than buffering the export, so response size is not bounded by your memory. Whether the store itself streams is down to the adapter you plug in.
Why is it free?
Because we would rather find out whether .NET teams actually want this than charge ten of them for it. The package is free under Elastic License 2.0: commercial use, modification, and self hosting are all fine. The limits are the ELv2 ones, so you cannot offer it to third parties as a hosted or managed service, and you have to keep the license notices intact. If a paid tier ever appears it will be for things this package does not do, not a paywall dropped in front of what already works. Free also means unsupported: no email support, no chat, no issue tracker, no SLA. Five documents ship inside the package instead, and one of them sets out every signed payload byte by byte, with test vectors and a numbered verification algorithm, so an auditor can redo the whole check in another language and nothing about your evidence depends on reaching us. If that works for your stack, there is no signup and no license key to wait for: the install command at the top of the page is where you start.