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.
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".
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.
pip install answerproof # core library + CLI
pip install "answerproof[api]" # + FastAPI verifier servicefrom 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.validThe 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.
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 |
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).passedLeaves and internal nodes use domain-separated hashing (0x00 / 0x01
prefixes) and odd nodes are promoted rather than duplicated, closing common
Merkle forgery vectors.
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", ...}.
pip install "answerproof[api]"
uvicorn answerproof.api:app --reloadGET /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.
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()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-verifyIssues 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.
MIT © royalpinto007. See LICENSE.
{ "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 } }