Skip to main content
Glama

Witnessed — verifiable action receipts for AI agents

CI npm @witnessed/sdk license

An agent signs a claim about a consequential action with its own key; a witness service independently timestamps and countersigns it; the receipt is stored and retrievable; and anyone can verify it offline with the witness's public key.

npm i @witnessed/sdk        # build it into your agent
npx @witnessed/mcp          # or run a local MCP server for any agent host

Live at https://witness.medtekki.no. Design and plan in docs/superpowers/.

Live beta

  • Endpoint: https://witness.medtekki.no (/healthz, /public-key, POST /receipts, GET /receipts/:id, POST /verify)

  • Agent discovery: the service describes itself for agents at GET / and /.well-known/receipts.json (JSON manifest), /llms.txt, and /openapi.json.

  • Hosted MCP: https://witness.medtekki.no/mcp (Streamable HTTP) exposes verify_receipt, get_receipt, service_info. Issuance is not hosted (it needs client-side signing) — issue with the SDK or the local MCP CLI.

  • Local MCP CLI: npx @witnessed/mcp runs a stdio MCP server that issues receipts signed with the agent's own key (tools issue_receipt, verify_receipt). Point any MCP host at it.

  • Published packages: @witnessed/sdk, @witnessed/verifier, @witnessed/core, @witnessed/mcp on npm.

  • Witness public key (build your trusted-key set from this to verify receipts offline):

    • key_id: zNo0zXMkkRwNUbNqtHj6diky8Nd9SvMZ8i7m6F99oFE

    • public_key: {"kty":"OKP","crv":"Ed25519","x":"oKDkj9ODRDR8ASpYs8pKALb0AnwS6u1j7WyivK9UpM4"}

Beta caveats: receipts are free/unbilled; the signing key is host-held (not yet KMS); treat as a labelled beta, not a compliance guarantee.

Related MCP server: DCL Evaluator

Packages

Package

Responsibility

@witnessed/core

Types, JCS canonicalization, Ed25519 keys/crypto, claim building & signing

@witnessed/verifier

Pure, offline receipt verification (reused client- and server-side)

@witnessed/witness

In-memory + durable SQLite stores, signer, anchor validators, witness logic, Hono HTTP service

@witnessed/sdk

ReceiptsClient — local signing, issue via witness, offline verify

@witnessed/mcp

MCP server exposing issue_receipt / verify_receipt tools

@witnessed/gcp-kms

Production adapter: a KmsSignFn backed by Google Cloud KMS (Ed25519)

What a receipt proves

Baseline (no anchor):

Agent K asserted action A over content-digest D at time T, independently witnessed at time T′, and the record is unaltered since.

With an anchor (effect-binding), the agent attaches an external reference such as an email Message-ID, and the witness independently validates it, adding a signed anchor_check:

...and the witness confirmed, at time T′, that the claimed external effect (Message-ID X) actually exists at the provider.

A failed or unvalidated anchor is recorded as verified: false, never rejected — receipts never gate the underlying action. Verifiers surface anchor_check so a consumer can choose to trust only effect-proven receipts.

Develop

Requires Node 22+ and npm (npm workspaces).

npm install
npm run typecheck   # tsc --noEmit across all packages + tests
npm test            # full Vitest suite
npm run check       # typecheck + test (the CI gate; see .github/workflows/ci.yml)

Deploy (beta)

The witness is a standard HTTP service (/healthz, /public-key, POST /receipts, GET /receipts/:id, POST /verify).

npm run gen:witness-key          # prints WITNESS_PRIVATE_JWK (keep secret) + the public key
cp .env.example .env             # set WITNESS_PRIVATE_JWK (and optional store / x402 vars)
npm run start:witness            # listens on :8787

Container / Fly.io (a Dockerfile and fly.toml are included):

fly launch --no-deploy
fly secrets set WITNESS_PRIVATE_JWK='...'   # value from gen:witness-key
fly volumes create receipts_data --size 1
fly deploy

Publish GET /public-key so verifiers can build their trusted-key set and check receipts offline. Beta note: an env-held private key is acceptable for a labelled beta; move signing to a KMS/HSM (@witnessed/gcp-kms + KmsSigner) before charging money or handling regulated data.

Trust model

  • The agent's private key never leaves the client; only the canonical digest and the agent signature go to the witness.

  • The witness signing key is pluggable via the Signer interface (async). LocalSigner holds a local key (dev only); KmsSigner delegates to a KMS/HSM via an injected KmsSignFn so the witness key never enters the process. Wire it with createApp({ signer, witnessPublicJwk, ... }). (Ed25519 signing required — works with GCP Cloud KMS / HSMs that support Ed25519; AWS KMS managed keys do not.)

    Production wiring with @witnessed/gcp-kms (the only module touching the GCP SDK is createGcpKmsClient; the adapter logic is injected-client + CRC32C-verified):

    import { createGcpKmsClient, gcpKmsSignFn, gcpKmsPublicJwk } from "@witnessed/gcp-kms";
    import { KmsSigner } from "@witnessed/witness/src/signer";
    
    const client = createGcpKmsClient();
    const keyVersion = "projects/P/locations/L/keyRings/R/cryptoKeys/witness/cryptoKeyVersions/1";
    const { publicJwk, keyId } = await gcpKmsPublicJwk(keyVersion, client);
    const app = createApp({
      signer: new KmsSigner(keyId, gcpKmsSignFn(keyVersion, client)),
      witnessPublicJwk: publicJwk,
    });
  • The witness time is authoritative; the agent's claimed time is also retained.

  • Receipts carry content digests, not content — privacy by default.

  • Durable storage: SqliteStore persists receipts append-only (PRIMARY KEY on id); inject it via createApp({ ..., store }). Postgres can implement the same ReceiptStore.

  • Record-keeping (RetentionStore): a RetentionPolicy { minimumDays } (EU AI Act ≈ 180 days) sets each record's retain_until; purgeExpired(now) deletes only records past their window and not under hold, returning both what it purged and what a legal hold kept (placeLegalHold/releaseLegalHold) — no silent deletion. Records with no policy are kept indefinitely.

  • Article-12 export: buildArticle12Export(receipts, keys, opts) produces an ordered, integrity-verified event log (each receipt re-verified, chain resolved) with effect/anchor, actor, timestamps, and human-oversight flags. It is format: "...v0" and carries an explicit disclaimer that the field mapping is not legally validated — verify with counsel.

  • x402 billing (optional): createApp({ x402: { facilitator } }) makes the witness charge per receipt. An unpaid POST /receipts returns HTTP 402 with payment requirements (USDC, bound to the claim id so a payment can't be replayed); a paid request settles via the injected PaymentFacilitator and the settlement (tx_hash) is recorded as witness-signed witness.payment — so getting paid and proving it are one on-chain-verifiable artifact. The facilitator is provider-agnostic; HttpFacilitator (@witnessed/witness/src/x402-facilitator) is a real client for a hosted x402 facilitator — it decodes the X-PAYMENT payload, calls POST /verify then POST /settle, and maps the returned transaction to the receipt's tx_hash (transport injected for tests; live chain settlement runs against a real facilitator).

  • Effect-binding: agents attach an anchor { type, value }; the witness runs a pluggable AnchorValidator (given the anchor value + the claim's payload_digest) and signs the result. Built in:

    • EmailMessageIdValidator (email.message_id) — verified when the message is found.

    • PaymentTxnIdValidator (payment.txn_id) — verified when the transaction is found and settled.

    • EhrRecordIdValidator (ehr.record_id) — MDR-style provenance: verified when the record exists, is in an accepted status, and its content hash matches the receipt's payload_digest (proving the agent wrote exactly the data it claimed).

    Real provider lookups live in @witnessed/witness/src/anchor-lookups: mailgunEmailLookup (Mailgun Events API, by RFC Message-ID), stripePaymentLookup (Stripe PaymentIntents/Charges), and fhirEhrLookup (FHIR REST + a fhirContentHash provenance reducer). Each takes an injected fetch for tests; live network/auth runs against the real provider.

  • Evidence chains: a receipt's prev lists predecessor receipt ids. Because id is a content hash, a prev link pins the exact predecessor and is covered by the signatures, so tampering with any link breaks the chain. verifyChain() verifies every receipt and resolves every link.

  • Human oversight: a reviewer's decision is an ordinary receipt with action.type = human.approval / human.rejection, signed by the reviewer's own key and prev-linked to the action under review — so oversight lands in the same evidence chain. Use client.approve(id, reason) / client.reject(id, reason). (Binding a reviewer key to a real licensed human is a separate identity layer, intentionally out of scope.)

A
license - permissive license
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/medtekki/witnessed'

If you have feedback or need assistance with the MCP directory API, please join our Discord server