Verifiable, tamper-evident receipts for RAG and agent answers.
Project description
answerproof
Verifiable, tamper-evident receipts for RAG and agent answers.
answerproof attaches a cryptographically-signed audit trail to every generated answer: which sources were retrieved, which the answer actually used, under whose permissions, with which model and parameters, plus a content hash of each source and a Merkle root over the retrieval set. Anyone can later verify a receipt independently, with nothing but the receipt and the library.
It is a real library and verifier, not a wrapper around a model.
Why
RAG and agent systems make claims. When something goes wrong (a leaked document, a hallucinated fact, a compliance question three months later) the usual answer is "trust the logs". Logs are mutable, centrally held, and prove nothing to an outside party.
A receipt is different: it is a signed, self-contained artifact that a third party can check without access to your database, your logs, or your servers. It turns "trust us" into "verify it yourself".
Threat model — what a receipt does and does not prove
A receipt proves:
- Integrity. The query, answer, sources, permissions, model and parameters have not been altered since signing. Any mutation breaks the Ed25519 signature.
- Source authenticity. Each source's content hash is recorded; given the original content, anyone can confirm it is byte-for-byte what was retrieved.
- Set membership. The Merkle root lets you prove a specific source was part of the retrieval set (via an inclusion proof) without revealing the others.
- Provenance of signer. The signature binds the receipt to a specific Ed25519 key; pin that key and you know who issued it.
- Grounding evidence. A transparent, rule-based record of which answer claims are supported by which sources, and which are unsupported.
A receipt does not prove:
- Truth. A well-grounded claim can still be wrong if the source is wrong.
- Semantic correctness of grounding. Citation binding is n-gram overlap; it can miss a correct paraphrase or accept a coincidental lexical match. It is an auditable signal, not a judge.
- Key trust. answerproof verifies signatures; it does not manage a PKI. You decide which public keys you trust.
- That retrieval was complete or unbiased. It records what was retrieved, not what should have been.
Install
pip install answerproof # core library + CLI
pip install "answerproof[api]" # + FastAPI verifier service
Quickstart
from answerproof import ReceiptBuilder, SigningKey, verify_receipt
signing_key = SigningKey.generate()
builder = ReceiptBuilder(signing_key)
builder.set_query("How tall is the Eiffel Tower?")
builder.set_answer("The Eiffel Tower is 330 metres tall.")
builder.set_principal("analyst-7", permissions=["kb:paris"], tenant="acme")
builder.set_model("gpt-x", provider="openai", params={"temperature": 0.0})
builder.add_source("doc-1", content="The Eiffel Tower is 330 metres tall.", score=0.92)
receipt = builder.finalize()
print(receipt.to_json())
# Independent verification — needs only the receipt (+ optional source contents).
verdict = verify_receipt(
receipt,
source_contents={"doc-1": "The Eiffel Tower is 330 metres tall."},
)
assert verdict.valid
Receipt anatomy
{
"payload": {
"schema_version": "1.0",
"receipt_id": "…",
"created_at": "2026-07-27T12:00:00Z",
"query": "How tall is the Eiffel Tower?",
"answer": "The Eiffel Tower is 330 metres tall.",
"principal": { "id": "analyst-7", "permissions": ["kb:paris"], "tenant": "acme" },
"model": { "name": "gpt-x", "provider": "openai", "params": { "temperature": 0.0 } },
"sources": [
{ "id": "doc-1", "content_hash": "sha256:…", "score": 0.92, "metadata": {} }
],
"cited_source_ids": ["doc-1"],
"merkle_root": "…", // Merkle root over source content hashes
"citations": [ { "source_id": "doc-1", "quote": "…", "method": "3gram-overlap", "score": 1.0 } ],
"grounding": { "claims": [ … ], "unsupported_claims": [ … ], "grounding_score": 1.0 }
},
"signature": {
"algorithm": "ed25519",
"public_key": "…", // base64 url-safe raw public key
"signature": "…" // detached signature over canonical payload
}
}
The payload is signed; the signature envelope carries the detached Ed25519 signature and the signer's public key. Source contents are never stored in the receipt, only their hashes.
Verify — including tamper detection
import json
from answerproof import verify_receipt
from answerproof.schema import Receipt
# ... start from a valid `receipt` ...
tampered = json.loads(receipt.to_json())
tampered["payload"]["answer"] = "The Eiffel Tower is in Berlin."
bad = Receipt.from_json(json.dumps(tampered))
verdict = verify_receipt(bad)
assert not verdict.valid
print(verdict.failures()[0].name) # -> "signature"
Every check is independent and reported separately:
| check | proves |
|---|---|
signature |
payload is unmodified and signed by the embedded key |
merkle |
recomputed Merkle root matches the signed root |
sources |
supplied source contents hash to the recorded hashes |
grounding |
citations reference real sources; grounding score is honest |
signer_pin |
(optional) signer public key matches an expected key |
Merkle inclusion proofs
Prove one source was in the retrieval set without revealing the rest:
from answerproof.merkle import MerkleTree
from answerproof.verifier import verify_inclusion
hashes = [s.content_hash for s in receipt.payload.sources]
proof = MerkleTree.from_hashes(hashes).proof(0)
assert verify_inclusion(receipt, receipt.payload.sources[0].id, proof).passed
Leaves and internal nodes use domain-separated hashing (0x00 / 0x01
prefixes) and odd nodes are promoted rather than duplicated, closing common
Merkle forgery vectors.
CLI
answerproof keygen # print a keypair as JSON
answerproof keygen --out id.key # write id.key and id.key.pub
answerproof verify receipt.json # verify (exit 0 = valid, 1 = invalid)
answerproof verify receipt.json --sources sources.json --json
answerproof verify receipt.json --expect-key <base64> # pin the signer
answerproof inspect receipt.json # human-readable summary
--sources takes a JSON object of {"source_id": "original content", ...}.
FastAPI verifier service
pip install "answerproof[api]"
uvicorn answerproof.api:app --reload
GET /health— liveness.POST /verify— body{ "receipt": {...}, "source_contents": {...}, "expected_public_key": "..." }, returns a JSON verdict.POST /verify/page— same body, returns a shareable HTML verification page.
The core library works fully standalone; the service is optional.
Integration snippet
Drop answerproof into an existing RAG pipeline at the seam between retrieval and generation:
def answer_with_receipt(query, retriever, llm, signing_key, principal):
hits = retriever.search(query, k=5)
answer = llm.generate(query, context=[h.text for h in hits])
builder = ReceiptBuilder(signing_key)
builder.set_query(query).set_answer(answer)
builder.set_principal(principal.id, permissions=principal.scopes)
builder.set_model(llm.name, provider=llm.provider, params=llm.params)
for h in hits:
builder.add_source(h.id, content=h.text, score=h.score)
return answer, builder.finalize()
Run it locally
git clone https://github.com/royalpinto007/answerproof
cd answerproof
pip install -e ".[dev]"
pytest -q # full test suite
python examples/demo_rag.py # produce + verify a receipt, then tamper and re-verify
Contributing
Issues and PRs welcome. Please run ruff check . and pytest before opening a
PR; CI runs both on Python 3.11 and 3.12. New cryptographic or Merkle behavior
must ship with tests, including a negative (tamper) case.
License
MIT © royalpinto007. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file answerproof-0.1.0.tar.gz.
File metadata
- Download URL: answerproof-0.1.0.tar.gz
- Upload date:
- Size: 25.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6f362510a6bdd9459a7d4ec5e07d0893ca5695c18a35f71ddde516052f875b5f
|
|
| MD5 |
ec70940744438e066fa5137c53e6ec68
|
|
| BLAKE2b-256 |
5e109b28b628beb5eb564502e42e9c36a4d339e47adea63807cfb57747d12de5
|
File details
Details for the file answerproof-0.1.0-py3-none-any.whl.
File metadata
- Download URL: answerproof-0.1.0-py3-none-any.whl
- Upload date:
- Size: 22.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3b2497eae501ffb8d751f362a102f33d3a458225a3fe6605b1baaffcba43caa1
|
|
| MD5 |
08a83780e6196c79f56dfa25ef35e69e
|
|
| BLAKE2b-256 |
fdda0f19a894d3dcc7d89fbe0c4bab2f5d72e54ae419295350fc9abe02ed099a
|