Proof of what your AI agents did.

A gateway between an agent and its tools. It checks every call against a grant a person signed and a policy, forwards or denies it, and issues a signed receipt either way. Anyone with the public keys can verify the receipt, offline.

git clone https://github.com/svayatta/agent-custody && cd agent-custody && bun install && bun run demo

Requires Node 22 and Bun. Runs four calls through the gateway, three refunds and a payout, and verifies each receipt.

Watch the overview (1:32)
Receipt · stripe.refund
2026-10-19 10:31:23 UTC
Denied
Asked for
refund £50,000.00 to cust_123
Gateway checked
customer cust_123, verified true observed
Policy
refunds to £1,000 for a verified customer sha256 ba4e4461ffc4…
Decision
deny · no permit policy matched
Agent
support-agent attested
Authorized by
user_456 a grant they signed
Log
leaf 2 of 2 · root 0735e94bba4c…

Product overview

Narrated, with captions.

The problem

An agent's only record is the log its own process wrote. Nobody outside the team can check it.

Made-up authority

The agent refunds £50,000. Its log says the customer asked. Nobody signed anything that allowed it.

A log that can be edited

The team that ran the agent holds the log. After the incident, the entry that matters is gone or different.

Evidence nobody else can check

Traces sit in a vendor account. The auditor gets a screenshot and a promise.

The refund demo

bun run demo runs a support agent against a test Stripe server. Each call writes a signed receipt to your disk. Then the demo verifies the receipts and edits one.

> refund £500 to a verified customer
  EXECUTED {"refund_id":"re_b5db9a9e","customer_id":"cust_123","amount":50000,"status":"succeeded"}
  receipt a8094805-e7d0-48da-bf05-9dcfdfe7a1dd

--- a8094805-e7d0-48da-bf05-9dcfdfe7a1dd
PASS  receipt signature (issuer key)  (keyid 13221dd037cb)
PASS  delegation signature (principal key)  (signed by 0b715318f782)
PASS  policy decision consistent with execution  (allow -> executed)
PASS  log inclusion proof  (leaf 0 of 1, root 4901cb4cdc68)
… 15 more checks

RESULT: VERIFIED

ISSUER: gateway, enforced outside the agent's process; the agent could neither skip nor forge this receipt

field           provenance  value
principal       attested    user_456
agent           attested    support-agent
model           claimed     claude-fable-5-1
tool            observed    stripe.refund
args            claimed     {"amount":50000,"customer_id":"cust_123"}
fact.customer   observed    {"email":"alex@example.com","id":"cust_123","verified":true}
policy          observed    allow [policy1] policy ba4e4461ffc4
consumed        observed    (no facts shown before this call)
execution       observed    executed (carries an upstream signature; pass --upstream-key to check it)

Source: packages/receipts/scripts/demo.ts. Receipt ids change on every run.

How it works

Every call goes through the same five steps.

01

Authorize

A person signs a grant: which agent, which tools, for how long.

02

Gate

The gateway fetches the facts itself, evaluates the policy, and forwards or denies.

03

Record

One signed receipt per call: who authorized it, what was asked, what was checked, what happened.

04

Log

The receipt's hash goes to a Merkle log with signed heads. A rewrite is detectable by anyone who kept a checkpoint.

05

Verify

Anyone with the public keys checks a receipt, in the shell or the browser, offline.

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.

Policies

Written in Cedar and evaluated on facts the gateway fetches itself. Each receipt records the policy digest.

permit(principal, action == Action::"stripe.refund", resource)
when { context.args.amount <= 100000 };

A ceiling per call, in minor units. No floats, no rounding.

Nine more, each run by the test suite, are in Writing policies.

Receipts compared with logs and traces

Receipts export to OpenTelemetry and Splunk, so you keep your current tools.

QuestionApplication logTraceGateway receipt
Written bythe agent's processthe agent's processa gateway, with its own key
Allowed or denied before the tool ran—no—noyes, and a denial is a receipt
Who authorized it—not recorded—not recordedthe grant a person signed
A rewrite is detectable—no—noyes, by anyone who kept a checkpoint
Who can check itlog access holdersthe tracing accountanyone with the public keys

Measured on a laptop (lower is better)

0.10 msa policy decision
0.52 msa full gateway call
0.14 msverifying a receipt
5.7 KBa receipt; the log gets 32 bytes

Medians, Apple silicon, Node 22, from scripts/bench.ts. You can run it.

Use cases

Each one has a runnable example.

Payments and refunds

Refund within a limit, for a customer the gateway verified.

Agents that write code

Risk-tiered merges. Change-management evidence for SOC 2 and ISO 27001.

Knowledge bases

Read anything, write only under notes/, with a source.

Your own repository

Every Claude Code tool call as a receipt. This repository runs that way.

Run the demo

Requires Node 22 and Bun. No account needed.

git clone https://github.com/svayatta/agent-custody && cd agent-custody && bun install && bun run demo