TL;DR
Attach a single LangChain BaseCallbackHandler that calls Marturia’s receipt endpoint on every on_agent_action and on_chain_end. The handler signs the run ID, tool call, and observation so you can later prove exactly what the agent did.
Why a LangChain developer should care¶
Customers increasingly ask “prove the agent really said that.” Without an immutable record you are left with log files that can be edited. Marturia receipts give you a hash-chained, Ed25519-signed artifact that anyone can verify offline, satisfying both support tickets and compliance audits.
Implementing the callback handler¶
Create a handler that emits a receipt for every consequential step. The example below is fully runnable with LangChain 0.2+ and the official Marturia Python client.
from typing import Any, Dict, Optional
from uuid import UUID
from langchain_core.callbacks import BaseCallbackHandler
from langchain_core.agents import AgentAction, AgentFinish
import marturia
class MarturiaCallbackHandler(BaseCallbackHandler):
def __init__(self, tenant_id: str, api_key: str):
self.client = marturia.Client(tenant_id=tenant_id, api_key=api_key)
def on_agent_action(
self,
action: AgentAction,
*,
run_id: UUID,
parent_run_id: Optional[UUID] = None,
**kwargs: Any,
) -> None:
payload = {
"agent_run_id": str(run_id),
"action": action.tool,
"tool_input": action.tool_input,
"log": action.log,
}
self.client.create_receipt(
event_type="agent_action",
payload=payload,
chain_id=str(parent_run_id) if parent_run_id else None,
)
def on_chain_end(
self,
outputs: Dict[str, Any],
*,
run_id: UUID,
parent_run_id: Optional[UUID] = None,
**kwargs: Any,
) -> None:
payload = {
"agent_run_id": str(run_id),
"final_output": outputs,
}
self.client.create_receipt(
event_type="chain_end",
payload=payload,
chain_id=str(parent_run_id) if parent_run_id else None,
)
Attach it when you build the agent:
from langchain.agents import initialize_agent
handler = MarturiaCallbackHandler(tenant_id="acme", api_key=os.environ["MARTURIA_KEY"])
agent = initialize_agent(tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, callbacks=[handler])
Receipt payload shape¶
Marturia expects a minimal but traceable object. The two fields that matter most are:
agent_run_id– LangChain’s nativerun_id(UUID). This becomes the stable identifier across all receipts for a single user request.payload– a JSON object containing the action taken plus the observation returned by the tool or the final chain output.
Everything else (timestamp, previous receipt hash, tenant signature) is added by the Marturia service.
Offline verification¶
Anyone can check receipts without contacting your servers:
pip install marturia-verify
marturia-verify --receipt receipt.json --public-key acme.pub
The tool walks the hash chain and validates the Ed25519 signature in a single command.
Common pitfalls¶
- Rate limits: The default plan allows 600 receipts per minute. Batch non-critical observations or request a higher limit.
- Payload size: Keep the
payloadunder 16 KiB. Large tool outputs should be summarized or referenced by hash. - Async vs sync: If you use
AsyncCallbackHandler, overrideon_agent_actionandon_chain_endwith theasyncversions; otherwise receipts are dropped silently. - Missing parent_run_id: Always pass the parent ID when nesting chains so the Merkle tree stays connected.
What this protects you from¶
Cryptographic receipts directly address the threat models described in the Marturia threat-models guide. They prevent an operator from retroactively changing an agent’s decision after a customer dispute and give regulators an immutable audit trail.
Related Marturia resources
- /docs/quickstart.html
- /docs/api.html
- /learn/lesson_07_threat_models.html