Audkit

What is Audkit?

Tamper-evident audit logging for TypeScript apps and AI agents — every event sequenced and committed to an RFC 6962 Merkle tree before the API answers.

Audkit is tamper-evident audit logging for TypeScript apps and AI agents.

Every event is sequenced and committed as a leaf of your project's RFC 6962 Merkle tree, whose root is Ed25519-signed and published hourly to a public transparency log. There is no API to edit or delete a record — and if anyone rewrites history out-of-band (a rogue DBA, a doctored backup), verify() rebuilds the tree and names the exact sequence where it breaks.

Agents act; someone has to be able to prove what they did

An AI agent that can refund a payment, change a role, or delete a record is a principal in your system with no HR file and no memory. When it goes wrong, "the model decided to" is not an answer a regulator, a customer, or your own incident review will accept.

Wrap the tool and every call an agent makes enters the record — with the risk label, the model-supplied reason, and whether your authorization gate let it through:

tools/refund.ts
import { auditedTool } from "audkit/ai";

const refund = auditedTool({
  name: "refund_payment",
  inputSchema: refundSchema,
  risk: "high",
  requireReason: true,
  authorize: ({ input }) => input.amountCents <= 50_00,
  handler: ({ paymentId }) => payments.refund(paymentId),
});

A call that succeeds logs success. A call the gate refuses logs denied — and then throws. A call that arrives without a reason logs failed before the error reaches the model. The denial is evidence, not a silent no.

Not just agents

Audkit is a general audit log. auditedTool() is one of four entry points, alongside withAuditLogging() and withAuditAction() for Next.js, and audit.log() for everything else.

Sealed before the ack

Most audit pipelines answer 202 Accepted and then queue the write. If the queue drops the message, reorders it, or the worker crashes, the "accepted" event was never a fact.

Audkit seals inline. By the time log() resolves, the event has already been:

  1. assigned a per-project sequence under the project row lock,
  2. encrypted, with a blinding nonce sealed inside the payload blob,
  3. committed as an RFC 6962 leaf appended to the project's Merkle tree in Postgres.
const { id, status } = await audit.log({ action: "invoice.approved", actor });
// status === "sealed"

status: "sealed" is a promise about durable state, not about a queue. Only order-independent mirror work — analytics counts, realtime broadcast, alert-rule evaluation, usage metering — runs after the response, and none of it can change the sequence or the tree.

What makes it evidence

PropertyHow
Append-onlyNo edit or delete endpoint exists. Verification rebuilds the tree from row content.
Order-fixedSequence assigned under a row lock before the response, so arrival order is sequence order.
Third-party checkableReceipts and tree heads are Ed25519-signed by the service key, not HMAC'd.
Checkable without trusting usClients verify RFC 6962 inclusion/consistency proofs with local hash arithmetic.
Unforgeable by the platformWith customer-held signing keys, events are signed before they leave your infrastructure.
Externally anchoredSigned tree heads are published hourly to Sigstore Rekor, which Audkit cannot edit.
Privacy-compatibleRetention crypto-shreds expired payloads; the leaf skeleton and its proofs still verify.

Tamper-evidence is deliberately out-of-band: nothing in the API lets history be rewritten, so what verify() catches is a direct-database forgery — and what an anchored root catches is the platform itself.

Where to go next

On this page