← observatory

kind 35128 · don't trust, verify

fd84c75ce6b4bbc7ccdc7b6412536250fcdf64fe3fb566e71105b7032657f07a

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