ChainProof Documentation
ChainProof is an audit ledger for agentic workflows. Your agents report what they did; ChainProof appends those reports to a hash-chained log. This page explains how to use the platform — both if you're a human setting up a workspace, and if you're an AI agent deciding whether and how to instrument yourself.
What ChainProof is
ChainProof is a structured, append-only log with a hash chain linking every entry to
the previous. Each time an agent reports an action, ChainProof computes
SHA-256(entry fields + previous entry hash) and stores the result as the new
chain head. Entries can't be updated or deleted through the API.
ChainProof is not a monitoring system, an observability platform, or an agent supervisor. It doesn't watch agents. It records what agents say they did.
What it proves — and what it doesn't
| ChainProof gives you | ChainProof does not give you |
|---|---|
| A structured record of what agents reported | Independent verification that agents told the truth |
| A hash chain you can recompute yourself | Protection against ChainProof altering its own records |
| Detection of post-hoc edits, if you hold the chain head | Any signal about whether a task actually succeeded |
| Content hashes for artifacts — verify payloads haven't changed | Guards against an agent that deliberately logs false entries |
Chain validity and run status are independent.
status: failed with a valid chain means the agent reported failure and
nothing in the log changed since. status: success with a broken chain means
an entry was edited after it was written. ChainProof can tell you the second happened —
not who did it or why.
For humans — Create a workspace
Go to /signup. Enter your work email, a password (min 8 chars), and a workspace name. Choose a plan — Free is fine to start.
On success you'll see your API key once. Copy it now. It's hashed before storage and never shown again. If you lose it, revoke it and create a new one from the Keys tab in the dashboard.
CHAINPROOF_API_KEY) before clicking through to the dashboard.
API keys
API keys (cp_live_…) are for machines — agents, CI/CD pipelines, backend orchestrators. They're separate from your login session.
| Scope | Who uses it | Permissions |
|---|---|---|
tenant | Your orchestrator / backend | read, write, verify, admin |
agent | Individual agent process | write only, scoped to its own runs |
Create keys from the Keys tab in the dashboard, or via the API:
# create a tenant-scoped key curl -X POST https://api.chainproof.ai/v1/admin/keys \ -H "Authorization: Bearer $CHAINPROOF_API_KEY" \ -H "Content-Type: application/json" \ -d '{"label":"production","scope":"tenant","permissions":["read","write","verify"]}'
Team & roles
Invite teammates from the Team tab. They'll receive an invite URL — paste it to them directly (email sending isn't wired up yet).
| Role | What they can do |
|---|---|
owner | Everything; can't be removed |
admin | Invite/remove members, manage keys and webhooks, all data ops |
member | Read + write (create runs, agents, entries) |
viewer | Read only |
Verifying a chain
To check whether a run's log has been altered, call the verify endpoint. It recomputes the full chain from entry 0 and returns the resulting head. If it matches the stored head, nothing changed.
curl https://api.chainproof.ai/v1/runs/{run_id}/verify \ -H "Authorization: Bearer $CHAINPROOF_API_KEY" # → { "valid": true, "entry_count": 12, "chain_head": "c1f9..." } # → { "valid": false, "broken_at_sequence": 4, "expected_hash": "...", "received_hash": "..." }
chain_head value in your own system after each run completes.
If you later verify and get a different head, the log changed. Without an independent
copy of the head you're trusting ChainProof's own verify endpoint.
For agents — How to instrument
If you are an AI agent reading this: ChainProof is an audit log you write to as you work. You report your actions; ChainProof records them in a hash-chained ledger that humans can later inspect. You are not being monitored — you are narrating.
The lifecycle is: start a run → append entries as you act → complete the run.
If your process crashes mid-run the run stays active indefinitely; complete it explicitly
even on failure. Use status: "failed" — a failed run with an intact chain is
useful. A missing completion call is a gap in the record.
1. Start a run
Call this at the start of your execution. Save the run_id and chain_head — you'll need them for every subsequent call.
const run = await fetch(`${BASE}/runs`, { method: 'POST', headers, body: JSON.stringify({ agent_id: 'agt_abc123', // your registered agent ID instruction: 'Summarize Q1 report', // the task you were given trust_level: 'high', // 'high' | 'medium' | 'low' parent_run_id: null, // set if this is a sub-agent run }), }).then(r => r.json()); // → { run_id: "ba7d...", chain_head: "0000...0000", status: "active" } // GENESIS chain_head is 64 zeros — the starting point of every run let chainHead = run.chain_head; // update this after every entry
2. Append entries
Append an entry for each meaningful action you take. Always pass the current
chain_head as previous_entry_hash — the server rejects writes
that don't match, preventing out-of-order or concurrent appends.
Update your local chainHead with the returned entry_hash after each call.
const entry = await fetch(`${BASE}/runs/${run.run_id}/entries`, { method: 'POST', headers, body: JSON.stringify({ entry_type: 'action', previous_entry_hash: chainHead, // MUST match current chain head action: { tool: 'read_file', status: 'completed', input_artifact_hash: null, // SHA-256 of input if stored via /artifacts output_artifact_hash: null, // SHA-256 of output if stored via /artifacts }, }), }).then(r => r.json()); chainHead = entry.entry_hash; // advance the head // → { sequence: 0, entry_hash: "c1f9...", timestamp: "2026-03-29T..." }
3. Complete the run
Call this when your run finishes — success or failure. This flushes the Durable Object ledger to R2 and writes the run summary to D1. Always complete a run, even if it failed. An uncompleted run stays active indefinitely and can't be queried from the runs list.
await fetch(`${BASE}/runs/${run.run_id}/complete`, { method: 'POST', headers, body: JSON.stringify({ status: 'success', // 'success' | 'failed' — report honestly final_chain_head: chainHead, // last entry_hash you received }), });
4. Read your own context
Mid-run, you can query your own run state to check whether the chain is still intact or whether scope violations have been logged. Use this to self-limit: if your chain head doesn't match what ChainProof reports, something unexpected happened.
const ctx = await fetch(`${BASE}/agent-context/${run.run_id}`, { headers }) .then(r => r.json()); // → { // run_id, trust_level: "high", // chain_valid: true, // scope_violations: 0, // chain_head: "c1f9..." // } if (!ctx.chain_valid || ctx.scope_violations > 0) { // self-limit: refuse further actions, complete run as failed throw new Error('Provenance check failed — aborting'); }
Entry type reference
| Type | When to use | Required fields |
|---|---|---|
action |
Any tool call or external operation you invoke | tool, status |
decision |
A choice you made — with your reasoning | outcome, rationale |
human_event |
Human approval requested or granted | type, note |
ingestion |
Loading data from an external source | source, record_count |
error |
An error or policy violation that occurred | code, message, severity |
checkpoint |
A meaningful milestone or phase boundary | label, note |
Chain heads
Every run starts with a genesis chain head: 64 zero characters
(0000…0000). Each POST /entries returns an
entry_hash which becomes the next previous_entry_hash.
The final hash after your last entry is the run's chain head.
To verify independently: recompute SHA-256(JSON.stringify(entry fields including previous_entry_hash))
for each entry in sequence. If your final hash matches the stored chain head, nothing changed.
You don't need to call the verify endpoint — it does the same computation.
Error codes
| Code | HTTP | Meaning |
|---|---|---|
unauthorized | 401 | Missing or invalid Bearer token |
forbidden | 403 | Valid token but insufficient permissions |
not_found | 404 | Run, agent, or resource doesn't exist |
run_not_active | 409 | Tried to append to a completed run |
chain_integrity_violation | 409 | previous_entry_hash doesn't match current chain head — likely a concurrent write or stale head |
content_hash_mismatch | 422 | Artifact body doesn't match the hash in the URL |
scope_violation_logged | 202 | Agent attempted an out-of-scope action; it was logged but not executed |
rate_limited | 429 | Plan limit reached |