Building with Satsignal from mobile apps and edge runtimes
What you can call from a browser, a Cloudflare Worker, a Deno Deploy isolate, or a phone webview — and what has to stay on the server side.
For the underlying canonical-doc shape and on-chain wire format, read /spec. For agent-specific patterns (policy snapshot, decision commitments, manifest), read /agents.
TL;DR
[mobile UI / edge runtime]
| read-side: any of the browser-callable surfaces below
|
| anchor-side: forward to your own backend
v
[your backend] --(API key)--> [proof.satsignal.cloud/api/v1/anchors]
The read side (lookups, verification, embedded badges, deep links into the verifier) runs entirely from a browser or edge runtime. Anchoring runs through your backend, because the API key MUST NOT be exposed to client JavaScript.
The four browser-callable surfaces
All four are CORS-friendly and need no API key.
1. GET /lookup_hash?sha=<64-hex> — "is this sha256 anchored on BSV?" Public, read-only, rate-limited per IP. Returns {"bundle_id":"...","created_utc":"...","txid":"..."} on hit, {} on miss. Access-Control-Allow-Origin: * on every response (including errors), so a fetch() from any origin works without a proxy.
const r = await fetch(
"https://proof.satsignal.cloud/lookup_hash?sha=" + sha256hex);
const j = await r.json();
// j.txid is set if anchored; {} if not.
/lookup_hash is an existence oracle keyed on sha256(file_bytes) (or, for merkle-row schemes, on the commit-doc sha). It does NOT resolve sealed-mode anchors (excluded by design — see SPEC_v2_sealed.md §7.2) or manifest-items-v1-style multi-file bundles (no single naked sha to key on — verify those by holding the bundle and chain-confirming the txid).
2. https://proof.satsignal.cloud/widget.js — drop-in "anchored on BSV" badge for any third-party page.
<div class="satsignal-verify"
data-sha="<64-hex sha256>"
data-bundle-url="<optional .mbnt url>"></div>
<script src="https://proof.satsignal.cloud/widget.js" async></script>
Renders a green/gray/amber chip with a deep link out to /verify. Idempotent and async-safe. Embedder's CSP needs to allow script-src https://proof.satsignal.cloud and connect-src https://proof.satsignal.cloud.
3. GET /verify?bundle_b64=<base64> and ?bundle_url=<url> — deep-link auto-load on the public verifier page. bundle_b64 is pure client-side (no fetch); bundle_url performs a same-origin fetch (the page's CSP connect-src 'self' only allows proof.satsignal.cloud URLs, which is what you want for /bundle/<id>.mbnt and /static/*.mbnt deep links).
4. The canonicalization helpers at the apex — satsignal.cloud/commit-reveal.js and satsignal.cloud/merkle-row.js. Vanilla ES modules, no dependencies. They produce the byte-exact canonical form that the chain anchored, so a client can compute a local hash and compare it to a /lookup_hash answer without ever trusting a server with the plaintext.
Anchoring is server-side only
POST /api/v1/anchors is intentionally NOT CORS-enabled. A browser preflight against it returns 501 — that is by design, not a bug.
The reason: anchoring requires a Bearer API key (Authorization: sk_live_...). If you opened CORS to the anchor endpoint, integrators would inevitably embed the key in client JavaScript, which would leak it to every visitor's devtools and network log. The 501 forces the right architecture:
[mobile app]
| POST /your/own/anchor with the user's payload
v
[your backend] -- holds the API key, calls --> /api/v1/anchors
| (returns receipt + txid)
v
[mobile app] <-- relays the receipt back to the client
Your backend can be tiny — a serverless function, an edge worker with the API key in a secret binding, or the API server you already have.
If you find yourself wanting to call /api/v1/anchors from a browser because "it would just be easier," stop. That convenience is exactly the leak the 501 prevents.
Web Crypto and secure contexts
The canonicalization helpers (commit-reveal.js, merkle-row.js) use crypto.subtle.digest for SHA-256. That requires a secure context — HTTPS or localhost. On an HTTP origin, the helpers throw:
crypto.subtle not available — need a secure context (https://)
What this means in practice:
- Production: deploy over HTTPS.
https://your-app.exampleworks. - Local dev: load via
http://localhost:<port>orhttp://127.0.0.1:<port>— these are secure contexts by spec. - Android emulator hitting the host machine via
http://10.0.2.2:<port>: not a secure context. Either run a self-signed HTTPS cert or proxy through localhost. - Edge runtimes: Cloudflare Workers, Deno Deploy, Vercel Edge, and Node 18+ all expose
crypto.subtleas a global, so the helpers work without changes.
QR codes and deep links
A complete .mbnt bundle is small. The standard demo is 836 bytes; the sealed demo is 922. After base64 encoding, that fits comfortably inside a single QR code.
QR-encode a bundle for in-person verification:
import base64, qrcode
with open("receipt.mbnt", "rb") as f:
b64 = base64.urlsafe_b64encode(f.read()).rstrip(b"=").decode()
url = "https://proof.satsignal.cloud/verify?bundle_b64=" + b64
qrcode.make(url).save("receipt-qr.png")
A scanner opens the URL, the verifier page decodes the bundle in JavaScript, runs the full pill verification, and renders the result. No network round-trip to fetch the bundle.
Deep-link to a hosted bundle when you don't want to embed the bytes in the URL:
https://proof.satsignal.cloud/verify?bundle_url=https://proof.satsignal.cloud/bundle/<id>.mbnt
Both query params auto-trigger the verify flow; the visitor lands on a fully-rendered result panel without clicking through a file picker.
The widget
For a regulator portal, a journalist's microsite, an internal compliance dashboard, or any other third-party page that just wants to show "this hash is anchored on BSV" inline, drop in the widget:
<p>Receipt for invoice #2024-1138:</p>
<div class="satsignal-verify"
data-sha="b0205959420906f936c32d3faad859f92f48110f5a25033fca6599ca56b16b31"
data-bundle-url="https://proof.satsignal.cloud/bundle/8018ba7ecf914d8e.mbnt"></div>
<script src="https://proof.satsignal.cloud/widget.js" async></script>
The widget reads data-sha, calls /lookup_hash, and renders one of:
✓ Anchored on BSV · tx <prefix>… · verify ↗(green chip)○ Not anchored on BSV · verify ↗(gray chip)⚠ Lookup failed (...)(amber chip)
The verify link opens /verify in a new tab. If you supplied data-bundle-url, the link is the deep-link form (/verify?bundle_url=...) so the visitor lands on a fully-rendered verification panel.
For inline full pill verification (rather than just the anchored status badge), embed /verify itself in an iframe — but note that proof.satsignal.cloud sets frame-ancestors 'none', so iframing is currently blocked. If you need this, file an issue describing the embedding origin and we'll talk through what a relaxed CSP for an embed-specific path would look like.
What is NOT browser-callable
For completeness, the surfaces that stay strictly server-side or same-origin:
POST /api/v1/anchors— anchoring (API key required; never expose to client JS).POST /notarize— legacy form-based anchoring (basic-auth gated).GET /bundle/<id>.mbntfor sealed bundles — auth-gated; the bearer-token URL is shared out-of-band with the recipient. Standard-mode bundles are public but not currently CORS-open for cross-originfetch(); use the deep-link/verify?bundle_url=...form instead.GET /admin— operator dashboard, gated.
If you find a use case that argues for opening CORS on one of these, we want to hear it. The default is closed because each opening is a security review, not a config tweak.
Source: docs/notary_spec/MOBILE.md.
Email hello@satsignal.cloud
for clarifications.