Agent integration
Terminology. The canonical vocabulary on every surface is proof (grouped in folders); API responses emit the canonical names only (proof_id, proof_url, folder_slug). Where this spec shows receipt / matter / bundle_id, it is documenting a frozen on-disk/on-chain format, a stable filename (RECEIPTS.md), or a legacy route that remains accepted inbound — proof and receipt denote the same record. Full alias map: the compatibility map.
In plain words: if you run an AI agent and need an auditable, tamper-evident record of what it was allowed to do, what it decided, and what it produced — provable later without trusting the agent's operator — this page is the deep implementer reference; the integration story and quickstart live in the docs. The pattern is: anchor the agent's policy/config at the start, anchor a commitment for each meaningful decision as it happens, anchor a manifest binding the whole run at the end, and hand the auditor a small JSON file plus the chain anchors. What an anchor does and does not prove is stated canonically in the bundle spec; what this specific pattern gets you is in What this gets you (and what it doesn't).
This is the implementer cookbook for the agent-session pattern: the full handoff schema, CI transport testing, lifecycle categories, and the friction points an agent specifically runs into that a human integrator might miss. For the integration overview and the four ways to wire Satsignal in, start at the docs. 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 parts are chain anchors via POST /api/v1/anchors — N + 2 in total: one policy, one per decision, one manifest. The fourth part, the handoff JSON, is the off-chain file the helper below writes for the auditor.
The minimal integration
import os
from agent_anchor import Session
with Session(api_key=os.environ["SATSIGNAL_API_KEY"], folder_slug="agent-runs-prod") 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
- Preview the demo:
python3 agent_anchor.py demo --api-key sk_...prints what would be anchored and exits. Pass--broadcastto fire the demo's real on-chain anchors against your quota. - Verify a handoff:
python3 agent_anchor.py verify-handoff --handoff handoff.jsonrecomputes the local sha for every decision AND chain-confirms each anchored txid via/lookup_hash. Pass--no-chain-confirmto skip the chain step — use that only for CI or offline tests, or when you confirm the transaction another way; on its own it is not a chain-anchor proof.
LangChain · MCP · OpenTelemetry · blob storage · plain CLI all have first-party packages so you don't hand-roll the agent-session pattern — see the integration map.
Onboarding (agent-capable, with one interactive step left)
Satsignal's agent surface is fully self-integrating after you have an API key — and the account itself no longer needs a human:
Agent self-serve signup. POST /api/v1/signup accepts an anti-bot proof object in place of the browser form's CAPTCHA. Two lanes are live: an invite code (minted by an existing workspace owner in their dashboard) or a proof-of-work challenge (hashcash). Start with the discovery document — GET https://app.satsignal.cloud/api/v1/signup — which is the source of truth for the lanes this host currently accepts and the current difficulty; then follow the agent signup guide for the full flow. Signup still ends in a single-use, 15-minute magic-link email, so the agent needs a readable email inbox; the account is created when the link is first used.
The classic human path works unchanged:
- 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:createandproofs:readscopes. - They hand the bearer string (
sk_...) to the agent runtime.
The first key is the one interactive step left on either path: after the magic-link signin, the bootstrap API key is minted in the dashboard at /w/<workspace>/keys, and that first mint is gated to a signed-in session. But a key carrying the keys:admin scope (grantable only in the dashboard) can then mint scoped sub-keys programmatically via POST /api/v1/keys — so a platform provisioning proof for its own tenants mints one keys:admin key by hand, then issues per-tenant sub-keys headlessly (decision 0047; keys:admin itself is never API-mintable, and sub-key scopes are bounded to a subset of the parent's). This programmatic-mint lane is operator-enabled per host and defaults off — where it is not enabled, POST /api/v1/keys returns a uniform 404; confirm or request it for your host via hello@satsignal.cloud (the GET /api/v1/signup discovery doc reports key_mint_api.enabled). Once the agent has a key, every subsequent integration step (creating a folder, anchoring policy + decisions + manifests, listing sibling anchors, reading individual proofs) is a Bearer: HTTP call — no further human in the loop.
Categories and the agent lifecycle
The six 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 |
document | Anywhere | A neutral tag for document-shaped anchors (contracts, reports, evidence files) that aren't agent runs at all |
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. document exists for the non-agent case — anchoring a contract, report, or evidence file under a label that says what it is, rather than borrowing an agent-shaped one.
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 proof and indexed for retrieval via GET /api/v1/anchors?session_id=<X>.
curl -H "Authorization: Bearer sk_..." \
-H "Content-Type: application/json" \
-d '{"folder_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 just the human-readable index key for the same grouping. Skip the manifest and you have a label but no proof; skip session_id and 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). Fewer anchors, weaker timing claim.
# Anchor every decision (default — strong timing, N+2 anchors)
with Session(api_key=..., folder_slug=...) as s: ...
# Batch only — manifest binds the set, no per-decision timing
with Session(api_key=..., folder_slug=..., commit_each=False) as s: ...
Anchor by default. For high-assurance workflows, anchor each meaningful decision as it happens (the default). For end-state-only audits, batch decisions into the final manifest. The first gives stronger timing evidence per decision; the second produces a lighter proof trail that still binds the full set.
Selective-disclosure proofs (proof_set)
A standard-mode anchor body may carry an optional proof_set instead of just sha256_hex. proof_set.byte_exact is required and must equal the submitted sha256_hex + file_size; you may add content_canonical and/or chunk_merkle (a Merkle root over per-chunk leaves — page / row / record), with the leaf hashes in the companion proof_leaves:
curl -H "Authorization: Bearer sk_..." -H "Content-Type: application/json" \
-d '{"folder_slug":"agent-runs","sha256_hex":"<h>","file_size":1234,
"proof_set":{"byte_exact":{"algo":"sha256","size":1234,"hash":"<h>"},
"chunk_merkle":{"scheme":"pdf-page-v1","algo":"sha256",
"leaf_count":12,"root":"<r>"}},
"proof_leaves":{"scheme":"pdf-page-v1","merkle_leaves":["..."],
"metadata":{"leaf_count":12}}}' \
https://app.satsignal.cloud/api/v1/anchors
Only the envelope (scheme / algo / leaf_count / root) is committed on-chain; the leaves ride off-chain in the bundle's proofs.json, and the verifier recomputes leaf→root from the original file — the commitment stays opaque, there is nothing for the operator to reconstruct. To attach a proof_set to a hash already anchored in the folder, pass force_new: true (default dedup keys on sha256_hex and otherwise returns 409 proof_set_requires_force_new rather than dropping the richer proofs). Scheme registry + the canonicalization each scheme implies: the bundle spec (bundle-v1.md §5). Top-level chunk_merkle / content_canonical 400 (they belong inside proof_set). Sealed mode accepts the same envelope under HMAC algos — see below.
Sealed-mode proof_set (mirror + blind)
mode=sealed accepts the same proof_set field name; the inner shapes carry HMAC algos and the byte_exact key is commitment (not hash). The commitment in the envelope must equal the top-level byte_exact_commitment. Works in both Mirror (with salt_b64) and Blind (omit salt_b64) — the wire envelope is identical between them; the response shape differs (mirror returns bundle_b64, blind returns canonical_b64 + doc_hash):
curl -H "Authorization: Bearer sk_..." -H "Content-Type: application/json" \
-d '{"folder_slug":"sealed-runs","mode":"sealed",
"salt_b64":"<32-byte salt, base64url — omit for blind>",
"byte_exact_commitment":"<HMAC-SHA256(salt, file)>",
"file_size":1234,
"proof_set":{
"byte_exact":{"algo":"hmac-sha256","commitment":"<same HMAC>"},
"chunk_merkle":{"algo":"merkle-hmac-sha256",
"scheme":"pdf-page-v1",
"leaf_count":12,"root":"<Merkle-HMAC root>"}},
"proof_leaves":{"scheme":"pdf-page-v1",
"merkle_leaves":["<leaf HMAC>", "..."],
"metadata":{"leaf_count":12}}}' \
https://app.satsignal.cloud/api/v1/anchors
As in the sealed file flow above, only the envelope (scheme / algo / leaf_count / root / salt_version) is committed on-chain; the leaf HMACs ride off-chain in proofs.json. Selective disclosure works the same way — revealing one chunk's HKDF-derived salt + canonicalized bytes lets a recipient recompute that leaf and re-derive the root; siblings stay opaque. file_size stays top-level — sealed canonical docs strip leak fields, and the size does not enter the on-chain envelope. Cross-mode algo mistakes (sha256 in a sealed body) 400 before the wallet is touched. No force_new 409 in sealed — sealed entries don't index a naked file hash, so there is no default-dedup gate to escape. See SPEC_v2_sealed.md §4.2 for the HMAC-tagged canonical-doc shape and §11 for the Blind submission protocol.
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. It carries the session metadata, the policy anchor + snapshot, every decision (with its envelope, sha, nonce, and txid), and the manifest anchor (txid + Merkle root + leaf count) — the full field-by-field shape is in the collapsed reference at the end of this section.
Auditor verification path:
- For each decision: recompute
sha256(canonicalize(envelope)), compare againstdecision_sha256_hex. Then chain-confirm by resolving eachdecision_sha256_hexviaGET /lookup_hash?sha=...and confirming the resolver returns the sametxidas the handoff. The helper'sverify-handoffsubcommand does both steps by default. Use chain confirmation for any external proof claim; local-only verification is for CI or offline tests, or when the verifier confirms the transaction another way — on its own it is not a chain-anchor proof. - For deeper confirmation: fetch each
txidvia any BSV block explorer's tx-lookup and confirm the OP_RETURN matches the canonical doc reconstructed from each decision's envelope. - 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.
Full schema below. Skip unless you're generating or parsing
handoff.jsonyourself. Reference shape of thesatsignal-agent-handoff-v1file the helper writes; not needed for the integration above. (The file format'sbundle_idkeys are the proof id under its frozen on-disk spelling.)
{
"version": "satsignal-agent-handoff-v1",
"session_id": "run-2026-05-09-001",
"folder_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": "..."
}
}
Testing without the network
Testing note. Skip unless you're writing CI for an integration that wraps
Session(). Not first-read; the integration above works without any of this.
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", folder_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 folder_slug
Terminology. The canonical organizing unit is a folder (
folder_slug,/api/v1/folders,404 folder_not_found). The legacymatter_slugbody key and/api/v1/mattersroute remain accepted inbound forever and resolve identically — see the compatibility map. New code should use the folder names.
A folder is the workspace's organizing unit; agents typically anchor under one long-lived folder per environment:
agent-runs-devagent-runs-prodeval-runs-prod
Create folders via POST /api/v1/folders ({slug, name}), or in the dashboard UI. The folder must exist before you anchor; otherwise POST /api/v1/anchors returns 404 folder_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.
What this does not do:
- Prove the agent obeyed its policy or instrumented honestly. Satsignal proves commitments to the records the integrator chose to anchor — not that the agent actually followed the anchored policy, used the claimed model, exposed every tool, or logged every relevant decision. An agent that anchors a policy snapshot and then ignores it still produces a perfectly valid proof of the snapshot, not of compliance. Completeness and truthful instrumentation remain integration obligations; the chain anchor makes the anchored records tamper-evident, not the agent honest.
- Fully non-interactive bootstrap. Signup itself is agent-capable (invite code or proof-of-work — see the agent signup guide), but the magic link still needs a readable email inbox, and the first API key per workspace is minted in the dashboard after signin. Once a
keys:adminkey exists, though, further scoped keys ARE mintable headlessly viaPOST /api/v1/keys(decision 0047) — so only the one bootstrap key is interactive, not every key. That programmatic-mint lane is operator-enabled per host and defaults off (a non-enabling host answersPOST /api/v1/keyswith a uniform404); confirm or request it via hello@satsignal.cloud before relying on it. - Session-bound canonical hashing.
session_idis off-chain; it's a label on the proof, not a hash input. The Merkle root in the manifest is what cryptographically groups the anchors. - Sealed-mode session_id support. Sealed-mode proofs (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 proofs row does. Adequate for sealed agent runs that share a workspace; insufficient if you want sealed-mode auditors to derive grouping without workspace access. - 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.
Questions about this specification? Email hello@satsignal.cloud.