Quick start#
With your license issued, get from nothing to a first sealed, verifiable run in under 30 minutes.
1. Install your license#
BitExact is licensed use only; there is no evaluation mode. Without a valid license nothing records or replays (the proxy still starts, and your agents' calls pass through it unrecorded, unless you turn on strict mode). When a license expires, recording continues through a grace period (14 days unless the license names another); after it the deployment becomes read-only: nothing new is recorded, and everything already recorded stays readable, verifiable and exportable.
Without a valid license only bitexact capture keygen, bitexact keygen, bitexact license, bitexact pki, bitexact recorder-key, bitexact version and bitexact witness answer. bitexact collector, bitexact mcp-proxy, bitexact recorder and bitexact serve start in any license state and check the license on each call: they pass the agents' traffic through, or declare a gap, rather than record.
Generate the recorder key your deployment will sign with, send us the public half, and we issue a license bound to it. With nothing but Docker on the host, the pilot makes it (step 2):
cd deploy/pilot && mkdir -p secrets
docker compose run --rm keys # prints the public half to send us
or, with the product installed from your platform's release bundle (Python 3.15+, Installing):
mkdir -p secrets # step 2 mounts this directory
bitexact keygen -o secrets/recorder.key # and recorder.pq.key; keep both on this host
bitexact capture keygen -o secrets/capture.key # keys hash-only fields
(umask 077; head -c 32 /dev/urandom | base64 > secrets/seal.secret) # ends runs (step 3)
export BITEXACT_SIGNING_KEY=$PWD/secrets/recorder.key
export BITEXACT_CAPTURE_KEY=$PWD/secrets/capture.key
export BITEXACT_LICENSE=$PWD/secrets/bitexact.license # a path, not the contents
bitexact license # confirms licensee, tier, expiry - and that this key may record
keygen prints both public halves, Ed25519 and post-quantum - send both
for the license - and the trust-file entry that pins them: give it to
whoever verifies your evidence. Keep capture.key with the recorder key,
never in the data volume.
For a key held in an HSM or a KMS, bitexact recorder-key prints its public
half (--pq its post-quantum half); it needs no license, since it is what the license is issued for.
The license names that recorder key: one deployment per key. Copied to a second stack signing with a different key, it is refused.
2. Install the services#
One command, for a pilot on one host: the deployment bundle's
deploy/pilot/docker-compose.yml runs all four services below with the license
and key from step 1 wired in, and nothing else on the host. Its header lists
the steps: load the image, docker compose run --rm keys, put the license in
secrets/, choose a viewer token, docker compose up -d. Then
docker compose run --rm recorder seed records a sample run, on the
recorder, the one process that writes chains. Its setup step
installs the key and license for the image's user (uid 10001) whoever owns
them on the host, so a key made mode 0600 by another account still works.
It also runs the recorder - the one process given the signing key, which
writes every run and the viewer's acts - and this host's collector, which
the proxy and TypeScript, Go and Java agents (step 3) hand their events to.
The rest of this step is a smaller deployment by hand.
BitExact is four services over one data volume. The recorder is the only process given the signing key: it writes and signs every run. The collector takes the proxy's calls and forwards them to it over mutual TLS. The proxy is what your client points at. The inspector, the viewer, never edits a recorded entry; the acts it can perform - a legal hold, a release, a two-person redaction - are handed to the recorder, which writes and signs them (see deployment.md for the full list).
We deliver the image as a tar; load it and pin what you loaded, so the
commands below name a digest rather than a floating tag. The image runs as
uid 10001: on a Linux host, files made by your account are unreadable to it
until you sudo chown -R 10001 secrets tls (the pilot's setup does the
equivalent for you).
Every container runs as that one uid, so file modes cannot keep one
service's keys from another: what each container mounts does. Each is
given its own TLS certificate and key, and the CA's certificate, from a
directory of its own under tls/, and only the secrets it reads, file by
file. pki/, which holds the CA's key, is mounted into none of them, and
only the recorder is given the signing key. A file mounted on its own stays
the file the container started with: after replacing one (a renewed
license), restart the containers that mount it.
# The image ID is the digest of what you just loaded; `docker run` takes it as is.
BITEXACT_IMAGE=$(docker load -q -i bitexact-<version>-<amd64|arm64>.tar | sed -n 's/^Loaded image: //p')
export BITEXACT_IMAGE=$(docker image inspect --format '{{.Id}}' "$BITEXACT_IMAGE")
docker network create bitexact
# Certificates for the services' mutual TLS, and the recorder's policy:
# the collector may write runs, the inspector may hand it its acts.
bitexact pki init --dir pki --name "BitExact CA"
bitexact pki issue --dir pki recorder --dns recorder
bitexact pki issue --dir pki host --spiffe spiffe://local/host
bitexact pki issue --dir pki inspector --spiffe spiffe://local/services/inspector
# The command line's own identity, mounted into no container: it alone is
# granted roles, purge records, seals and anchors.
bitexact pki issue --dir pki operator --spiffe spiffe://local/operators/cli
mkdir -p operator && mv pki/operator.crt pki/operator.key operator/ \
&& cp pki/ca.crt operator/
# Each service's certificate and key, and the CA's certificate, in a
# directory only that service's container mounts; the CA's key stays in pki/.
for s in recorder host inspector; do
mkdir -p tls/$s && cp pki/ca.crt pki/$s.crt pki/$s.key tls/$s/
done
cat > secrets/policy.json <<'EOF'
{"rules": [
{"peer": "spiffe://local/host", "runs": "*",
"kinds": ["tool_call", "http_call", "nondet", "identity", "context",
"marker", "policy_check", "model", "human_decision",
"decision_record"]},
{"peer": "spiffe://local/services/inspector", "runs": "*", "kinds": [],
"acts": ["hold", "release-hold", "redaction-request",
"redaction-approve", "redaction-reject", "custody", "fork",
"ledger:cases", "ledger:capture-expectations", "ledger:refusals",
"log-checkpoint", "ledger-heads", "redact", "access",
"events-retry"]},
{"peer": "spiffe://local/operators/cli", "runs": "*", "kinds": [],
"acts": ["hold", "release-hold", "redaction-request",
"redaction-approve", "redaction-reject", "custody", "fork", "ledger",
"log-checkpoint", "log-backfill", "purge-record", "anchor",
"ledger-heads", "seal", "redact", "access", "events-retry"]}]}
EOF
(umask 077; head -c 32 /dev/urandom | base64 > secrets/intake.secret)
# Recorder - the one process given the signing key:
docker run -d --name recorder --network bitexact -v bitexact-data:/data \
-v $PWD/secrets:/secrets:ro -v $PWD/tls/recorder:/tls:ro \
-e BITEXACT_LICENSE=/secrets/bitexact.license \
-e BITEXACT_SIGNING_KEY=/secrets/recorder.key \
-e BITEXACT_CAPTURE_KEY=/secrets/capture.key \
-e BITEXACT_RECORDER_POLICY=/secrets/policy.json "$BITEXACT_IMAGE" \
recorder --host 0.0.0.0 --port 7470 --tls-cert /tls/recorder.crt \
--tls-key /tls/recorder.key --tls-client-ca /tls/ca.crt
# Collector - takes the proxy's calls and forwards them to the recorder:
docker run -d --name collector --network bitexact -v bitexact-spool:/spool \
-v $PWD/tls/host:/tls:ro \
-v $PWD/secrets/capture.key:/secrets/capture.key:ro \
-v $PWD/secrets/intake.secret:/secrets/intake.secret:ro \
-e BITEXACT_CAPTURE_KEY=/secrets/capture.key "$BITEXACT_IMAGE" \
collector --spool /spool --recorder recorder:7470 \
--tls-cert /tls/host.crt --tls-key /tls/host.key --tls-ca /tls/ca.crt \
--intake-host 0.0.0.0 --intake-exposed --intake-port 7378 \
--intake-secret-file /secrets/intake.secret
# Proxy - what your client points at (port 8787), on this host's loopback;
# deployment.md (The listener) covers serving agents elsewhere. No key:
docker run -d --name proxy --network bitexact -p 127.0.0.1:8787:8787 \
-v bitexact-data:/data \
-v $PWD/secrets/bitexact.license:/secrets/bitexact.license:ro \
-v $PWD/secrets/capture.key:/secrets/capture.key:ro \
-v $PWD/secrets/intake.secret:/secrets/intake.secret:ro \
-v $PWD/secrets/seal.secret:/secrets/seal.secret:ro \
-e BITEXACT_LICENSE=/secrets/bitexact.license \
-e BITEXACT_CAPTURE_KEY=/secrets/capture.key \
-e BITEXACT_COLLECTOR=http://collector:7378 \
-e BITEXACT_COLLECTOR_SECRET_FILE=/secrets/intake.secret \
-e BITEXACT_SEAL_SECRET_FILE=/secrets/seal.secret "$BITEXACT_IMAGE"
# Inspector - the viewer over the same data (port 8788), published on this
# host's loopback only: it speaks plain HTTP, and its sign-in token and the
# evidence must not cross a network in the clear. BITEXACT_HEALTH_PORT
# points the image's health probe at the port this role actually serves;
# without it the container runs but never reads as healthy. The viewer serves
# a non-loopback address only with authentication set. Open it in a browser
# and sign in with the token; an API client sends it as
# `Authorization: Bearer <token>`. No key: its acts go to the recorder.
docker run -d --name inspector --network bitexact -p 127.0.0.1:8788:8788 \
-v bitexact-data:/data -v $PWD/tls/inspector:/tls:ro \
-v $PWD/secrets/bitexact.license:/secrets/bitexact.license:ro \
-v $PWD/secrets/capture.key:/secrets/capture.key:ro \
-e BITEXACT_LICENSE=/secrets/bitexact.license \
-e BITEXACT_CAPTURE_KEY=/secrets/capture.key \
-e BITEXACT_RECORDER=recorder:7470 \
-e BITEXACT_RECORDER_TLS_CERT=/tls/inspector.crt \
-e BITEXACT_RECORDER_TLS_KEY=/tls/inspector.key \
-e BITEXACT_RECORDER_TLS_CA=/tls/ca.crt \
-e BITEXACT_HEALTH_PORT=8788 \
-e BITEXACT_VIEWER_TOKENS=<a-long-random-token>:admin \
"$BITEXACT_IMAGE" view --host 0.0.0.0 --port 8788
# For browsers on other machines, put a TLS-terminating reverse proxy on this
# host in front of 127.0.0.1:8788 and add -e BITEXACT_VIEWER_COOKIE_SECURE=1,
# so the session cookie is sent over HTTPS only - or give the viewer its own
# certificate with --tls-cert and --tls-key (deployment.md, The listener).
# or use the SDK/CLI directly, from your platform's release bundle
# (installing.md); note: the import is import bitexact
pip install --no-index --find-links wheels --require-hashes -r requirements.lock
pip install --no-index --no-deps bitexact_sdk-*.whl
3. Point your client here (zero code)#
Set your OpenAI/Anthropic client's base URL at the recorder, and send a run-id header so calls group into one run - one id per decision, so a complaint about one customer finds one run:
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8787/v1",
default_headers={"X-BitExact-Run-Id": "loan-42-decision"})
const client = new OpenAI({ baseURL: "http://localhost:8787/v1",
defaultHeaders: { "X-BitExact-Run-Id": "loan-42-decision" } });
Any language works the same way: the base URL and the header. Your traffic
now records as http_call evidence. A proxy-only run stays unsealed (it
captures the wire, not your tools) until it is ended. Your client ends it
when the decision is made, with the seal secret from step 1 (the pilot's
keys step makes it as secrets/seal.secret):
curl -X POST -H "Authorization: Bearer $(cat secrets/seal.secret)" \
http://localhost:8787/bitexact/runs/loan-42-decision/seal
or, on the recorder's host, bitexact seal:
bitexact seal my-run
A seal is evidence, so the route is closed without that secret, and it seals only a run the proxy itself recorded - never an SDK's or the recorder's (deployment.md, Ending a proxy run over HTTP). The recorder can keep serving while a seal lands; it takes the run's lock for the moment it needs. The seal attests where recording stopped, never that every call the agent made went through the proxy.
TypeScript, Go and Java SDKs record tool calls and decisions as well as model traffic. The pilot already runs the recorder and this host's collector; start the agent pointed at it:
BITEXACT_COLLECTOR=http://127.0.0.1:7378 \
BITEXACT_COLLECTOR_SECRET_FILE=$PWD/secrets/intake.secret \
node agent.js # or your Go binary, or java -jar agent.jar
Set only on the agent, they leave the bitexact commands in this shell
recording into the store as before.
Each SDK's README has a five-minute first recording.
4. Make it an exhibit (recommended)#
The SDK path seals the run and captures your tool calls and decisions — the things an auditor cares about — not just http traffic. It is two decorators:
import bitexact
from bitexact.adapters import wrap_openai
from openai import OpenAI
client = OpenAI()
with bitexact.run("my-run") as r: # a run id records once: use a new one per run
client = wrap_openai(client, r)
@bitexact.record_tool # wrap your real tools
def get_balance(account_id): ...
# ... run your agent as usual; the run seals when the block ends ...
The run seals when the with block ends normally. If an exception ends it,
the run stays unsealed: it did not finish, so its end is not attested, and
bitexact verify says unsealed. Seal it with bitexact seal my-run once
you have looked, if what it holds is the record you want to keep. A run id
records once: reusing one records under <id>-<8 hex> with a run_id_taken
warning, and r.run_id is the id the run actually got.
Print this block any time, pre-filled with your host and port:
bitexact quickstart
5. See it verify#
Open the inspector at http://localhost:8788 (the viewer container from
step 2) and sign in with the viewer token, or run bitexact view if you
installed the CLI. On a store with no runs yet the inspector opens on Get
started: it checks the license and the recorder key, waits for your first
run and shows it the moment it arrives (no reload), then shows its verdict and
offers its evidence pack. Otherwise, click the run and press Verify.
The green verdict, "Intact, sealed and signed", means the captured trajectory is intact, sealed, honestly redacted and signed by a recorder key, and carries what a replay would need — not that it is the complete set of calls the agent made (BitExact proves the integrity of what was captured and surfaces gaps; it never claims completeness). Without a recorder key the verdict is amber, "Intact and sealed, not signed".
Prove it without trusting this app. Build an evidence pack and run the
standalone, open-source verifier on your own machine. It is one Python file
that imports nothing from BitExact, and the pack vendors the exact copy that
produced it under data/verification/ - export does not, which is why the
pack is what you build here. The release's sha256 is in the verifier's
CHECKSUMS.txt so you can confirm the copy you hold. The verifier needs
cryptography and asn1crypto, and nothing else:
bitexact evidence-pack my-run -o my-run.evidence-pack
pip install cryptography asn1crypto
python my-run.evidence-pack/data/verification/bitexact_verifier.py \
my-run.evidence-pack/data/runs/my-run.bundle.json \
--expect-recorder-key <the recorder public key>
The recorder public key is the one keygen (or docker compose run --rm keys) printed in step 1; bitexact recorder-key prints it again.
No agent handy?#
Seed a real, offline, sealed sample run — an automated adverse loan-decline, a decision about a person — and open it:
bitexact seed
bitexact view
If the inspector is already open on Get started, run bitexact seed and
watch the run arrive: the screen checks every 3 seconds.
The sample's model response is a fixture (an in-chain marker says so), but the recording, chain, and seal are real: it verifies exactly like a live run.
Next#
- What a bundle proves, and how an auditor verifies one → Verifying a bundle
- The inspector, for compliance and audit staff → Inspector guide
- The record kinds and the provenance model → Bundle format
- Deploying behind your gateway / on-prem → Deployment