Witnessed
Integrates with Google Cloud KMS for Ed25519 signing, enabling production-grade key management for witness signatures.
Allows verifying email message IDs (anchor type 'email.message_id') via the Mailgun Events API, enabling proof of email sending.
Allows verifying payment transaction IDs (anchor type 'payment.txn_id') via the Stripe API, enabling proof of payment settlement.
Witnessed — verifiable action receipts for AI agents
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 hostLive 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) exposesverify_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/mcpruns a stdio MCP server that issues receipts signed with the agent's own key (toolsissue_receipt,verify_receipt). Point any MCP host at it.Published packages:
@witnessed/sdk,@witnessed/verifier,@witnessed/core,@witnessed/mcpon npm.Witness public key (build your trusted-key set from this to verify receipts offline):
key_id:zNo0zXMkkRwNUbNqtHj6diky8Nd9SvMZ8i7m6F99oFEpublic_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: Agent Receipts
Packages
Package | Responsibility |
| Types, JCS canonicalization, Ed25519 keys/crypto, claim building & signing |
| Pure, offline receipt verification (reused client- and server-side) |
| In-memory + durable SQLite stores, signer, anchor validators, witness logic, Hono HTTP service |
|
|
| MCP server exposing |
| Production adapter: a |
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 :8787Container / 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 deployPublish 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
Signerinterface (async).LocalSignerholds a local key (dev only);KmsSignerdelegates to a KMS/HSM via an injectedKmsSignFnso the witness key never enters the process. Wire it withcreateApp({ 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 iscreateGcpKmsClient; 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:
SqliteStorepersists receipts append-only (PRIMARY KEY onid); inject it viacreateApp({ ..., store }). Postgres can implement the sameReceiptStore.Record-keeping (
RetentionStore): aRetentionPolicy { minimumDays }(EU AI Act ≈ 180 days) sets each record'sretain_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 isformat: "...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 unpaidPOST /receiptsreturns HTTP402with payment requirements (USDC, bound to the claim id so a payment can't be replayed); a paid request settles via the injectedPaymentFacilitatorand the settlement (tx_hash) is recorded as witness-signedwitness.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 theX-PAYMENTpayload, callsPOST /verifythenPOST /settle, and maps the returnedtransactionto the receipt'stx_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 pluggableAnchorValidator(given the anchor value + the claim'spayload_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'spayload_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), andfhirEhrLookup(FHIR REST + afhirContentHashprovenance reducer). Each takes an injectedfetchfor tests; live network/auth runs against the real provider.Evidence chains: a receipt's
prevlists predecessor receipt ids. Becauseidis a content hash, aprevlink 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 andprev-linked to the action under review — so oversight lands in the same evidence chain. Useclient.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.)
This server cannot be installed
Maintenance
Latest Blog Posts
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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