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 apexsatsignal.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:


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:

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:

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.