Skip to content

Getting started ​

Two paths. The SDK path records what the agent's own process reports: the quickest way to a receipt, and every field in it is the agent's word (claimed). The gateway path puts a process the agent does not control between it and the tools, so a call can be denied before it runs and a stranger can accept the record. Start with the SDK to see a receipt today; move a tool to the gateway when its call moves money or touches production. Either path runs in observe mode first, "mode": "observe" in the config, recording what the policy would deny without blocking anything, until the policy is right.

Pick your stack ​

Install the package, make a key and a config once, then add the lines for your framework: Claude Code, the Claude Agent SDK, the OpenAI Agents SDK, LangChain, Vercel AI, OpenClaw, DeepSeek Harness, Hermes, Google ADK, or plain Python. Each produces a receipt in receipts/ that the browser verifier checks with the .pub file.

bash
npm install @agent-custody/receipts && npx agent-custody keygen --dir keys --name app
json
// .claude/settings.json: one hook command records every tool call and can deny one before it runs
{ "hooks": {
  "PreToolUse":  [{ "matcher": "Bash|Write|Edit|mcp__.*", "hooks": [{ "type": "command", "command": "agent-custody hook --config /abs/path/sdk.json" }] }],
  "PostToolUse": [{ "matcher": "Bash|Write|Edit|mcp__.*", "hooks": [{ "type": "command", "command": "agent-custody hook --config /abs/path/sdk.json" }] }]
} }
ts
import { query } from "@anthropic-ai/claude-agent-sdk";
import { loadSdkConfig } from "@agent-custody/receipts";
import { claudeAgentHooks, createSdkIssuer } from "@agent-custody/receipts/sdk/claude";

const issuer = createSdkIssuer(loadSdkConfig("./sdk.json"));
for await (const msg of query({ prompt: "Refund the customer", options: { hooks: claudeAgentHooks(issuer) } })) { /* … */ }
ts
import { Agent } from "@openai/agents";
import { loadSdkConfig, createSdkIssuer } from "@agent-custody/receipts";
import { wrapTools } from "@agent-custody/receipts/sdk/openai-agents";

const issuer = createSdkIssuer(loadSdkConfig("./sdk.json"));
const agent = new Agent({ name: "billing", tools: wrapTools(issuer, [refundTool, lookupTool]) });   // a denied call never runs; the model sees why
ts
import { tool } from "@langchain/core/tools";
import { loadSdkConfig, createSdkIssuer } from "@agent-custody/receipts";
import { receiptCallbacks } from "@agent-custody/receipts/sdk/langchain";

const issuer = createSdkIssuer(loadSdkConfig("./sdk.json"));
await refund.invoke({ customer_id, amount }, receiptCallbacks(issuer));                              // record what happened
const enforced = tool(issuer.wrap("stripe.refund", fn), { name: "stripe.refund", schema });         // or deny before it runs
ts
import { generateText } from "ai";
import { loadSdkConfig, createSdkIssuer } from "@agent-custody/receipts";
import { wrapTools } from "@agent-custody/receipts/sdk/vercel-ai";

const issuer = createSdkIssuer(loadSdkConfig("./sdk.json"));
const result = await generateText({ model, prompt, tools: wrapTools(issuer, tools) });
ts
// an OpenClaw plugin entry: every tool call the agent makes, a denied one blocked before it runs
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-sdk";
import { createSdkIssuer, loadSdkConfig } from "@agent-custody/receipts";
import { registerOpenClaw } from "@agent-custody/receipts/sdk/openclaw";

export default definePluginEntry({ id: "agent-custody", name: "agent-custody", description: "A receipt for every tool call",
  register(api) { registerOpenClaw(api, createSdkIssuer(loadSdkConfig("./sdk.json"))); } });
yaml
# the module is the plugin: list it in cordis.yml (dsh web --patch ./cordis.yml) with the SDK config as its setting
- insert:
    - id: agent-custody
      name: "@agent-custody/receipts/sdk/deepseek-harness"
      config:
        config: /abs/path/sdk.json
yaml
# ~/.hermes/plugins/agent-custody/plugin.yaml, beside an __init__.py that reads: from agent_custody.hermes import register
name: agent-custody
version: 1.0.0
provides_hooks: [pre_tool_call, post_tool_call]
config_schema:
  sidecar_url: { type: str, default: "http://127.0.0.1:8791" }
# then: pip install agent-custody && hermes plugins enable agent-custody
python
# pip install "agent-custody[adk]"; the sidecar is `npx agent-custody serve --config sdk.json`
from google.adk.agents import LlmAgent
from agent_custody import Client
from agent_custody.adk import adk_callbacks

agent = LlmAgent(name="billing", model="gemini-2.5-flash", tools=[refund, lookup], **adk_callbacks(Client()))   # a denied call is skipped; the model sees the receipt
python
# pip install agent-custody; the sidecar is `npx agent-custody sidecar`
from agent_custody import Client, PolicyDeniedError

client = Client("http://127.0.0.1:8791")
refund = client.wrap("stripe.refund", lambda args: stripe.refund(**args))
refund({"amount": 5000})   # decide, run, record; raises PolicyDeniedError on deny

Then verify the receipt in the shell, or drop it on /verify:

bash
npx agent-custody verify receipts/<id>.json --issuer-key keys/app.pub --log log.jsonl

Every adapter is on the SDK page with what it enforces and what it only records. To make a receipt evidence a customer or auditor accepts, put the gateway in front of the tool and log to a log run by someone else: the receipt then carries a signed grant, a policy decision made outside the agent, and a tree head signed by a key you do not hold.

Principala person, with a keyAgentany frameworkGatewaychecks the grantevaluates the policyfetches facts itselfToolMCP, REST, anythingsigns a granttrusted keytool callonly if permittedone per call, allowed or deniedSigned receiptwho, what, saw, did, dependedhashMerkle logsigned headsCheckpointspublishedVerifierpublic keys, nothing elsereads
The gateway sits between the agent and its tools. Every call becomes a signed receipt whose hash lands in a tamper-evident log. Anyone with the public keys can verify a receipt, offline.

The whole loop, in one file ​

Node 22 or later. Both packages ship compiled JavaScript with type declarations.

bash
npm install @agent-custody/receipts @agent-custody/state

1. Give the agent's process a signing key and a config. The key signs every receipt. The config says where receipts and the log go.

bash
npx agent-custody keygen --dir keys --name app
json
{ "agentId": "support-bot", "identity": { "keyFile": "keys/app.key" }, "receiptsDir": "receipts", "logFile": "log.jsonl" }

2. Record a tool call as a receipt. Wrap the functions the agent calls, or record a call explicitly. Either way a signed receipt lands in receipts/ and a leaf in the Merkle log.

ts
import { createSdkIssuer, loadSdkConfig } from "@agent-custody/receipts";

const issuer = createSdkIssuer(loadSdkConfig("./sdk.json"));

const lookup = issuer.wrap("crm.lookup", async (args: { id: string }) => crm.lookup(args));   // records every call
const customer = await lookup({ id: "acct:42" });

const bundle = issuer.record({ tool: "crm.lookup", args: { id: "acct:42" } }, { status: "executed", result: customer });   // or record one by hand and keep the bundle

3. Verify it, offline. Anyone with the public key and a copy of the log can check the signature, the statement, and the inclusion proof. No access to the agent or the issuer needed.

ts
import { loadPublicKey, verifyBundle } from "@agent-custody/receipts";

verifyBundle(bundle, { issuerKeys: [loadPublicKey("keys/app.pub")], principalKeys: [], logFile: "log.jsonl" }).ok;   // true
bash
npx agent-custody verify receipts/<id>.json --issuer-key keys/app.pub --log log.jsonl    # the same check from the shell

4. Record what the agent now believes, citing the receipt. The ledger is bitemporal: it knows when a fact was true and when the agent learned it. A belief points back at the receipt that produced it.

ts
import { receiptIdOf } from "@agent-custody/receipts";
import { Ledger } from "@agent-custody/state";

const ledger = new Ledger("./ledger.jsonl"); // or "./ledger.sqlite", or "postgres://…"
const belief = await ledger.assert({ subject: "acct:42", predicate: "plan", value: customer.plan, space: "org", actor: "support-bot", source: { receiptId: receiptIdOf(bundle) } });

5. When a belief is wrong, undo it without losing the record. Retract removes it from the present, keeps it visible to questions about the past, and restores whatever it had superseded. The receipt id says exactly which call produced the bad belief.

ts
await ledger.retract({ factId: belief.fact.factId, actor: "user:admin", reason: "CRM lookup returned a stale plan" });
await ledger.asOf({ subject: "acct:42" });                                                  // [] now
await ledger.asOf({ subject: "acct:42", validAt: earlier, txAt: earlier });                  // still shows what was believed then
await ledger.history(belief.fact.factId);                                                    // assert, retract, with actors and reasons

This whole loop is one runnable file, packages/state/examples/02-receipt-to-belief.ts, executed by the test suite.

Where to go next

  • Enforce instead of record: put the gateway between the agent and its MCP tools. The gateway
  • Hook an existing framework, or another language. The interceptor SDK
  • Write policies, for tools and for memory, with tested examples. Policies, and what a verified receipt proves: Verification
  • Put beliefs in the ledger. State