End-to-end operator runbook for deploying the Anchr two-party binary bet on regtest (local development) or testnet (public preview). The production-style deploy uses encrypted FROST DKG keys (PR-D), the auto-resolver (PR-B), trustless TLSNotary verification (PR-C), and the browser-side Cashu bet flow (PR-E).
Live testnet deploy: https://anchr-market.fly.dev (Fly region
sin, testnut.cashu.space mint, anchr-relay Nostr relay, anchr-tlsn-verifier).
+----------------------------+
| TLSNotary verifier binary |
| (cryptographic proofs) |
+-------------+--------------+
^
| /submit-resolution
| /resolve
|
+-------------+ bet/lock/redeem +----+----+ sign/redeem +-----------+
| Browser |<------------------>| Market |<----------------->| FROST |
| (Cashu UI) | (Hono routes) | Server | | t-of-n |
+------+------+ +----+----+ | cluster |
^ ^ +-----------+
| cashuB tokens |
v | Lightning mint
+------+------+ +----+----+
| Cashu mint |<--------------------+ lnd |
| (Nutshell) | pay invoice +----------+
+-------------+
- Browser holds proofs in localStorage and creates P2PK-locked exchange tokens. Server never custodies funds.
- Market server is a pure matchmaker:
/markets,/bet,/submit-token,/resolve,/sign-proofs,/redeem,/wallet/config,/wallet/faucet. - FROST cluster issues threshold signatures over P2PK conditions
when the market resolves. Pre-1.0, FROST is optional — if
FROST_MARKET_CONFIG_PATHis unset, the server falls back to HTLC preimage reveal.
This is the fastest way to play with the full flow on your machine.
# 1. Build UI bundle + tailwind CSS once
deno task build:ui
deno task build:css
# 2. Bring up Cashu mint + lnd + nostr relay
docker compose up -d
./scripts/init-regtest.sh
docker compose restart cashu-mint
# 3. Start the two-party binary bet server
CASHU_MINT_URL=http://localhost:3338 \
NOSTR_RELAYS=ws://localhost:7777 \
MARKET_PORT=3001 \
deno run --config deno.json --allow-all example/two-party-binary-bet/server.tsOpen http://localhost:3001. You should see the empty-market home page
(screenshot) with a Cashu wallet
banner that confirms the mint URL.
Click +1,000 sats in the wallet panel — that mints a fresh Cashu
token via regtest Lightning, swaps it at the mint, and credits your
browser-only balance.
In production, you want a t-of-n threshold cluster instead of a single HTLC preimage. PR-D's encrypted DKG bootstrap covers this.
# 1. Run distributed key generation for both YES and NO outcome groups.
# The output is FROST signer config files, encrypted at rest with
# AES-256-GCM and PBKDF2-SHA256 (600k iterations).
FROST_KEY_PASSPHRASE='choose-a-strong-passphrase' \
deno run --allow-all scripts/frost-market-dkg-bootstrap.ts \
--threshold 2 --total 3 --output-dir .frost-market
# 2. Each signer node loads its own config. Boot the cluster.
deno run --allow-all scripts/frost-market-oracle-cluster.ts
# 3. Boot the market server pointing at signer-1's config.
FROST_MARKET_CONFIG_PATH=.frost-market/signer-1.json \
FROST_KEY_PASSPHRASE='same-passphrase-as-above' \
CASHU_MINT_URL=http://localhost:3338 \
deno run --config deno.json --allow-all example/two-party-binary-bet/server.tsWhat changes for the user: tokens are P2PK-locked to a 2-of-2 multisig
of [group_pubkey_no, counterparty_pubkey] (for YES bettors) — to
unlock, the winner needs both the counterparty's signature and a
threshold FROST signature from the cluster. Compromising one signer
in a 2-of-3 deploy is not enough to settle dishonestly.
Loss-of-passphrase is unrecoverable. Back up
.frost-market/signer-*.json and the passphrase out-of-band.
The auto-resolver in auto-resolver.ts polls every market past its
deadline and:
- Fetches the truth-source URL through its SSRF-hardened fetcher
(no redirects, 10 s timeout, 1 MiB body cap, allowlisted destinations
via
ALLOW_LOCAL_TRUTH_SOURCES). - Evaluates the market's
resolution_conditionon the body. - Settles via
settleMarket(...).
For full trustlessness, anyone (not just the operator) can submit a
TLSNotary proof of the truth-source response via
POST /markets/:id/submit-resolution with { tlsn_presentation }. The
server cryptographically verifies the proof, then evaluates the
condition. No one needs to be trusted to read the URL — the
binding is (server name, response body, session timestamp), all
covered by the TLSNotary signature.
This requires the TLSNotary verifier binary on the host:
cd crates/tlsn-verifier && cargo build --releaseShip the market with FROST 2-of-3 threshold signing, fronted by Fly's managed TLS, using the existing public testnut Cashu mint and the already-deployed Anchr Nostr relay + TLSN verifier.
+---------------------- anchr-market.fly.dev ----------------------+
| |
| scripts/market-cluster-entrypoint.ts (PID 1, tini) |
| ├── frost-signer-1 (127.0.0.1:4001) |
| ├── frost-signer-2 (127.0.0.1:4002) |
| ├── frost-signer-3 (127.0.0.1:4003) |
| └── market-server (0.0.0.0:8080) ←── Fly proxy → :443 |
| |
| /data (persistent volume) holds the decrypted signer-N.json |
+------------------------------------------------------------------+
│ │
▼ ▼
testnut.cashu.space anchr-relay.fly.dev
(public Cashu mint) (Nostr relay, kind 30078)
│
▼
anchr-tlsn-verifier.fly.dev (TLSN proxy, used by browser
to produce attestations)
Three FROST signer shares live in distinct processes (so they don't share an address space) but the same Fly machine. That's the threshold crypto guarantee, not geo-distribution — the next iteration is splitting into three Fly apps.
The fastest way is the Bootstrap Anchr Market workflow. It creates the Fly app + volume idempotently, runs DKG inside the runner, uploads the encrypted shares + passphrase as Fly secrets, then deploys.
- In GitHub → Settings → Secrets and variables → Actions, add:
FLY_API_TOKEN_MARKET— Fly deploy token scoped toanchr-market(flyctl tokens create deploy --name anchr-market).FROST_KEY_PASSPHRASE— generate locally withopenssl rand -hex 32and paste.
- Actions → Bootstrap Anchr Market → Run workflow.
After it succeeds, every future commit to main redeploys
automatically via the regular deploy-market job. To rotate FROST
keys, re-run the bootstrap workflow with rotate_keys=true.
# 1. Create the Fly app + volume.
flyctl apps create anchr-market
flyctl volumes create frost_data --app anchr-market --region nrt --size 1
# 2. Generate the FROST 2-of-3 DKG output, encrypt with a passphrase,
# and emit the `flyctl secrets set` command.
FROST_KEY_PASSPHRASE=$(openssl rand -hex 32) \
deno run --allow-all scripts/frost-market-prepare-secrets.ts --app anchr-market
# 3. Run the printed `flyctl secrets set …` command. It uploads the
# passphrase + 3 base64-encoded encrypted configs as Fly secrets.
# 4. Deploy.
flyctl deploy --remote-only --config fly.market.tomlThe orchestrator decrypts each FROST_SIGNER_{1,2,3}_CONFIG_B64 to
/data/signer-N.json (mode 0600) on boot, spawns the cluster on
127.0.0.1:4001-4003, then starts the market server.
Re-run frost-market-prepare-secrets.ts and re-deploy. Markets
created under the old group pubkeys can no longer be settled — drain
them first, or pin to a fixed htlc_hash_yes / htlc_hash_no and use
the HTLC fallback path for those legacy markets.
- Public Cashu mint:
https://testnut.cashu.space(set infly.market.toml). - Public Nostr relay:
wss://anchr-relay.fly.dev(the server publishes markets as Nostr kind 30078 on creation). - TLSN verifier proxy:
https://anchr-tlsn-verifier.fly.dev(browser hits it for proof generation; the market server runs the verifier binary locally to validate submitted presentations). - FROST 2-of-3 with passphrase-encrypted shares (PR-D).
- TLS via Fly (
force_https = trueinfly.market.toml). - Add API key or NIP-98 auth middleware in
server.ts(writeAuth/rateLimitcurrently no-op for the demo). Wire it to your KMS. - Run the screenshot script as a smoke test in CI:
deno run --allow-all scripts/market-screenshots.ts.
# In-process test of the full lifecycle (skips when mint isn't reachable):
deno test --allow-all e2e/two-party-binary-bet-lifecycle.test.ts
# Just the unit + route tests (no Docker needed):
deno task test:example
# Full local run:
deno task test:all| Step | Screenshot |
|---|---|
| Empty markets list (default first run) | 01-empty-markets.png |
| Create-market form | 02-create-market-form.png |
| Market list with one seeded market | 03-market-list.png |
| Market detail + bet panel | 04-market-detail.png |
Re-capture them after UI changes:
deno task build:ui && deno task build:css
deno run --allow-all scripts/market-screenshots.tsThe script boots the market server in-process on port 3098, drives
headless Chromium via Playwright through the empty state and the
create-market form, optionally seeds one demo market, and writes
the screenshots to docs/two-party-binary-bet/screenshots/.
- The browser-side mint redemption step (combining
oracle_sig+own_sigand swapping at the Cashu mint) is documented inMarketDetail.tsx's "Settlement" panel but not yet auto-driven by the UI. Users currently copy the held cashuB token + the oracle signatures out and use a Cashu CLI to redeem. PR-H will close this. - The auth middleware is a no-op (
noopMiddlewareinserver.ts). Swap in API-key or Nostr-NIP-98 auth before exposing publicly. - TLSNotary verifier binary must be installed manually. We don't ship prebuilt binaries yet.