Anchor objects in S3 / R2 / GCS / Azure Blob to BSV via Satsignal. Files stay in your bucket; portable proof sidecars land next to them.
Project description
satsignal-blob
Anchor objects in S3 / R2 / GCS / Azure Blob to BSV via
Satsignal. Files stay in your bucket;
portable proof sidecars (.mbnt + .proof.json) land next to them.
Keep your files where they are. Satsignal gives them independent proof.
pip install satsignal-blob[s3] # or [gcs], [azure], [all]
What it does
For each object under a bucket prefix, satsignal-blob streams the
bytes through sha256, posts the digest (not the file) to Satsignal's
POST /api/v1/anchors, and writes two sidecar objects next to the
original:
| sibling | what it is | who uses it |
|---|---|---|
contract.pdf.mbnt |
full evidence bundle (zip) — txid, canonical doc, miner acceptance, BSV chain anchor | anyone who needs to verify the receipt offline against a public BSV explorer |
contract.pdf.proof.json |
one-page summary — sha256, txid, receipt URL, bundle URL, timestamp | humans, dashboards, grep / jq pipelines |
The original contract.pdf is untouched. The file bytes never leave
your bucket — Satsignal only sees the sha256.
CLI quickstart
Vocabulary:
folderis the canonical public name;matteris a frozen legacy alias and keeps working forever. The CLI accepts--folderalongside--matter; the library acceptsfolder_slug=alongsidematter_slug=. Either--folderor--matteris required (legacy invocations that pass--matterare unaffected). If both are set to different values the command fails loudly; equal or single is fine. The.proof.jsonsidecar carries bothfolder_slugandmatter_slug(andproof_id/bundle_id,proof_url/receipt_url). 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).
export SATSIGNAL_API_KEY=sk_...
# Anchor every PDF under a prefix, cap at 50 attempts.
satsignal-blob anchor s3://my-bucket/contracts/2026/ \
--folder contracts-2026 \
--include '*.pdf' \
--max-files 50
# Dry-run first to preview hashes + plan without firing any anchors:
satsignal-blob anchor s3://my-bucket/contracts/2026/ \
--folder contracts-2026 --include '*.pdf' --dry-run
A successful run prints one line per object plus a summary on stderr:
[OK] s3://my-bucket/contracts/2026/a.pdf sha=ab1234567890 bundle=87f9c2b4… txid=e7c4f1…
[OK] s3://my-bucket/contracts/2026/b.pdf sha=cd5678901234 bundle=2a91b3e5… txid=e7c4f1…
[SKIP] s3://my-bucket/contracts/2026/c.pdf (.mbnt sidecar exists; --force to re-anchor)
satsignal-blob: anchored=2 duplicate=0 skipped_existing=1 skipped_sidecar=0 failed=0
Pass --json to emit one JSON object per line — pipe through jq /
fluentd / anything line-oriented.
Supported backends
satsignal-blob resolves URIs through fsspec.
Install the optional dep that matches your storage:
| storage | URI scheme | install extra | backend |
|---|---|---|---|
| AWS S3 / MinIO / Wasabi / B2 / Cloudflare R2 | s3:// |
pip install satsignal-blob[s3] |
s3fs |
| Google Cloud Storage | gs:// |
pip install satsignal-blob[gcs] |
gcsfs |
| Azure Blob Storage | az:// |
pip install satsignal-blob[azure] |
adlfs |
| Local filesystem | /path/ or file:/// |
(none) | built-in |
Cloudflare R2 is S3-compatible — use s3:// and configure the
AWS_ENDPOINT_URL_S3 env var to point at your R2 endpoint.
Library API
from satsignal_blob import anchor_prefix
for outcome in anchor_prefix(
"s3://my-bucket/contracts/2026/",
api_key="sk_...",
folder_slug="contracts-2026",
include=["*.pdf"],
):
if outcome.status == "anchored":
print(outcome.receipt_url)
Or a single object:
from satsignal_blob import anchor_object
result = anchor_object(
"s3://my-bucket/contracts/2026/contract.pdf",
api_key="sk_...",
folder_slug="contracts-2026",
)
print(result.txid, result.receipt_url)
Event-driven (S3 → Lambda)
For real-time anchoring on every PUT, wire an S3 ObjectCreated:*
event to a Lambda that calls anchor_object. Copy
examples/lambda_handler.py into a
Python 3.11 Lambda, set three env vars, and you're done:
import os, urllib.parse
from satsignal_blob import anchor_object
API_KEY = os.environ["SATSIGNAL_API_KEY"]
WORKSPACE_SLUG = os.environ["SATSIGNAL_WORKSPACE_SLUG"]
FOLDER_SLUG = os.environ["SATSIGNAL_FOLDER_SLUG"]
def lambda_handler(event, context):
results = []
for record in event.get("Records", []):
bucket = record["s3"]["bucket"]["name"]
key = urllib.parse.unquote_plus(record["s3"]["object"]["key"])
if key.endswith((".mbnt", ".proof.json")):
continue # don't anchor sidecars
outcome = anchor_object(
f"s3://{bucket}/{key}",
api_key=API_KEY,
folder_slug=FOLDER_SLUG,
)
results.append({"key": key, "status": outcome.status,
"bundle_id": outcome.bundle_id})
return {"results": results}
Same pattern works for Cloud Functions (GCS finalize) and Azure Functions (blob trigger) — just swap the scheme and the event shape.
What this proves (and what it doesn't)
A Satsignal anchor over sha256(file) proves:
- The operator of this Satsignal workspace knew this sha256 by the block time of the on-chain anchor. Anyone with the file can later recompute the sha and verify against the chain entry — without trusting Satsignal or your storage provider.
It does not prove:
- That the file existed before the anchor. The chain only records when the sha entered the ledger.
- The identity of whoever uploaded it. The receipt is "this sha was anchored by this workspace at this time" — not "this person did it."
For workflows that need before-the-anchor existence (e.g. press-
embargo proofs), pair satsignal-blob with a commit-reveal flow
via satsignal-cli or
the /commit_reveal.py helper.
Threat model
- sha-only. File bytes never leave your process. The Satsignal API only sees the digest + size + optional label + filename.
- Untrusted attribute fields.
labelandfilenameare clipped server-side at 256 chars and validated for control substrings. Still — don't put secrets in either. - Fail-open per object. One file's API failure does not abort the
walk. The exit code is
1if any file failed; the per-file error is in the JSON output. - No bytes on chain. Satsignal never anchors file contents — only the digest. This is by design and is not a config option.
Related packages
satsignal-cli— general-purpose anchor + verify CLI; can verify any.mbntfile written by satsignal-blob.satsignal-mcp— MCP server exposing the same 5 primitives to MCP-aware agents.satsignal-otel— anchor OpenTelemetry spans (failed evals, release gates).satsignal-action— GitHub Action: anchor build artifacts on every release.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file satsignal_blob-0.3.0.tar.gz.
File metadata
- Download URL: satsignal_blob-0.3.0.tar.gz
- Upload date:
- Size: 25.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dd418c5decfdbf08dc555f56fc8e7cd053bd96e4a3d4330d68ac1c9dd4c9bb18
|
|
| MD5 |
4fcb26aae90fcf2bb17258f96e4b3611
|
|
| BLAKE2b-256 |
99b412447aad2cd7a2c5d87736f2a92b04b65693bab7c2574c2e780eb4aee87c
|
File details
Details for the file satsignal_blob-0.3.0-py3-none-any.whl.
File metadata
- Download URL: satsignal_blob-0.3.0-py3-none-any.whl
- Upload date:
- Size: 19.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8a298c907a2321a61576cbc63f81d6d04bcbdbc3db724ccdcb36679f5b46d5e8
|
|
| MD5 |
fd96dbd8a8f77eaedb83caf8f41d4274
|
|
| BLAKE2b-256 |
ccb1395e66da14b1ff17dea84542ca964e171940f17a79ebff192805d20b2cc5
|