Skip to content
BitExactdocs
Documentation
On this page

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.

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#