Agent integration
How to make an autonomous AI agent self-integrate with Satsignal in about ten lines of code, plus the friction points an agent specifically runs into that a human integrator might miss.
This page targets agents (and the people building them). For the underlying canonical-doc shape and on-chain wire format, read /spec.
TL;DR
Anchor four things per agent run:
- A policy snapshot at the start. What was the agent allowed to do? Which model, which tools, which budget. (
category: "policy_snapshot") - One commitment per meaningful decision during the run. (
category: "commitment", wrapped in a fresh-nonce envelope so timing pre-discloses without content pre-disclosing.) - One evidence-bundle manifest at the end. The Merkle root that binds all the decisions together cryptographically. (
category: "evidence_bundle",mode: "manifest".) - A handoff JSON file that an auditor receives alongside the chain anchors so they can reconstruct the session offline.
The first three are anchors on the BSV chain via POST /api/v1/anchors. The fourth is an off-chain JSON file. The five-anchor pattern (1 + N decisions + 1 manifest) is what the helper below produces.
The 4-line integration
from agent_anchor import Session
with Session(api_key="sk_live_...", matter_slug="agent-runs") as s:
s.policy(system_policy_text=SYSTEM_PROMPT, model_config={"model": "claude-opus-4-7"})
for step in plan:
s.decide(step.label, run_step(step))
# __exit__ anchors the manifest + writes handoff.json
That's the whole thing. Each decide() call is one chain anchor. The manifest at the end binds them together with a Merkle root. The context manager writes handoff.json so the auditor has everything they need.
The helper is stdlib-only Python, no SDK install:
- Source: agent_anchor.py
- Run the demo:
python3 agent_anchor.py demo --api-key sk_... --matter agent-runs
Using LangChain? The first-party langchain-satsignal package (pip install langchain-satsignal) drops the same primitives into a LangChain agent as LangChain-native components — anchor a policy snapshot at session start, commit-reveal each decision, finalize with a manifest. Source-of-truth for the integration lives in the public repo above.
Not an agent integration? For one-file-at-a-time anchoring (and local verification) from a shell or a non-LangChain script, the official satsignal-cli (pip install satsignal-cli) is the shorter path: satsignal anchor file.pdf --broadcast then satsignal verify file.pdf. The CLI implements the bundle-v1 verification procedure end-to-end. Scripted flows can opt into --strict to exit non-zero (code 7) if the local .mbnt sidecar can't be written, even when the on-chain anchor succeeds.
Onboarding (the part that needs a human)
Satsignal's agent surface is fully self-integrating after you have an API key. Getting the API key the first time is not — there is no agent-only signup path:
- A human visits
https://app.satsignal.cloud/loginand enters an email address. - They click the magic-link in the inbox.
- They mint an API key with the
anchors:createandreceipts:readscopes. - They hand the bearer string (
sk_...) to the agent runtime.
Once the agent has the key, every subsequent integration step (creating a matter, anchoring policy + decisions + manifests, listing sibling anchors, reading individual receipts) is a Bearer: HTTP call — no further human in the loop.
The reason for the human gate is botnet-defense, not agent-hostility: unauthenticated key minting on a public site is exactly the kind of endpoint that attracts reflector abuse. We have not yet built an agent-attested signup path; it's tracked in the backlog.
Categories and the agent lifecycle
The five anchor categories map to a session timeline:
| Category | When | Who reads it |
|---|---|---|
policy_snapshot | Once, at start | The auditor reconciling "what was this agent allowed to do" |
commitment | Per decision | The auditor proving "this output was committed at THIS time, before disclosure" |
evidence_bundle | Once, at end | The auditor verifying "all of these decisions belong to this session" |
output | Anywhere | The default — produced artifacts that aren't decisions per se |
memory_checkpoint | Per long-run state save | The auditor reconstructing rolling state across sessions |
Most agent runs will use the first three. output is the catch-all default (it's what category resolves to when you omit the field entirely). memory_checkpoint is for agents that maintain state across sessions and want a tamper-evident trail of how that state evolved.
Cross-anchor correlation: session_id
Every anchor request accepts an optional session_id field in the body — a freeform string (1-128 chars, [A-Za-z0-9_.-]). When set, the field is stored alongside the receipt and indexed for retrieval via GET /api/v1/anchors?session_id=<X>.
curl -H "Authorization: Bearer sk_..." \
-H "Content-Type: application/json" \
-d '{"matter_slug":"agent-runs","sha256_hex":"...","file_size":1234,
"category":"commitment","session_id":"run-2026-05-09-001"}' \
https://app.satsignal.cloud/api/v1/anchors
Important: session_id is off-chain by design. The cryptographic "these anchors belong together" binding is the evidence-bundle manifest's Merkle root, anchored at end-of-session. session_id is the human-readable index key for the same grouping — it makes the cross-anchor query work without making every verifier-of-a-single- anchor need to know it.
If you skip the manifest at end-of-session, you have a label but no proof. If you skip session_id but anchor a manifest, you have proof but no convenient query. The Session() helper does both.
Anchor-per-decision vs batch-only
The default Session() flow anchors every decide() call individually (N decisions → N + 2 anchors total: 1 policy + N commitments + 1 manifest). Use this when disclosure timing matters — when the auditor needs to know each decision was committed before it was revealed.
For end-state-only audits, instantiate with commit_each=False. The helper holds decisions locally and only anchors the policy + manifest (2 anchors total). Cheaper, weaker timing claim.
# Anchor every decision (default — strong timing, N+2 anchors)
with Session(api_key=..., matter_slug=...) as s: ...
# Batch only — manifest binds the set, no per-decision timing
with Session(api_key=..., matter_slug=..., commit_each=False) as s: ...
Cost (May 2026): ≈ 119 sats per anchor at 0.1 sat/byte fee floor. A 5-anchor session ≈ 595 sats ≈ $0.0004 at $60/BSV. An agent making 10K decisions/day with commit_each=True tops out near $3/day in chain fees — i.e., "anchor by default, not by exception" is economically viable for agents.
Handoff JSON schema (satsignal-agent-handoff-v1)
The helper writes this to handoff.json on session exit. An auditor receiving the file can verify every chain anchor + recompute every decision sha without contacting the agent operator.
{
"version": "satsignal-agent-handoff-v1",
"session_id": "run-2026-05-09-001",
"matter_slug": "agent-runs",
"agent": {"name": "my-evaluator-bot", "version": "1.4.2"},
"started_at_utc": "2026-05-09T12:00:00Z",
"ended_at_utc": "2026-05-09T12:03:42Z",
"policy_anchor": {
"txid": "e8eb2e63...",
"bundle_id": "abcd1234deadbeef",
"snapshot_sha256_hex": "...",
"anchored_at_utc": "..."
},
"policy_snapshot": { "version": "satsignal-policy-snapshot-v1", "...": "..." },
"decisions": [
{
"label": "step-1",
"decision_sha256_hex": "...",
"nonce_hex": "<128-bit hex>",
"envelope": { "version": "satsignal-agent-decision-v1",
"nonce_hex": "...", "payload": { "...": "..." } },
"txid": "beca54bb...",
"bundle_id": "...",
"anchored_at_utc": "..."
}
],
"manifest_anchor": {
"txid": "b39d795a...",
"bundle_id": "...",
"root": "<merkle root hex>",
"leaf_count": 3,
"anchored_at_utc": "..."
}
}
Auditor verification path:
- For each decision: recompute
sha256(canonicalize(envelope)), compare againstdecision_sha256_hex. The helper'sverify-handoffsubcommand does this locally. - For each
txid: fetch viaGET /api/v1/lookup/<txid>(or any BSV block explorer's tx-lookup) and confirm the OP_RETURN matches the canonical doc reconstructed from each decision's envelope. This is the chain-confirmation step. - For
manifest_anchor.root: recompute the Merkle root over the ordereddecision_sha256_hexvalues, compare. The root binds the decision set — an auditor can prove no decision was added or removed after the fact without breaking this match.
Testing without the network
Session(...) accepts a transport= callable so framework CI can exercise the wrapper without hitting the API. Default is urllib; your test passes a closure that returns canned (status, response_bytes) tuples. APIError is still raised on non-2xx, so 401 / 429 branches stay testable.
from agent_anchor import Session, APIError
def fake_transport(method, url, headers, body, timeout):
# assert on body if you want; return any canned response
return 200, b'{"txid":"deadbeef","bundle_id":"abcd"}'
with Session(api_key="sk_test", matter_slug="m",
transport=fake_transport, write_handoff=False) as s:
s.decide("step-1", {"x": 1})
assert s.decisions[0]["txid"] == "deadbeef"
Signature: transport(method, url, headers, body_bytes, timeout) -> (status_int, response_bytes). Same shape as a requests adapter; trivial to wire up against any HTTP mocking library.
Choosing a matter_slug
A matter is the workspace's organizing unit; agents typically anchor under one long-lived matter per environment:
agent-runs-devagent-runs-prodeval-runs-prod
Create matters via POST /api/v1/matters ({slug, name}), or in the dashboard UI. The matter must exist before you anchor; otherwise POST /api/v1/anchors returns 404 matter_not_found.
What this gets you (and what it doesn't)
You get:
- Tamper-evident timing: every decision anchored before disclosure.
- Tamper-evident grouping: manifest root binds the decision set.
- Selective disclosure: reveal one decision, the others stay hidden; the chain commitment still binds everything.
- Offline auditor: handoff.json + chain anchors is enough to verify a session without trusting (or contacting) the agent operator.
You don't get (yet, or by design):
- Agent-attested signup. Onboarding still requires a human with an email inbox.
- Session-bound canonical hashing.
session_idis off-chain; it's a label on the receipt, not a hash input. The Merkle root in the manifest is what cryptographically groups the anchors. - Sealed-mode session_id support. Sealed-mode receipts (where the chain anchor commits to an HMAC of the file rather than the file's sha256) accept
session_idon the request body, but the sealed canonical doc itself doesn't surface it; the workspace receipts row does. Adequate for sealed agent runs that share a workspace; insufficient if you want sealed-mode auditors to derive grouping without workspace access. Tracked in the backlog. - Real-time push. The API is request/response; there is no WebSocket or webhook for "tell me when an anchor lands". Polling
GET /api/v1/anchors?session_id=Xworks for short-window polling.
Source: docs/notary_spec/AGENTS.md.
Email hello@satsignal.cloud
for clarifications.