Skip to main content

satsignal-otel

OpenTelemetry SpanProcessor that anchors selected GenAI spans to BSV via Satsignal. One integration covers any observability stack already speaking OTel — Langfuse, LangSmith, Arize, Datadog, Honeycomb — and adds a tamper-evident receipt for the spans that matter.

Your observability stack shows the run. Satsignal proves the run record hasn't been edited since.

pip install satsignal-otel

What it does

Drop the processor into your TracerProvider. Only spans carrying the attribute satsignal.anchor=true are anchored — everything else flows through to your existing exporters untouched. Matching spans are batched and posted as a single manifest-mode anchor on BSV, binding all leaves under one Merkle root. The on-chain anchor is the receipt your auditor uses to prove the span record has not been edited since that block was mined.

By default the SDK is opt-in per span, batched (1-minute window), sha-only (your span bytes never leave the process), and fail-open (anchor failures log + drop; your app keeps running).

Failed-eval auto-anchor (headline)

Wire one line into your eval pipeline. When a scorer drops below threshold, mark the span — Satsignal anchors the failure with a per- span receipt so the timing claim ("we knew at 14:32 UTC") is provable.

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from satsignal_otel import SatsignalSpanProcessor, auto_anchor_on_eval_fail

provider = TracerProvider()
provider.add_span_processor(SatsignalSpanProcessor(
    api_key=os.environ["SATSIGNAL_API_KEY"],
    folder_slug="otel-evals",
))
trace.set_tracer_provider(provider)

tracer = trace.get_tracer(__name__)

with tracer.start_as_current_span("eval.scorer") as span:
    score = run_scorer(prompt, response)
    span.set_attribute("gen_ai.eval.score", score)
    auto_anchor_on_eval_fail(span, threshold=0.7)

The helper sets satsignal.anchor=true only when score < threshold; under threshold spans are anchored individually (mode=single) so the receipt timing is per-event, not amortized across a batch.

Release-gate anchor (secondary)

At deploy time, mark the release-manifest span. One receipt per release — what shipped, with the eval evidence around it.

from satsignal_otel import mark_for_anchor

with tracer.start_as_current_span("release.gate") as span:
    span.set_attribute("gen_ai.system", "anthropic")
    span.set_attribute("gen_ai.model", "claude-opus-4-7")
    span.set_attribute("prompt.version", PROMPT_VERSION)
    span.set_attribute("eval.pass_rate", PASS_RATE)
    span.set_attribute("config.hash", CONFIG_HASH)
    mark_for_anchor(span, mode="single", label=f"release-{GIT_SHA}")

Configuration

Vocabulary: folder_slug is the canonical name; matter_slug is a deprecated legacy alias that is still accepted silently. The constructor accepts either; at least one is required (legacy callers that pass matter_slug= are unaffected). If both are set to different values the constructor raises ValueError.

Compatibility note (v0.3.0, vocabulary sunset): requests now send the canonical folder_slug wire key, and the live Satsignal API emits canonical response keys only (proof_id, proof_url, folder_slug). Reading still falls back to the legacy response keys (bundle_id, receipt_url, matter_slug) for older self-hosted servers, but a self-hosted server too old to accept the folder_slug request key needs v0.2.x of this package. .folder_slug / .matter_slug read accessors and AnchorResult.proof_id / .proof_url / .folder_slug accessors are available; new code should use the canonical names.

SatsignalSpanProcessor(
    api_key,                    # required: SATSIGNAL_API_KEY
    folder_slug,                # canonical: workspace folder for proofs
    # matter_slug,              # deprecated legacy alias of folder_slug
    base_url="https://app.satsignal.cloud",
    flush_interval=60.0,        # seconds between manifest flushes
    max_batch_size=500,         # force-flush when queue hits this size
    daily_anchor_cap=1000,      # client-side guard against runaway anchoring
    fail_open=True,             # log + drop on anchor failure
    transport=None,             # inject a callable for tests (see below)
)

Attributes the processor reads

Attribute Type Effect
satsignal.anchor bool Required to anchor. True → span is queued.
satsignal.anchor.mode str "manifest" (default, batched) or "single" (per-span anchor).
satsignal.anchor.label str Optional display label on the receipt (truncated at 256 chars).
satsignal.anchor.force_new bool True bypasses server-side dedup (single mode only).

What gets anchored

The sha256 of a deterministic canonical-JSON encoding of the span's {name, kind, status, start_time, end_time, attributes, events, links, resource}. The bytes never leave your process — only the hash. The OTel trace_id and span_id ride along as an off-chain session_id so a verifier can correlate the receipt back to your existing trace data.

This is chain-of-custody for the span record, not the underlying prompt + completion bytes. Your trace store still owns the bytes; the anchor proves they have not been edited since.

Threat model

  • Span attributes are attacker-controllable when prompts include user-provided strings. The label is treated as untrusted display text server-side (length-capped, harness-string-rejected). The sha256 is bytes-are-bytes.
  • An anchor proves anchorer-knowledge, not world-existence. The on-chain receipt commits the anchorer's knowledge of the canonical bytes at the anchored time; it does NOT prove the span existed before then. For end-to-end provenance pair this with a commit- reveal flow over the underlying prompt + completion.
  • Semantic-convention churn. The GenAI OTel semantic conventions are still in development. auto_anchor_on_eval_fail reads gen_ai.eval.score, gen_ai.evaluation.score, and eval.score — pass score_attribute= if your stack names it something else, or pass score= directly.

Sister packages

Testing without the network

The processor accepts a transport= callable matching urllib's shape:

def fake(method, url, headers, body, timeout):
    return 200, b'{"proof_id": "abc", "txid": "deadbeef", ...}'

processor = SatsignalSpanProcessor(
    api_key="sk_test", folder_slug="f", transport=fake,
)

The full test suite exercises the processor against a mock transport; see tests/test_processor.py for canned-response patterns.

License

MIT.

Metadata

Release files for satsignal-otel 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for satsignal-otel 0.3.0
File Size Uploaded
satsignal_otel-0.3.0.tar.gz 27.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for satsignal-otel 0.3.0
File Interpreter ABI Platform
satsignal_otel-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 47.5 kB

Release files / satsignal_otel-0.3.0.tar.gz

Download URL satsignal_otel-0.3.0.tar.gz
Size 27.7 kB
Tags Source
SHA-256 checksum
How to use checksums
af5f977e07bdf23f08cc67837b987b59eb43f74c0d16f1894457c53935c9fd62
BLAKE2b-256 checksum
How to use checksums
7021b0680536b670ff814edbe66cc21a14344fbf3f9e7e64f8eb409f4a9f978a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 10, 2026.

Transparency log

Release files / satsignal_otel-0.3.0-py3-none-any.whl

Download URL satsignal_otel-0.3.0-py3-none-any.whl
Size 19.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
55a0376e0065932abb9e39c5f777e152d444e9f62b78f69d02e9d11c8cf41c60
BLAKE2b-256 checksum
How to use checksums
2fa8bc3f75018f56b5249f9f7de9ce87f940f655c90a387f63536eaabd16ce9d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 10, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page