kind 35128 · 2026-10-10 06:20 UTC
Two-of-three escrow — a buyer, a seller, and an arbiter who can't cheat each other
Two-of-three escrow on Nostr A single HTML file that lets a buyer, a seller, and an arbiter hold money in escrow without trusting each other, without trusting a company, and without trusting a blockchain — just three keypairs, a set of Nostr relays, and a bit of cryptography that enforces the rule: any two of the three can move the money, and no single one can. There is no server, no build step, and no signup. You open the file, you pick a role, and you're an escrow participant. WHY THIS EXISTS --------------- Normal escrow requires a company in the middle: a payment processor, a bank, a marketplace. That company holds the money, decides who's right in a dispute, and can freeze you out whenever it wants. It's also a single point of failure and a single point of censorship. This project replaces that company with three things. 1. A 2-of-3 threshold signature (FROST on secp256k1). The three participants each hold one share of a single public key. Two shares together produce a valid signature; one share alone produces nothing. This is the "escrow address" — funds sent to it can only leave when two of the three parties agree. 2. Nostr relays as the message bus. All coordination — proposing the deal, handing out shares, signing a release, raising a dispute, resolving it — travels as ordinary signed Nostr events over public relays. There is no coordination server. If a relay goes down, you switch relays. 3. A client-side state machine that shows the current phase of the escrow and only offers the actions that are valid right now. The rules are enforced by the cryptography, not by the UI; the UI just makes them visible. The result: a buyer, a seller, and an arbiter can agree on terms, lock funds, and settle up — with no middleman and no way for any single participant to walk off with the money. WHAT EACH ROLE CAN AND CANNOT DO -------------------------------- Role Holds Can do Cannot do -------- --------------- --------------------------------- --------------------------------- Buyer one FROST share fund the escrow; co-sign release move funds alone; force a refund alone Seller one FROST share receive funds on delivery move funds alone Arbiter one FROST share co-sign with whichever side is right move funds alone; act without a counterparty Any two of the three together can authorize a release or a refund. No one acting alone can do anything. THE LIFECYCLE ------------- idle -> proposed -> keygen -> funded -> in_progress -> delivered -> release_pending -> released (happy path) -> disputed -> resolving -> resolved (dispute path) -> refund_pending -> refunded (timeout path) -> cancelled (mutual cancel) -> failed (terminal error) Every transition is a signed Nostr event. Public coordination uses kind 30078 (app-specific data) keyed by d = "escrow:<uuid>". Private coordination — handing out FROST shares, dispute notices — uses NIP-17/NIP-59 gift wrap (a kind 13 seal inside a kind 1059 wrap). The relay only sees a random pubkey and an opaque blob. QUICK START ----------- 1. Open the file. There is no build step; it's a single .html document. Modern browsers run it as-is, pulling nostr-tools, @frostr/bifrost, and @frostr/igloo-core from an ESM CDN. 2. If you have a NIP-07 browser extension (Alby, nos2x, etc.), the app uses it automatically for signing. If not, it falls back to an ephemeral in-memory key that's clearly labelled "demo only". 3. Press "Demo mode". This spawns three local actors in the page so you can walk the entire lifecycle — propose, keygen, fund, deliver, dispute, resolve — without real counterparties and without real funds. The real FROST math and the real relay message flow still run. If you have DevTools open, you can watch the WebSocket traffic. 4. On first login, a guided tour runs. It spotlights each major panel — your identity, the relay status, the relay preferences panel, the wizard, the escrow detail view, the timeline, diagnostics — and can be skipped, resumed, or restarted from the header or the Diagnostics panel. YOUR RELAYS ARE YOURS --------------------- The app never overwrites the relay list you choose. Once you save a list — whether you fetched it from your own NIP-65 (kind:10002) event or typed it in manually — it lives under escrownostr.userRelays.v1 in localStorage and the boot sequence will not touch it again. Only an explicit user action changes it: - Fetch my NIP-65 relays — pulls your kind:10002 event and saves the result with source: 'nip65'. - Edit manually — a textarea, one URL per line, ws:// rejected outside localhost. - Reset to default — clears the preference entirely. The relay pool opens at most 4 concurrent WebSocket connections at once, no matter how long your list is. The rest are kept as standbys and swapped in automatically if an active relay fails repeatedly or drops its socket. The full list is preserved in storage; the pool only decides which subset to activate at any given moment. This matters because the alternative — opening a WebSocket per relay — turns a long NIP-65 list into a battery and bandwidth problem, and a single flapping relay into a reconnect storm. NOSTR EVENTS USED ----------------- Kind Purpose ----- ------------------------------------------------------------- 30078 Escrow proposal, dispute, and resolution (addressable) 1 Funding notice, delivery confirmation 13 Inner seal — signed by the real sender 1059 Outer gift wrap — signed by a random one-time key 9735 Zap receipt (validated when funding via Lightning) 10002 NIP-65 relay list (fetched once; never overwritten) 27235 NIP-98 HTTP auth (used only when a relay demands AUTH) FROST CEREMONY -------------- Each role instantiates a BifrostNode (from @frostr/bifrost) with its own FROST share. To release or refund: 1. The initiator calls node.req.sign(payload, { type: 'message', peers: [counterparty] }). 2. Bifrost exchanges /sign/sender/req and /sign/handler/req messages over relays using NIP-44-encrypted, gift-wrapped transport. 3. The returned aggregate signature is the escrow release proof. Peer policies limit which counterparties a given role can send to or accept from at any point in the state machine, so the network itself can't trick a node into co-signing something it shouldn't. SECURITY NOTE — READ THIS BEFORE YOU SEND REAL MONEY ---------------------------------------------------- This is a demonstration. Real custody requires: 1. An audited Bitcoin transaction builder that constructs a real 2-of-3 P2WSH script and signs its BIP-340 sighash. This app exercises the FROST threshold-signature math and the Nostr coordination layer, not the on-chain transaction construction. 2. Hardware-backed share storage. An in-browser FROST share is only as safe as the browser tab it lives in. 3. Careful key hygiene — never paste an nsec into a web page, never reuse identity keys across roles, never run this on a machine you don't trust. 4. Independent audit of the FROST implementation and of the escrow logic. Do not send real funds to any address produced by this page. HOW THE FILE IS ORGANIZED ------------------------- Everything lives in one HTML file, split into // #region markers so you can jump between them in any editor: #region config relays, timeouts, retry policy, feature flags, connection cap #region types every type, every discriminated union, every branded ID #region schemas Zod schemas for every external payload #region errors structured error classes #region utils logging, timeouts, retry, URL normalization #region state the escrow state machine (exhaustive reducer + never assertion) #region persistence versioned localStorage #region relay-prefs NIP-65 fetch, user relay preference, cap logic #region nostr-client RelayPool, NIP-17/NIP-59 gift wrap, signers #region frost wrappers around @frostr/igloo-core #region escrow-orchestrator ties the reducer, relay pool, FROST nodes, and persistence together #region ui render-on-state-change UI (no framework, no virtual DOM) #region tour the first-login guided wizard #region demo three-actor demo mode #region boot startup CODING STANDARD --------------- This file follows a strict internal checklist, applied to every line: - Zod validation at every trust boundary — network, disk, DOM. - Exhaustive discriminated-union state with never assertions so the compiler catches unhandled cases. - No floating promises; every async path has a try/catch or an explicit void with a justification. - Bounded retry with jitter on every network call. - Every user-facing string and number goes through a formatter that handles undefined, NaN, and out-of-range values. - Stable list keys — never array indices. - Versioned persistence with a schema check; corrupted data resets cleanly. - No any, no !, no as without a runtime guard. CONTRIBUTING ------------ DMs about project are welcome. Please keep the single-file constraint — the whole point is that a person can open one HTML document and use the app. If you're adding a feature, do it inside the existing #region blocks rather than introducing a build step. For bug reports, include: - What you did - What you expected - What happened - The output of window.__escrowDebug (paste it as JSON) - Your browser and version LICENSE ------- MIT. See LICENSE.
Missing files
These files were checked. They were not on the hosts listed here.
| path | sha256 | missing on |
|---|---|---|
| /__MACOSX/._index.html | ab8d280ad91805ff59bf228ab8fbe4c1ac4a81e8b5619cf37b0049d9961864f1 |
https://nsite.run · missing
|
Manifest
- event id
- fd84c75ce6b4bbc7ccdc7b6412536250fcdf64fe3fb566e71105b7032657f07a
- aggregate x
- None
- d tag
- escrow-nostr
Paths
| path | sha256 | hosts | |
|---|---|---|---|
| /index.html | 1893e39b821f2e98409569c0952d10f9e11b72247cd66e791045238f20a35a70 |
https://nsite.run · degraded
https://nostr.download · degraded
https://blssm.us · degraded
https://blossom.yakihonne.com · degraded
https://zencrown.relay2aeon.quest/ · degraded
|
preview |
| /__MACOSX/._index.html | ab8d280ad91805ff59bf228ab8fbe4c1ac4a81e8b5619cf37b0049d9961864f1 |
https://nsite.run · missing
https://nostr.download · degraded
https://blssm.us · degraded
https://blossom.yakihonne.com · degraded
https://zencrown.relay2aeon.quest/ · degraded
|
preview |
| /404.html | 1893e39b821f2e98409569c0952d10f9e11b72247cd66e791045238f20a35a70 |
https://nsite.run · degraded
https://nostr.download · degraded
https://blssm.us · degraded
https://blossom.yakihonne.com · degraded
https://zencrown.relay2aeon.quest/ · degraded
|
preview |