langchain-satsignal
LangChain callback that anchors agent decisions on Bitcoin SV via Satsignal.
Why this exists. When an autonomous agent decides something — picks a tool, runs a query, signs off on an output — there's no native record proving what was decided or when. langchain-satsignal wires a BaseCallbackHandler into the four-anchor pattern Satsignal uses for agent runs: a policy snapshot at the start, a commit-reveal anchor per decision, and an evidence-bundle manifest at the end. Auditors get a tamper-evident, offline-verifiable record without needing access to your agent's runtime.
Install
pip install langchain-satsignal
Only requires langchain-core. No SDK. The Satsignal helper is vendored in (stdlib-only) so the dependency tree stays flat.
Quickstart
from langchain_satsignal import SatsignalCallbackHandler, SatsignalConfig
handler = SatsignalCallbackHandler(SatsignalConfig(
api_key="sk_live_...", # mint in your Satsignal workspace
folder_slug="agent-runs", # workspace folder to anchor under
# # legacy alias: matter_slug="agent-runs"
agent_name="my-evaluator",
agent_version="1.0.0",
))
# Pass to any LangChain agent / chain via the callbacks config:
result = executor.invoke(
{"input": "..."},
config={"callbacks": [handler]},
)
That's it. The handler:
- anchors a policy snapshot at the top-level chain start (system prompt + user input hashes),
- anchors a commit-reveal commitment per ReAct step (default; configurable),
- anchors an evidence-bundle manifest at the top-level chain end (Merkle root over all decisions).
See examples/react_agent.py for a complete runnable example.
Config
| Field | Default | Notes |
|---|---|---|
api_key |
(required) | Bearer key minted from your Satsignal workspace |
folder_slug |
(required*) | Server-side folder; must exist before the first anchor. Preferred name. |
matter_slug |
(required*) | Frozen legacy alias of folder_slug — still supported forever |
base_url |
https://app.satsignal.cloud |
Override for self-hosted / staging |
fail_open |
True |
API errors logged + swallowed; agent execution continues |
decide_on |
"agent_action" |
"agent_action" (per ReAct step) / "tool_start" (per tool call) / "chain_end" (final outputs only) |
transport |
None |
Test hook. Inject a callable to mock HTTP — see "Testing without the network" |
session_id |
top-level run_id | Override only if you need a custom correlation key |
write_handoff |
False |
If True, writes handoff.json on chain end (contains tool inputs/outputs — review for secrets first) |
agent_name |
None |
Optional metadata baked into the policy snapshot |
agent_version |
None |
Optional metadata baked into the policy snapshot |
* Vocabulary:
folder_slugis the canonical public name;matter_slugis a frozen legacy alias and keeps working forever. Supply either one (legacy code passingmatter_slug=is unaffected). If both are set to different values the constructor raises. The CLI takes--folderalongside--matter. Since 0.3.0 the HTTP request to the Satsignal API sends the canonicalfolder_slugkey (self-hosted servers need a 2026-06+ build, which accepts both).handoff.jsoncarries bothfolder_slugandmatter_slug(andproof_idalongsidebundle_idin each anchor record).
decide_on granularity
| Value | One anchor per… | When to use |
|---|---|---|
agent_action |
ReAct step (LLM decision to act) | Default. Strongest "what did the agent decide" claim per anchor |
tool_start |
Each individual tool call | Tool-heavy agents where each external effect needs its own timestamp |
chain_end |
The final chain output only | Cheapest mode. Manifest still binds the run, but per-decision timing isn't anchored |
Cost: ≈ 119 sats / anchor at 0.1 sat/byte BSV fee floor (≈ $0.0001 at $60/BSV in May 2026). A 5-step agent with decide_on="agent_action" runs about $0.0007 in chain fees. See the Satsignal /agents doc for full cost analysis.
Fail modes
fail_open=True (default) is right for most agents — an audit-trail gap is preferable to a halted agent. Errors go to stderr:
[satsignal] anchor failed: HTTP 429: ...; continuing (fail_open=True)
fail_open=False is right for regulated workflows where unanchored decisions are unacceptable. The first API error halts the agent.
A future release will add fail_pending (queue locally, anchor when network recovers); for now that flow needs a custom transport.
Testing without the network
Frameworks that integrate this need to test their wrapper without burning chain fees. The transport= config takes any callable with the signature:
transport(method, url, headers, body_bytes, timeout) -> (status_int, response_bytes)
so your test suite can substitute any HTTP mocking library. Example:
def fake_transport(method, url, headers, body, timeout):
return 200, b'{"txid":"deadbeef","bundle_id":"abc","root":"f00d","leaf_count":1}'
handler = SatsignalCallbackHandler(SatsignalConfig(
api_key="sk_test", folder_slug="m", transport=fake_transport,
))
Status codes 4xx/5xx still raise APIError, so the handler's fail_open / fail_closed branches are testable. See tests/test_callback.py for the full pattern (8 tests, run offline in 0.04s).
How the handler maps to LangChain hooks
| LangChain callback | Satsignal action |
|---|---|
on_chain_start (top-level: parent_run_id is None) |
Build policy snapshot from serialized + inputs, anchor it |
on_chain_start (nested) |
No-op (no double anchoring) |
on_agent_action (with decide_on="agent_action") |
Wrap action in commit-reveal envelope, anchor commitment |
on_tool_start (with decide_on="tool_start") |
Wrap tool input in commit-reveal envelope, anchor commitment |
on_chain_end (top-level) |
(If decide_on="chain_end": anchor final outputs.) Anchor manifest. |
on_chain_error (top-level) |
Skip manifest (chain failed); clean up the session |
Concurrency: one handler instance can serve multiple top-level chain runs concurrently — each gets its own session keyed by the top-level run_id. Within a single chain, callbacks fire serially per LangChain's contract.
Limitations (v0.1)
- Sync handler only. LangChain dispatches sync callbacks in a thread for async chains, so this works for both, but a native async handler is deferred.
- No streaming token anchors. Per-token anchoring would dominate chain fees and add no audit value.
- No fail-pending queue mode. See "Fail modes" above.
- Policy snapshot is heuristic.
serializedfrom LangChain is hashed wholesale; for a tighter snapshot, build one yourself with the underlying_anchor.build_policy_snapshot(...)and anchor it before invoking the chain.
Links
License
MIT. See LICENSE.
Metadata
Release files for langchain-satsignal 0.3.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 | |
|---|---|---|---|
| langchain_satsignal-0.3.0.tar.gz | 22.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| langchain_satsignal-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 41.6 kB
Release files / langchain_satsignal-0.3.0.tar.gz
| Download URL | langchain_satsignal-0.3.0.tar.gz |
|---|---|
| Size | 22.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4b2670b025ffd9834e30c97a040815bed1297db8d9958f483ffe6a42c0d96b49
|
|
BLAKE2b-256 checksum How to use checksums |
6179af0418865715099409700d5415f1cce68f1d5120fd294f1c10ba42adf6d3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Release files / langchain_satsignal-0.3.0-py3-none-any.whl
| Download URL | langchain_satsignal-0.3.0-py3-none-any.whl |
|---|---|
| Size | 19.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0a511008f133efe6df27088af5cdc7e10c6349f88a10f352e0a207ed8fb5ec71
|
|
BLAKE2b-256 checksum How to use checksums |
cf67a08db19941a76affb72ff03634070e8b4ec1eebfb08806615b94e167efe7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|