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:

  1. A policy snapshot at the start. What was the agent allowed to do? Which model, which tools, which budget. (category: "policy_snapshot")
  2. One commitment per meaningful decision during the run. (category: "commitment", wrapped in a fresh-nonce envelope so timing pre-discloses without content pre-disclosing.)
  3. One evidence-bundle manifest at the end. The Merkle root that binds all the decisions together cryptographically. (category: "evidence_bundle", mode: "manifest".)
  4. 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:

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:

  1. A human visits https://app.satsignal.cloud/login and enters an email address.
  2. They click the magic-link in the inbox.
  3. They mint an API key with the anchors:create and receipts:read scopes.
  4. 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:

CategoryWhenWho reads it
policy_snapshotOnce, at startThe auditor reconciling "what was this agent allowed to do"
commitmentPer decisionThe auditor proving "this output was committed at THIS time, before disclosure"
evidence_bundleOnce, at endThe auditor verifying "all of these decisions belong to this session"
outputAnywhereThe default — produced artifacts that aren't decisions per se
memory_checkpointPer long-run state saveThe 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:

  1. For each decision: recompute sha256(canonicalize(envelope)), compare against decision_sha256_hex. The helper's verify-handoff subcommand does this locally.
  2. For each txid: fetch via GET /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.
  3. For manifest_anchor.root: recompute the Merkle root over the ordered decision_sha256_hex values, 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:

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:

You don't get (yet, or by design):

Source: docs/notary_spec/AGENTS.md. Email hello@satsignal.cloud for clarifications.