chain-receipt-sdk
Emit, verify, replay, and chain Chain-Receipts (v1.0.0) from any
Python LLM-agent loop. Drop-in callback for LangChain, plus a
framework-agnostic ReceiptBuilder for raw API loops.
Verifiable record of every LLM call: emit a signed Receipt, replay it against any vendor, and check whether the chain holds.
pip install chain-receipt-sdk
chain-receipt verify sha256:<hash>
chain-receipt replay sha256:<hash> --n 10
60-second demo
from chain_receipt_sdk import ReceiptBuilder, ClientInfo, Interaction
from chain_receipt_core import compute_text_hash, compute_tool_calls_hash, generate_keypair
sk, pub = generate_keypair()
client = ClientInfo(
name="my-agent",
version="0.1.0",
platform="python-3.12",
emitter_pubkey=f"ed25519:{pub}",
)
b = ReceiptBuilder(client=client, private_key=sk, chain_seed=pub.encode())
inter = Interaction(
vendor="anthropic",
model="claude-sonnet-4-5",
temperature=0.0,
system_prompt_hash=compute_text_hash("you are helpful"),
prompt_hash=compute_text_hash("hello"),
response_hash=compute_text_hash("hi"),
tool_calls_hash=compute_tool_calls_hash([]),
n_tool_calls=0,
latency_ms=120,
)
r = b.build(interaction=inter)
print(r.receipt_id, r.chain.sequence_number)
LangChain callback
The ReceiptCallback is a BaseCallbackHandler — pass it via config={"callbacks": [cb]} to any LangChain LLM, chat model, or LCEL chain. One Receipt is emitted per on_llm_end / on_chat_model_end event; sequential calls chain automatically.
Minimal working example (zero-cost; uses LangChain's built-in fake LLM):
from langchain_core.language_models.fake import FakeListLLM
from chain_receipt_sdk.callback import ReceiptCallback
cb = ReceiptCallback(
client_name="my-langchain-agent",
vendor="anthropic",
model="claude-sonnet-4-5",
)
llm = FakeListLLM(responses=["the answer is 42"])
llm.invoke("what is 6 times 7?", config={"callbacks": [cb]})
print(f"emitted {len(cb.receipts)} Receipts")
print(cb.receipts[0].receipt_id, cb.receipts[0].chain.sequence_number)
Production example (real LLM; swap the fake for ChatAnthropic / ChatOpenAI / any chat model):
from langchain_anthropic import ChatAnthropic
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from chain_receipt_sdk.callback import ReceiptCallback
cb = ReceiptCallback(
client_name="my-langchain-agent",
vendor="anthropic",
model="claude-sonnet-4-5",
capture_payload=True, # only set this if you control the data sensitivity
)
llm = ChatAnthropic(model="claude-sonnet-4-5", temperature=0)
prompt = ChatPromptTemplate.from_messages(
[("system", "be terse"), ("human", "{q}")]
)
chain = prompt | llm | StrOutputParser()
chain.invoke({"q": "what is 6 times 7?"}, config={"callbacks": [cb]})
Note on RunnableLambda: wrapping a plain Python function via RunnableLambda does NOT fire on_llm_start / on_llm_end — those events only fire when an actual language-model class is invoked. If you need to attribute receipts to non-LLM steps, use ReceiptCallback.emit(...) directly.
LlamaIndex callback
Same shape, different framework. chain_receipt_sdk.llamaindex.ReceiptCallback is a BaseCallbackHandler for LlamaIndex's CallbackManager. One Receipt per CBEventType.LLM event; FUNCTION_CALL events between LLM events accumulate into the next emitted Receipt's tool_calls_hash.
from llama_index.core import Settings
from llama_index.core.callbacks import CallbackManager
from chain_receipt_sdk.llamaindex import ReceiptCallback
cb = ReceiptCallback(
client_name="my-llamaindex-agent",
vendor="anthropic",
model="claude-sonnet-4-5",
capture_payload=True, # only if you control the data sensitivity
)
Settings.callback_manager = CallbackManager([cb])
# ... your normal LlamaIndex query / chat / agent pipeline ...
# Each LLM call emits one Receipt; multi-step pipelines emit one per step.
for r in cb.receipts:
print(r.receipt_id, r.chain.sequence_number, r.interaction.tool_calls_hash)
Install: pip install chain-receipt-sdk[llamaindex] (or pin llama-index-core>=0.13 yourself).
LangGraph adapter
LangGraph is built on LangChain's callback infrastructure, so the LangChain ReceiptCallback works automatically when attached to a compiled graph via compiled.invoke(config={"callbacks": [cb]}). The chain_receipt_sdk.langgraph module adds a thin convenience helper attach_receipts() that pre-attaches the callback so you don't have to thread config={"callbacks": [...]} through every call site:
from langgraph.graph import StateGraph, START, END
from chain_receipt_sdk.langgraph import ReceiptCallback, attach_receipts
# ... build your graph as usual ...
compiled = graph.compile()
cb = ReceiptCallback(
client_name="my-langgraph-agent",
vendor="anthropic",
model="claude-sonnet-4-5",
)
wrapped = attach_receipts(compiled, cb)
result = wrapped.invoke({"question": "hi"})
for r in wrapped.callback.receipts:
print(r.receipt_id, r.chain.sequence_number)
attach_receipts() returns a wrapper that exposes the same invoke / ainvoke / stream / astream surface as the compiled graph and merges your callback into any user-supplied callbacks. Conditional routing emits Receipts only for the chosen branch (verified in tests).
Install: pip install chain-receipt-sdk[langgraph].
Pydantic AI adapter
Pydantic AI uses native OpenTelemetry instrumentation. Once Cruxia CV v0.1.0 ships with always-on gen_ai.* span emission, Pydantic AI's own OTel spans will be visible alongside Cruxia CV's chain-receipt spans in any OTel backend (Datadog / Honeycomb / Grafana / New Relic). That's the recommended production integration.
For direct Receipt emission without an OTel collector, chain_receipt_sdk.pydantic_ai ships a callback + Agent wrapper:
from pydantic_ai import Agent
from chain_receipt_sdk.pydantic_ai import ReceiptCallback, attach_receipts
agent = Agent("anthropic:claude-sonnet-4-5", system_prompt="be terse")
cb = ReceiptCallback(
client_name="my-pydantic-ai-agent",
vendor="anthropic",
model="claude-sonnet-4-5",
)
wrapped = attach_receipts(agent, cb)
result = wrapped.run_sync("what is 6*7?")
for r in wrapped.callback.receipts:
print(r.receipt_id, r.chain.sequence_number)
attach_receipts() wraps the Agent so every .run_sync() / .run() auto-emits a Receipt at completion. Alternatively, call cb.emit_from_result(result) manually after each agent.run_sync(...).
Note on instructions= vs system_prompt=: Pydantic AI's instructions= parameter is passed to the model but NOT included in the message history, so the adapter can't see it. Use system_prompt= if you want the system context captured in the Receipt's system_prompt_hash.
Install: pip install chain-receipt-sdk[pydantic-ai].
DSPy adapter
DSPy (≥3.0) exposes BaseCallback at dspy.utils.callback. Register via dspy.settings.configure(callbacks=[cb]) (or scoped dspy.context(callbacks=[cb])); DSPy fires on_lm_start / on_lm_end / on_tool_start / on_tool_end around every LM call across your Predict / ChainOfThought / ReAct / BootstrapFewShot pipeline.
import dspy
from chain_receipt_sdk.dspy import ReceiptCallback
cb = ReceiptCallback(
client_name="my-dspy-program",
vendor="anthropic",
model="claude-sonnet-4-5",
)
# Scoped via context (preferred — doesn't mutate global settings)
with dspy.context(lm=dspy.LM("anthropic/claude-sonnet-4-5"), callbacks=[cb]):
program = dspy.Predict("question -> answer")
result = program(question="what is 6*7?")
for r in cb.receipts:
print(r.receipt_id, r.chain.sequence_number)
Each LM call emits one Receipt; tool calls (via dspy.ReAct etc.) accumulate between LM events and fold into the next Receipt's tool_calls_hash.
Install: pip install chain-receipt-sdk[dspy].
CLI
chain-receipt status # local chain head + count
chain-receipt verify sha256:<hash> # validate signature + chain link
chain-receipt replay sha256:<hash> --n 10 # re-run captured prompt N times
chain-receipt chain --since 2026-04-25 # list Receipts since timestamp
chain-receipt publish sha256:<hash> # push to chain-determinism.org
Local chain storage
Receipts are written to ~/.chain-receipt/chain.jsonl (one JSON per line)
when emitted via ReceiptCallback or ReceiptBuilder.build(persist=True).
The chain_receipt_sdk.chain.LocalChain reader/writer is the public API
for that file.
Replay determinism
chain_receipt_sdk.replay re-runs the captured prompt against the
same vendor + model + temperature N times and reports
final_answer_consistent, tool_seq_identical, tool_args_identical,
and a Wilson 95% CI for the divergence rate. Integration test gate
in tests/test_replay.py.
License
MIT — see LICENSE.
Metadata
Release files for chain-receipt-sdk 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| chain_receipt_sdk-0.2.0.tar.gz | 40.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| chain_receipt_sdk-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 73.5 kB
Release files / chain_receipt_sdk-0.2.0.tar.gz
| Download URL | chain_receipt_sdk-0.2.0.tar.gz |
|---|---|
| Size | 40.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2a05c75149559fde063b0cbc7f7a0c228269d96a2b37babc892a325424ab8c16
|
|
BLAKE2b-256 checksum How to use checksums |
8ce2eb48f9794a4cb6d7d0f08ed9170b1b9e4e38158505a6fa4916b28973e728
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.10.15
|
Release files / chain_receipt_sdk-0.2.0-py3-none-any.whl
| Download URL | chain_receipt_sdk-0.2.0-py3-none-any.whl |
|---|---|
| Size | 33.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e39ce24c8772bd572cf09c5125b2bc4d7019b802cf5e44044585d3047f48343e
|
|
BLAKE2b-256 checksum How to use checksums |
a10ba508982611e699381d0cb404502bea305b746576af7a3adec7634d337f1f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.10.15
|