Skip to main content

Verifiable, tamper-evident receipts for RAG and agent answers.

Project description

answerproof

Verifiable, tamper-evident receipts for RAG and agent answers.

CI Python License

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

answerproof-0.1.0.tar.gz (25.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

answerproof-0.1.0-py3-none-any.whl (22.5 kB view details)

Uploaded Python 3

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

Hashes for answerproof-0.1.0.tar.gz
Algorithm Hash digest
SHA256 6f362510a6bdd9459a7d4ec5e07d0893ca5695c18a35f71ddde516052f875b5f
MD5 ec70940744438e066fa5137c53e6ec68
BLAKE2b-256 5e109b28b628beb5eb564502e42e9c36a4d339e47adea63807cfb57747d12de5

See more details on using hashes here.

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

Hashes for answerproof-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3b2497eae501ffb8d751f362a102f33d3a458225a3fe6605b1baaffcba43caa1
MD5 08a83780e6196c79f56dfa25ef35e69e
BLAKE2b-256 fdda0f19a894d3dcc7d89fbe0c4bab2f5d72e54ae419295350fc9abe02ed099a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page