A tamper-evident audit trail for an agent in ten minutes
A worked example to follow from an empty folder: an agent with three tools, a signed record of every tool call, a check of those records, and the files an auditor needs. Python and TypeScript.
Everything up to step 5 runs on your machine and sends nothing. The outputs on this page are from a local run of exactly this code; only the folder path was shortened.
1. Install #
Python 3.9 or later, or Node.js 18 or later. In an empty folder:
# Python
python3 -m venv .venv && .venv/bin/python -m pip install signafaide
# TypeScript
npm init -y && npm pkg set type=module && npm install signafaide tsx2. Write the agent #
A claims agent with three tools: it reads a claim, scores it for fraud and approves the payout. A fixed plan stands in for the model, so the example runs with no model key. In your own agent the model chooses which tool to call, and the two signafAIde lines are the same.
Save this as agent.py or agent.ts:
Python
import signafaide
def retrieve_claim(claim_id: str) -> dict:
return {"claim_id": claim_id, "amount": 180.0, "policy": "home-contents"}
def fraud_score(claim_id: str, amount: float) -> float:
return 0.07
def approve_payout(claim_id: str, amount: float) -> dict:
return {"claim_id": claim_id, "approved": True, "amount": amount}
retrieve_claim, fraud_score, approve_payout = signafaide.wrap_tools(
[retrieve_claim, fraud_score, approve_payout], agent="claims-bot"
)
signafaide.declare_tools(["retrieve_claim", "fraud_score", "approve_payout"], agent="claims-bot")
def run(claim_id: str) -> dict:
# A fixed plan stands in for the model, so this runs with no model key.
claim = retrieve_claim(claim_id)
score = fraud_score(claim_id, claim["amount"])
if score < 0.5:
return approve_payout(claim_id, claim["amount"])
return {"claim_id": claim_id, "approved": False}
if __name__ == "__main__":
print(run("CLM-1042"))TypeScript
import { declareTools, wrapTools } from "signafaide";
const tools = wrapTools(
{
retrieve_claim: (claimId: string) => ({ claimId, amount: 180, policy: "home-contents" }),
fraud_score: (claimId: string, amount: number) => 0.07,
approve_payout: (claimId: string, amount: number) => ({ claimId, approved: true, amount }),
},
{ agent: "claims-bot" },
);
declareTools(["retrieve_claim", "fraud_score", "approve_payout"], { agent: "claims-bot" });
// A fixed plan stands in for the model, so this runs with no model key.
function run(claimId: string) {
const claim = tools.retrieve_claim(claimId);
const score = tools.fraud_score(claimId, claim.amount);
if (score < 0.5) return tools.approve_payout(claimId, claim.amount);
return { claimId, approved: false };
}
console.log(run("CLM-1042"));Two lines do the work:
wrap_tools(Python) orwrapTools(TypeScript) returns copies of the tools that write a signeddecisionrecord before the tool runs and anoutcomerecord after it.agentnames the chain the records join.declare_toolsordeclareToolstakes the agent's full tool list and writes it into the same chain, so a reader can see which of the agent's tools are recorded.
3. Run it #
# Python
.venv/bin/python agent.py
# TypeScript
npx tsx agent.tsThe Python run prints one line from the SDK and then the result:
signafaide: recording locally in /home/dev/claims-agent/.signafaide (signed and chained; no API key, so nothing is sent). Run `signafaide init` to send hashes for co-signature and public timestamps, or set SIGNAFAIDE_API_KEY.
{'claim_id': 'CLM-1042', 'approved': True, 'amount': 180.0}The folder now has .signafaide/: the agent's key in agents/, and the records with their inputs and outputs in chain/. The folder carries its own .gitignore, so it stays out of version control. Back up agents/: the key in it never leaves your machine.
4. Verify #
# Python
.venv/bin/signafaide verify
# TypeScript
npx signafaide verifylocal state: /home/dev/claims-agent/.signafaide
1. Envelope schema PASS 10 records
2. Record hashes NOT CHECKED 0 records; 10 not yet shipped (no receipt); nothing co-signed yet
3. Client signatures PASS 10 records
4. Agent chains PASS 10 records
5. Merkle proofs NOT CHECKED 0 records; nothing co-signed yet
6. Batch hashes and signatures NOT CHECKED 0 batches; nothing co-signed yet
7. Batch chain NOT CHECKED 0 batches; nothing co-signed yet
8. Signed heads NOT CHECKED 0 heads; nothing co-signed yet
9. drand rounds NOT CHECKED 0 rounds
10. Anchors NOT CHECKED 0 anchors; local state holds no anchors
11. OpenTimestamps NOT CHECKED 0 proofs
12. Content hashes PASS 10 records
coverage: _pending/nokey/claims-bot: all 3 declared tools are recorded
checked here: signatures, chain links and content hashes of all 10 records, using this machine's keys
co-signature, public timestamp: not yet. Run `signafaide init` to send hashes, or self-host (SIGNAFAIDE_URL)
PASS: 4 of 12 checks passed, 8 not checked (nothing co-signed yet)How to read it:
- Ten records. Four are the tool list (the list and each of the three tools). Six are the run: a decision and an outcome for each of the three tool calls.
- Four checks passed. The envelope, the agent's signatures, the chain and the content hashes can be checked on this machine.
- Eight checks were not run. They need a witness's co-signature and a public timestamp, which do not exist yet. A check that was not run is never counted as passed.
- The coverage line. All three declared tools are recorded. It is a statement to read, not a pass or a fail.
Add a tool to the list without wrapping it, for example send_notification, run the agent again, and the coverage line names it:
coverage: _pending/nokey/claims-bot: 1 declared tool is not recorded: send_notificationThat is the limit of recording, shown in the records themselves: a tool that is not wrapped leaves no record of its calls, and a tool the program never declared does not appear at all.
5. Send hashes to a witness #
Up to here nothing has left your machine, and a person who holds the agent's key could rewrite the chain and sign it again. A witness removes that: it co-signs each batch and fixes it in time. This step is a decision for a person, and it is the first one that sends anything.
# Python
.venv/bin/signafaide init
# TypeScript
npx signafaide initsignafaide init creates a free sandbox with no account, email or card, saves SIGNAFAIDE_API_KEY to .env, adds .env to .gitignore and prints a link to claim the sandbox, with how many days an unclaimed sandbox is kept. Claiming is free and needs no card: the sandbox moves into your own organisation and stops expiring. From then on each record sends its salted content hash with the tool name, the agent name and the time. Inputs and outputs stay in .signafaide/. The ten records written before ship with their original times, because they are less than 7 days old.
Run signafaide verify again afterwards and the witness's checks run too. The public timestamp is made every hour and usually confirms within a few hours; until then that check shows as pending, never as passed. To keep the evidence in your own environment, self-host the witness instead.
This page stops short of running init, so it shows no output for this step or the next.
6. Export for an auditor #
An auditor needs three files and one command.
signafaide export -o pack.json
signafaide keys export -o agent-keys.jsonpack.jsonis the verification pack: the records with their receipts, for every agent in the project.--agent,--fromand--tonarrow it. It needs the API key from step 5; without one the command stops and tells you to runsignafaide init.agent-keys.jsonholds the public half of every agent key, each signed by itself. It must come from you, not from the pack, so that the service cannot add a key of its own.head.jsonis your.signafaide/head.json, the newest receipt for each agent. It shows that no record was cut from the end.
The key export works before init and prints the command the auditor runs:
wrote agent-keys.json (1 key); give it to your auditors with pack.json and head.json: signafaide verify pack.json --keys <witness>/.well-known/signafaide-keys.json --agent-keys agent-keys.json --head head.jsonThe auditor runs that on their own machine. Verification is never metered. The console can also give an auditor a read-only link or a printable report: see Auditors and compliance and Verification packs.
What this proves, and what it does not #
- It proves integrity and timing. Who signed each record, that it has not changed, and the window of time in which it came to exist.
- It does not judge the decision. A record says the agent approved the payout, not that it should have. Records are evidence, not certification.
- It covers what goes through it. The three wrapped tools are recorded. A call made around the wrapped copies leaves no record. For MCP servers, the gateway records every call that passes through it; see Tool coverage.
Next steps: the production guide for keys, containers and CI, and the page for your framework under For your framework.