Quickstart
This walks through wrapping an LLM call so it emits a signed receipt, then verifying the chain offline.
1. Generate a signing key
npx receipta key gen --out keys/This writes keys/<key_id>.pub (the trusted public key) and prints the key's fingerprint. Verify the fingerprint on a second channel (e.g. confirm it matches what's in the repo's README) — this is the trust bootstrap. By default the private key is held in memory only and discarded; store it securely (env/KMS) for signing.
If you want to sign DSSE exports later, persist the private key too:
npx receipta key gen --out keys/ --out-private signing-key.json
# signing-key.json is mode 0600 — PROTECT THIS FILE.See the CLI reference for the full key gen contract.
2. Wrap your OpenAI calls
import OpenAI from 'openai';
import { withReceipts } from '@receipta/openai';
import { openStore, generateKeyPair } from '@receipta/core';
// Open (or create) a receipt store — a single append-only file holding one hash chain.
const store = await openStore('./receipts.log.receipta');
// Load your signing key (here generated in-memory; in practice load from env/KMS).
const signer = generateKeyPair();
// Construct a wrapped client — use it exactly as you would `new OpenAI(...)`.
const client = withReceipts(
OpenAI,
{ apiKey: process.env.OPENAI_API_KEY! },
{
store,
signer,
actor: { type: 'service', id: 'my-app', label: 'support-bot' },
},
);
// Every chat completion now emits a signed, hash-chained receipt:
const res = await client.chat.completions.create({
model: 'gpt-4o',
messages: [{ role: 'user', content: 'Summarize the quarterly report.' }],
});
console.log(res.choices[0]?.message?.content);Each call appends one receipt per HTTP attempt (including retries) to ./receipts.log.receipta. The response you receive is byte-identical to what you'd get without receipta — wrapping never changes the result.
3. Anthropic
import Anthropic from '@anthropic-ai/sdk';
import { withReceipts } from '@receipta/anthropic';
const client = withReceipts(
Anthropic,
{ apiKey: process.env.ANTHROPIC_API_KEY! },
{
store,
signer,
actor: { type: 'service', id: 'my-app' },
},
);
const res = await client.messages.create({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 1024,
messages: [{ role: 'user', content: 'Hello' }],
});4. Vercel AI SDK
import { registerTelemetry } from 'ai';
import { receiptaTelemetry } from '@receipta/vercel';
registerTelemetry(receiptaTelemetry({ store, signer, actor: { type: 'agent', id: 'my-agent' } }));
// every generateText/streamText now emits a receipt via the telemetry callback5. Verify the chain offline
npx receipta verify ./receipts.log.receipta --trust-root keys/On a valid chain:
✓ valid: 42 receipt(s) verified.Exit code is 0 on success, non-zero on any divergence, and 2 if the trust root can't be established. Verification needs no network — only the receipt file and the trusted public key.
6. Export for auditors
# OCSF (for SIEMs), JSON, or CSV — no re-signing, store never altered:
npx receipta export ./receipts.log.receipta --format ocsf --out receipts.ocsf.json
# in-toto Statement v1 per receipt (unsigned, pipeable into cosign/another signer):
npx receipta export ./receipts.log.receipta --format intoto
# DSSE envelope over each Statement, signed at export time with your key:
npx receipta export ./receipts.log.receipta --format dsse --key signing-key.jsonExports to JSON, CSV, OCSF, in-toto, or DSSE without re-signing — the store is never altered. The DSSE format signs a NEW envelope around each unmodified receipt with the key from --key (produced by receipta key gen --out-private). See the CLI reference for how a recipient verifies a DSSE envelope.
7. Watch a forged receipt fail
pnpm verify:demoBuilds a demo store, verifies it (passes), tampers one receipt's content, and re-verifies — showing the precise divergence (which receipt, which field, why).