# PromptLedger

> Token counters tell you how much you spent. ContextGuard tells you if it was worth it. **PromptLedger proves what happened.**

Tamper-evident audit trail for LLM calls. Hash-chained logging with verification, compliance export, and SDK middleware.

## The Problem

AI teams log LLM calls to flat files, databases, or observability platforms. None of them can answer: **"Has anyone modified a recorded response after the fact?"**

When a security analyst flags a CRITICAL finding but someone changes the stored output to LOW, regular logs won't catch it. PromptLedger will — every entry is SHA-256 hashed and chained to the previous entry. Modify anything, and the chain breaks.

**PromptLedger proved it**: In the tamper detection demo, an attacker modifies a stored severity rating from CRITICAL to LOW. Verification instantly detects the broken hash chain and pinpoints the exact tampered entry.

## The Solution

Treat LLM audit trails as a **ledger**, not a log. Three capabilities:

- **Record** — Append-only hash-chained entries for every LLM call
- **Verify** — Chain integrity check from genesis to head in milliseconds
- **Export** — Compliance-ready JSON with verification proof and statistics

## Quick Start

```bash
# See the tamper detection demo (records → verifies → tampers → re-verifies)
npx promptledger scenario

# Verify your ledger chain
npx promptledger verify

# Chain statistics
npx promptledger stats

# Recent entries
npx promptledger recent 20

# Export full ledger for compliance
npx promptledger export audit-2026-Q1.json

# JSON output for piping
npx promptledger verify --json
```

## Programmatic Usage

### Record LLM Calls

```typescript
import { LedgerEngine } from 'promptledger';

const ledger = new LedgerEngine({
  agent: 'claims-processor',
  sessionId: 'session-001',
});

// Record any LLM call
const entry = ledger.record({
  provider: 'anthropic',
  model: 'claude-sonnet-4-6',
  operation: 'chat',
  input: [
    { role: 'system', content: 'You are a claims analyst.' },
    { role: 'user', content: 'Review this medical evidence.' },
  ],
  output: [
    { role: 'assistant', content: 'Based on the evidence, I found...' },
  ],
  tokens: { input: 1250, output: 890 },
  latencyMs: 1340,
  tags: ['claims', 'medical-review'],
});

console.log(entry.hash);     // SHA-256 hash of this entry
console.log(entry.sequence);  // Position in chain
```

### Verify Chain Integrity

```typescript
const result = ledger.verify();

console.log(result.valid);          // true — chain intact
console.log(result.entriesChecked); // 147
console.log(result.headHash);       // Current chain head
console.log(result.verificationMs); // 12ms for 147 entries

// If tampered:
// result.valid === false
// result.brokenAt === 42
// result.brokenReason === "Entry 42 hash mismatch — content was modified"
```

### SDK Middleware (Anthropic)

```typescript
import Anthropic from '@anthropic-ai/sdk';
import { LedgerEngine, createAnthropicWrapper } from 'promptledger';

const client = new Anthropic();
const ledger = new LedgerEngine({ agent: 'my-app' });
const wrap = createAnthropicWrapper({ ledger, provider: 'anthropic' });

// Wrap any SDK call — automatically recorded to ledger
const request = {
  model: 'claude-sonnet-4-6',
  max_tokens: 1024,
  messages: [{ role: 'user', content: 'Hello' }],
};

const { response, entryId, entryHash } = await wrap(
  () => client.messages.create(request),
  request,
);

// response = normal Anthropic response
// entryId = UUID of the ledger entry
// entryHash = SHA-256 hash for this call
```

### SDK Middleware (OpenAI)

```typescript
import OpenAI from 'openai';
import { LedgerEngine, createOpenAIWrapper } from 'promptledger';

const client = new OpenAI();
const ledger = new LedgerEngine({ agent: 'my-app' });
const wrap = createOpenAIWrapper({ ledger, provider: 'openai' });

const request = {
  model: 'gpt-4o',
  messages: [{ role: 'user', content: 'Hello' }],
};

const { response, entryId } = await wrap(
  () => client.chat.completions.create(request),
  request,
);
```

### Export for Compliance

```typescript
const exportData = ledger.export();

// exportData includes:
// - version: '1.0'
// - verification: { valid: true, entriesChecked: 147, ... }
// - stats: { totalEntries: 147, totalTokens: { ... }, ... }
// - entries: [ ... all entries ... ]

// Write to file
import { writeFileSync } from 'fs';
writeFileSync('audit-export.json', JSON.stringify(exportData, null, 2));
```

### Query & Filter

```typescript
// By agent
const agentCalls = ledger.query({ agent: 'claims-processor' });

// By model
const claudeCalls = ledger.query({ model: 'claude-sonnet-4-6' });

// By time range
const todayCalls = ledger.query({
  since: '2026-04-01T00:00:00Z',
  until: '2026-04-02T00:00:00Z',
});

// Combined
const recent = ledger.query({
  agent: 'security-analyst',
  operation: 'chat',
  limit: 50,
});
```

## How It Works

```
Entry #0 (Genesis)              Entry #1                       Entry #2
┌─────────────────────┐   ┌─────────────────────┐   ┌─────────────────────┐
│ previousHash: 000…  │   │ previousHash: a3f…  │   │ previousHash: 7d2…  │
│ input: [...]        │   │ input: [...]        │   │ input: [...]        │
│ output: [...]       │   │ output: [...]       │   │ output: [...]       │
│ tokens: {in, out}   │   │ tokens: {in, out}   │   │ tokens: {in, out}   │
│ hash: a3f…     ─────┼──→│ hash: 7d2…     ─────┼──→│ hash: e91…          │
└─────────────────────┘   └─────────────────────┘   └─────────────────────┘

If Entry #1 output is modified after recording:
  → Recomputed hash ≠ stored hash → CHAIN BROKEN at #1
  → Entry #2 previousHash ≠ Entry #1 new hash → CHAIN BROKEN at #2
```

Each entry's hash covers: `sequence + previousHash + timestamp + provider + model + operation + input + output + tokens`. Change any field and the hash changes. The chain catches both content tampering and entry reordering.

## CLI Reference

```
promptledger scenario              Tamper detection demo
promptledger verify                Verify chain integrity (exit code 0=intact, 1=broken)
promptledger stats                 Chain statistics
promptledger recent [N]            Most recent N entries (default: 10)
promptledger export [file]         Export ledger as JSON (default: promptledger-export.json)
promptledger --help                Help

Options:
  --db <path>      Database path (default: ~/.promptledger/ledger.db)
  --json           JSON output
```

## Storage

PromptLedger uses SQLite (via `better-sqlite3`) for storage:

- Default location: `~/.promptledger/ledger.db`
- WAL mode for concurrent read performance
- Indexed on: sequence, timestamp, agent, session_id, model
- Override with `--db <path>` (CLI) or `storagePath` option (API)
- Use `:memory:` for testing / ephemeral ledgers

## Compliance Mapping

| Requirement | Framework | How PromptLedger Addresses It |
|------------|-----------|-------------------------------|
| Traceability | EU AI Act Art. 12 | Every LLM call recorded with full input/output |
| Logging & Monitoring | NIST AI RMF MG-2.2 | Append-only ledger with chain verification |
| Audit Trail | ISO 42001 A.6.2.6 | Hash-chained entries with tamper detection |
| Audit Record Content | NIST 800-53 AU-3 | Structured records: who, what, when, tokens |
| Audit Record Integrity | NIST 800-53 AU-10 | SHA-256 hash chain prevents undetected modification |
| Non-repudiation | NIST 800-53 AU-10(2) | Chain verification proves entry authenticity |

## License

MIT

---

Built by [ACE](https://aceadvising.com) — Advanced Consulting Experts

*Regular logs tell you what happened. PromptLedger proves nobody changed the story.*
