<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Denver Mtange</title>
    <description>The latest articles on DEV Community by Denver Mtange (@mtange).</description>
    <link>https://dev.to/mtange</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4013427%2F94fb5d01-72de-474d-984c-ca71ff629a2b.png</url>
      <title>DEV Community: Denver Mtange</title>
      <link>https://dev.to/mtange</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mtange"/>
    <language>en</language>
    <item>
      <title>Building Pontmore: From Protocol Spec to Working Standalone Escrow POC</title>
      <dc:creator>Denver Mtange</dc:creator>
      <pubDate>Thu, 13 Aug 2026 19:56:46 +0000</pubDate>
      <link>https://dev.to/mtange/building-pontmore-from-protocol-spec-to-working-standalone-escrow-poc-39gh</link>
      <guid>https://dev.to/mtange/building-pontmore-from-protocol-spec-to-working-standalone-escrow-poc-39gh</guid>
      <description>&lt;p&gt;When I opened &lt;a href="https://github.com/pontmore/protocol/pull/12" rel="noopener noreferrer"&gt;PR #12&lt;/a&gt; on the Pontmore protocol repo, I was trying to answer one question: can we define a standard way for applications to invoke an escrow service directly, without routing through a swap state machine? The answer was yes , but the path from spec to working POC to simplified protocol taught me more than I expected about designing interoperable financial infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: PIP-01 Was Discovery-Only
&lt;/h2&gt;

&lt;p&gt;PIP-01 (the Pontmore Escrow Descriptor) initially served a narrow purpose: allow agents to discover compatible escrow mechanisms for fiat-to-Bitcoin swaps. It was a discovery tool, not an execution engine.&lt;/p&gt;

&lt;p&gt;A pre-PR #12 descriptor looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"escrow_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lightning_hold_invoice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"networks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"bitcoin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lightning"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"funding_rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"required_confirmation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"invoice_held"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"release_rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"release_trigger"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"counterparty_fiat_payment_confirmed"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dispute_rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"policy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"operator_resolved"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This told an agent "this escrow exists and works with Lightning hold invoices." But it didn't tell a standalone application how to create an escrow, fund it, release it, or cancel it. Those details were implicit in PIP-02's swap state machine , you needed a swap to use an escrow. There was no path for an application to say "I need an escrow between two people, let me create one."&lt;/p&gt;

&lt;h2&gt;
  
  
  PR #12: The Standalone Service Interface
&lt;/h2&gt;

&lt;p&gt;The driving force was &lt;a href="https://github.com/pontmore/protocol/issues/11" rel="noopener noreferrer"&gt;Issue #11&lt;/a&gt;: "Define escrow service invocation in PIP-01." The solution was an optional &lt;code&gt;service&lt;/code&gt; block in the descriptor that tells applications how to talk to the escrow directly ; endpoints, authentication, operations, funding models, and release decision formats.&lt;/p&gt;

&lt;p&gt;The resulting spec defined:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Transport&lt;/strong&gt;: &lt;code&gt;https&lt;/code&gt; as the canonical transport, with room for additional transports&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication&lt;/strong&gt;: &lt;code&gt;nostr_http_auth&lt;/code&gt; (NIP-98) , your Nostr pubkey IS your identity, no bearer tokens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Canonical operations&lt;/strong&gt;: &lt;code&gt;create&lt;/code&gt;, &lt;code&gt;funding_instructions&lt;/code&gt;, &lt;code&gt;fund_status&lt;/code&gt;, &lt;code&gt;release&lt;/code&gt;, &lt;code&gt;refund&lt;/code&gt;, &lt;code&gt;split&lt;/code&gt;, &lt;code&gt;cancel&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Funding models&lt;/strong&gt;: &lt;code&gt;single_funder&lt;/code&gt;, &lt;code&gt;two_party&lt;/code&gt;, &lt;code&gt;m_of_n&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Release decisions&lt;/strong&gt;: &lt;code&gt;mutual_consent&lt;/code&gt;, &lt;code&gt;operator_decision&lt;/code&gt;, &lt;code&gt;oracle_signature&lt;/code&gt;, &lt;code&gt;application_signed_result&lt;/code&gt;, &lt;code&gt;threshold_participant_signatures&lt;/code&gt;, &lt;code&gt;split_decision&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wire contract&lt;/strong&gt;: a &lt;code&gt;schema_url&lt;/code&gt; pointing to a normative OpenAPI document&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A standalone-sufficient descriptor now carried a full service contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"escrow_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"custodial_escrow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"networks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"lightning"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"funding_rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"required_confirmation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"invoice_paid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"funding_timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"86400_seconds"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"release_rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"release_trigger"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"application_signed_result"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"refund_trigger"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"timeout_or_dispute_refund_decision"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dispute_rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"policy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"operator_resolved"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"transport"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"https"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"interface"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pontmore_escrow_http_v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://escrow.example.com/pontmore/v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"auth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"nostr_http_auth"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"operations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"create"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"funding_instructions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fund_status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"release"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"refund"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cancel"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"funding_model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"single_funder"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"two_party"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"m_of_n"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"release_decisions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"mutual_consent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"operator_decision"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"application_signed_result"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"schema_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://escrow.example.com/pontmore/v1/openapi/v1.0.0.json"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The PR also patched structural loopholes identified during implementation: cross-instance replay protection (oracle/threshold signatures now commit to the stable escrow ID), funding-phase timeout enforcement (cancelling a partially-funded escrow must refund all funded sides), and deadlock prevention (any timeout path using mutual consent must declare a non-consent fallback).&lt;/p&gt;

&lt;p&gt;PR #12 was merged on August 11, 2026 , with review feedback that would prove significant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review Feedback That Seeded PR #17
&lt;/h2&gt;

&lt;p&gt;During review, &lt;a href="https://github.com/okjodom" rel="noopener noreferrer"&gt;okjodom&lt;/a&gt; left several comments that revealed a deeper design tension:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"We could simplify this definition by deferring the service, transport and interface version to the declared OpenAPI schema doc."&lt;/p&gt;

&lt;p&gt;"Please remove the escrow state machine definition. Implementation details must not be part of the generic PIP-01 spec."&lt;/p&gt;

&lt;p&gt;"I'm quite wary of having to define these operational semantics in the escrow descriptor."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The tension was clear: PR #12 had made PIP-01 the source of truth for service behavior ; endpoints, operations, state machines, decision formats. But the reviewer wanted to pull all of that back into the referenced schema, leaving PIP-01 as a lightweight compatibility object. He approved the PR anyway , "this has provably moved us forward by a margin, let's land and iterate" — and opened &lt;a href="https://github.com/pontmore/protocol/pull/17" rel="noopener noreferrer"&gt;PR #17&lt;/a&gt; within hours to begin the simplification.&lt;/p&gt;

&lt;h2&gt;
  
  
  The POC: Building Against PR #12
&lt;/h2&gt;

&lt;p&gt;While the spec was being reviewed, I had to build a working implementation to prove the concept was viable. The result is &lt;a href="https://github.com/mk-Denver/pontmore-lightning-escrow" rel="noopener noreferrer"&gt;pontmore-lightning-escrow&lt;/a&gt; — a custodial escrow service running on Render with Blink Lightning custody and Supabase persistence. It's live at &lt;code&gt;standalone-escrow.onrender.com&lt;/code&gt; and has processed real Lightning escrows during testing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                   Nostr Relays               HTTPS Clients
                  (nos.lol etc)           (rollpot, curl, apps)
                       │                         │
                       │ kind 30361              │ NIP-98 auth header
                       │ descriptor              │ (signed kind 27235 event)
                       ▼                         ▼
              ┌─────────────────────────────────────────┐
              │           Express Server                │
              │           (Render, port 3000)           │
              │                                         │
              │  ┌──────────────┐  ┌────────────────┐   │
              │  │  NIP-98 Auth │  │  Escrow Engine │   │
              │  │  Middleware  │  │   + 5 release  │   │
              │  │              │  │  decision types│   │
              │  └──────────────┘  └────────────────┘   │
              │                                         │
              │            ┌────────────────────────┐   │
              │            │                        │   │
              │            ▼                        ▼   │
              │   ┌──────────────┐           ┌──────────────┐
              │   │   Supabase   │           │    Blink     │
              │   │   (Postgres) │           │  (Lightning) │
              │   │              │           │              │
              │   │ · escrow     │           │ · invoices   │
              │   │   instances  │           │ · payments   │
              │   │ · funders    │           │ · LN address │
              │   │ · state RPC  │           │   payouts    │
              │   └──────────────┘           └──────────────┘
              │
              └─────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Express Server&lt;/strong&gt; handles all PIP-01 operations via NIP-98 authenticated endpoints. Every mutating request carries a signed kind 27235 Nostr event in the &lt;code&gt;Authorization&lt;/code&gt; header , the authenticated pubkey IS the participant's identity. No user registration, no session management, no password resets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Escrow Engine&lt;/strong&gt; validates state transitions, verifies release/refund Schnorr signatures across five decision formats, manages opaque single-use enrollment tokens, and orchestrates multi-party funding. All state mutations go through a Postgres RPC that atomically checks the current state before transitioning , no application-level locking required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Supabase&lt;/strong&gt; provides durable Postgres storage with two tables (&lt;code&gt;escrow_instances&lt;/code&gt; for escrow metadata, &lt;code&gt;escrow_funders&lt;/code&gt; for per-participant funding state) and a &lt;code&gt;transition_escrow_state&lt;/code&gt; RPC that prevents race conditions. If two requests race to transition the same escrow, exactly one succeeds and the other gets a clean conflict error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blink&lt;/strong&gt; is the Lightning custody backend , creates BOLT11 invoices per participant (with per-invoice platform fees), detects payments via polling, and executes Lightning Address payouts on release. Payout idempotency is guaranteed via deterministic keys scoped to the escrow, purpose, and recipient.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Test Suite and Universal Tester
&lt;/h2&gt;

&lt;p&gt;No automated tests existed when I started , manual curl commands were the only validation. I built a 50-test integration suite targeting the live Render deployment, covering descriptor discovery, NIP-98 auth edge cases (5 failure modes), open enrollment with single-use enforcement, multi-party funding visibility, release decision verification, cancel authorization, idempotency across creators, state machine correctness, and OpenAPI schema compliance against live responses.&lt;/p&gt;

&lt;p&gt;All tests use ephemeral secp256k1 keypairs with no shared state between tests. Cleanup is automated via &lt;code&gt;afterAll&lt;/code&gt; hooks that cancel any created escrows.&lt;/p&gt;

&lt;p&gt;I also built a separate tool , the &lt;a href="https://github.com/mk-Denver/escrow-tester" rel="noopener noreferrer"&gt;Escrow Descriptor Tester&lt;/a&gt; ; a web UI that queries Nostr relays for kind 30361 events, runs 40+ PIP-01 spec validation checks per descriptor, and executes live service tests against standalone endpoints. It found 18 published descriptors on nos.lol, most of them &lt;code&gt;custodial_escrow&lt;/code&gt; with &lt;code&gt;operator_resolved&lt;/code&gt; dispute policy. Several had subtle spec violations: empty &lt;code&gt;required_confirmation&lt;/code&gt; strings, non-standard funding models, or missing decision signer blocks.&lt;/p&gt;

&lt;h2&gt;
  
  
  PR #17: The Simplification
&lt;/h2&gt;

&lt;p&gt;While the POC was running against the PR #12 spec, okjodom opened &lt;a href="https://github.com/pontmore/protocol/pull/17" rel="noopener noreferrer"&gt;PR #17&lt;/a&gt; — a cleanup that recasts PIP-01 from a "descriptor-defined standalone service interface" into a "compatibility/discovery object with a schema pointer." The motivation, in his words: "PIP-01 had started to carry too many responsibilities."&lt;/p&gt;

&lt;p&gt;The key changes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before (PR #12)&lt;/th&gt;
&lt;th&gt;After (PR #17)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10+ &lt;code&gt;service&lt;/code&gt; fields: transport, interface, endpoint, auth, operations, funding_model, release_decisions, decision_signers, schema_url&lt;/td&gt;
&lt;td&gt;One field: &lt;code&gt;service.schema { type, url }&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Named funding models: single_funder, two_party, m_of_n&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;funding_threshold&lt;/code&gt; / &lt;code&gt;participant_count&lt;/code&gt; cardinality (m-of-n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Descriptor-level &lt;code&gt;release_rules&lt;/code&gt; with triggers and fallbacks&lt;/td&gt;
&lt;td&gt;Removed; release and refund behavior belongs to schema_url&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Canonical state machine defined in PIP-01&lt;/td&gt;
&lt;td&gt;Removed; swap lifecycle to PIP-02, service behavior to schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subtype-specific field lists (implementations, custody_authority, release_authority, refund_authority)&lt;/td&gt;
&lt;td&gt;Trimmed to purpose, compatibility invariants, and public/private boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The new minimal descriptor, as defined in the &lt;a href="https://github.com/pontmore/protocol/blob/agent/simplify-pip01/PIP-01-escrow-descriptor.md" rel="noopener noreferrer"&gt;upstream spec&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"escrow_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"custodial_escrow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"networks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"bitcoin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lightning"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"funding_rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"funding_threshold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"participant_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"required_confirmation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"invoice_paid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"funding_timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"funding timeout"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dispute_rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"policy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"operator_resolved"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"timeout_fallback"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"operator_decision"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reference_format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bolt11_or_custodial_escrow_reference"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"openapi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://escrow.example.com/pontmore-escrow.openapi.json"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"updated_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1775559028&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The migration path is clear but disruptive. The live &lt;code&gt;standalone-escrow.onrender.com&lt;/code&gt; deployment still serves the PR #12 descriptor shape. Rollpot (the reference client) currently reads &lt;code&gt;descriptor.service.endpoint&lt;/code&gt; and &lt;code&gt;descriptor.service.funding_model&lt;/code&gt; directly. Under PR #17, it should instead fetch &lt;code&gt;service.schema.url&lt;/code&gt;, validate the referenced OpenAPI document, and discover servers, paths, security schemes, and operation metadata from the schema. The OpenAPI document already carries most of the behavior PR #17 removes from PIP-01.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Protocol design is iterative.&lt;/strong&gt; The spec went from discovery-only (pre-PR #12) to rich service interface (PR #12) to minimal compatibility object (PR #17) in the span of three weeks. Building while the spec evolves means accepting that some code will be thrown away but the discarded code teaches you what the protocol actually needs. Every field we removed from the descriptor was a field we learned didn't belong there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The descriptor is a commitment, not documentation.&lt;/strong&gt; Publishing a kind 30361 event on Nostr relays is a public declaration. Changing it means broadcasting a kind 5 deletion event and republishing. This creates healthy pressure to get the descriptor right before publishing and makes the published event a useful signal for clients who want to verify an operator keeps their promises.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-party accounting compounds.&lt;/strong&gt; Per-participant invoices, per-funder funding status, aggregate payout calculation across funded sides, proportional refunds on timeout cancellation , the edge cases multiply with each additional participant. A separate &lt;code&gt;escrow_funders&lt;/code&gt; table was essential; the alternative of cramming everything into a single escrow row would have been unmaintainable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The public/private boundary is the protocol's backbone.&lt;/strong&gt; Wallet IDs, custody backend identifiers, API keys, and bearer secrets do not belong in a public Nostr event. PR #17 enforces this boundary by making PIP-01 a compatibility object whose only service field is a schema pointer. The schema_url is where implementation details live and where they stay out of the public relay.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The POC: &lt;a href="https://github.com/mk-Denver/pontmore-lightning-escrow" rel="noopener noreferrer"&gt;github.com/mk-Denver/pontmore-lightning-escrow&lt;/a&gt;&lt;/em&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;The tester: &lt;a href="https://github.com/mk-Denver/escrow-tester" rel="noopener noreferrer"&gt;github.com/mk-Denver/escrow-tester&lt;/a&gt;&lt;/em&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;The protocol: &lt;a href="https://github.com/pontmore/protocol" rel="noopener noreferrer"&gt;github.com/pontmore/protocol&lt;/a&gt; ,&lt;a href="https://pontmore.xyz" rel="noopener noreferrer"&gt;website&lt;/a&gt;&lt;/em&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;PR #12 (merged): &lt;a href="https://github.com/pontmore/protocol/pull/12" rel="noopener noreferrer"&gt;pontmore/protocol#12&lt;/a&gt;&lt;/em&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;PR #17 (simplification): &lt;a href="https://github.com/pontmore/protocol/pull/17" rel="noopener noreferrer"&gt;pontmore/protocol#17&lt;/a&gt;&lt;/em&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;Protocol discussion: &lt;a href="https://openbitcoin.africa/t/designing-standalone-escrows-in-pontmore-pip-01/28" rel="noopener noreferrer"&gt;Open Bitcoin Africa&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>bitcoin</category>
      <category>nostr</category>
      <category>protocol</category>
    </item>
    <item>
      <title>Learning by Building: Rust for Bitcoiners 1.0</title>
      <dc:creator>Denver Mtange</dc:creator>
      <pubDate>Sun, 05 Jul 2026 10:36:26 +0000</pubDate>
      <link>https://dev.to/mtange/learning-by-building-rust-for-bitcoiners-10-41ef</link>
      <guid>https://dev.to/mtange/learning-by-building-rust-for-bitcoiners-10-41ef</guid>
      <description>&lt;p&gt;When diving into Bitcoin development, reading protocol specifications and documentation will only get you so far. At some point, you have to get your hands dirty. I recently decided to tackle learning Rust the gold standard language for modern Bitcoin infrastructure by building a raw Bitcoin keypair and address generator from scratch.&lt;/p&gt;

&lt;p&gt;I quickly discovered that combining Rust's notoriously strict compiler with Bitcoin's unforgiving cryptography is the ultimate trial by fire. Here is what I learned during "Version 1.0" of my journey, the challenges I ran into, and how compiler-driven development helped me overcome them.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Goal: A From-Scratch Legacy Wallet
&lt;/h2&gt;

&lt;p&gt;The mission was simple on paper: generate 32 bytes of secure randomness (a private key), derive the public key using Elliptic Curve cryptography, hash it down, and encode it into a human-readable legacy Bitcoin address.&lt;/p&gt;

&lt;p&gt;Setting up the project with &lt;code&gt;cargo new&lt;/code&gt; was straightforward. I also quickly learned Rust's core philosophy of safety: variables are immutable by default (like a mined block) unless explicitly marked with &lt;code&gt;mut&lt;/code&gt;. But the real lessons started when I tried to pull in external tools to do the math.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenge 1: The Bleeding Edge of Rust and Traits
&lt;/h2&gt;

&lt;p&gt;To get 32 bytes of entropy, I added the &lt;code&gt;rand&lt;/code&gt; crate to my project. Following standard tutorials, I wrote &lt;code&gt;rand::thread_rng().gen()&lt;/code&gt;. The compiler immediately threw a wall of text at me: &lt;code&gt;expected identifier, found reserved keyword 'gen'&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How I Overcame It:&lt;/strong&gt; I had stumbled right into a modern Rust ecosystem update. In the new Rust 2024 Edition, &lt;code&gt;gen&lt;/code&gt; was made a reserved keyword for a new language feature. The &lt;code&gt;rand&lt;/code&gt; crate authors had to rename their method to &lt;code&gt;random()&lt;/code&gt;, and &lt;code&gt;thread_rng()&lt;/code&gt; to simply &lt;code&gt;rng()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But fixing the names wasn't enough. The compiler then complained it couldn't find &lt;code&gt;.random()&lt;/code&gt;. This taught me about &lt;strong&gt;Rust Traits&lt;/strong&gt;. Functions don't just float in the void; they belong to specific toolbelts (Traits). The compiler acted like a senior developer sitting next to me, explicitly suggesting I add &lt;code&gt;use rand::RngExt;&lt;/code&gt; to the top of my file. Once I brought the trait into scope, I successfully generated my 32-byte array: my raw private key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenge 2: Elliptic Curves and The &lt;code&gt;Result&lt;/code&gt; Enum
&lt;/h2&gt;

&lt;p&gt;Next, I needed to derive a compressed Public Key using the &lt;code&gt;secp256k1&lt;/code&gt; crate. I passed my 32 bytes into the cryptography engine expecting a clean public key to pop out. Instead, I got a mismatched types error: &lt;code&gt;expected reference &amp;amp;SecretKey, found reference &amp;amp;Result&amp;lt;SecretKey, Error&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How I Overcame It:&lt;/strong&gt;&lt;br&gt;
This taught me two crucial concepts at once:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Rust's &lt;code&gt;Result&lt;/code&gt; Enum:&lt;/strong&gt; Rust doesn't use standard exceptions that crash your program unpredictably. Functions that &lt;em&gt;might&lt;/em&gt; fail return a &lt;code&gt;Result&lt;/code&gt; box containing either the success data or an error. I had to use &lt;code&gt;.expect()&lt;/code&gt; to unwrap the box and explicitly tell the program what to do if the math failed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Bitcoin Curve Order:&lt;/strong&gt; But why would an exact 32-byte array fail? Because of Bitcoin's math. The secp256k1 curve has a maximum limit. There is an infinitesimally small chance (less than 1 in 10^38) that 32 random bytes generate a number mathematically "too big" for the curve. Rust forced me to acknowledge and handle this edge case before my code was legally allowed to compile.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Challenge 3: Hashing and Feature Flags
&lt;/h2&gt;

&lt;p&gt;After successfully deriving my public key (and learning why modern keys start with a &lt;code&gt;02&lt;/code&gt; prefix to indicate an even Y-coordinate to save blockchain space), I put it through the standard Hash160 pipeline (SHA-256 followed by RIPEMD-160).&lt;/p&gt;

&lt;p&gt;The final step was converting this raw 20-byte payload into a readable address using the &lt;code&gt;bs58&lt;/code&gt; crate. I wrote the code to apply the &lt;code&gt;0x00&lt;/code&gt; mainnet version byte and the double-SHA256 checksum. The compiler threw one last hurdle: &lt;code&gt;no method named 'with_check_version' found&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How I Overcame It:&lt;/strong&gt;&lt;br&gt;
I learned about &lt;strong&gt;Cargo Feature Flags&lt;/strong&gt;. Because Rust is obsessed with making compiled programs as tiny as possible, crate authors often hide heavy code behind optional flags. The Base58Check algorithm is heavy, so it was turned off by default. Without changing a single line of Rust code, I ran &lt;code&gt;cargo add bs58 --features check&lt;/code&gt; in my terminal. The compiler was happy, and my terminal printed a flawless, valid legacy Bitcoin address.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion: Why Rust is Perfect for Bitcoin
&lt;/h2&gt;

&lt;p&gt;Building this simple generator taught me that the Rust compiler is not your enemy; it is your incredibly pedantic pair-programmer.&lt;/p&gt;

&lt;p&gt;Next up in my journey: adapting this tool to connect to a local Signet node and upgrading the engine to generate modern Native SegWit (Bech32) addresses.&lt;/p&gt;

&lt;p&gt;you can check the code  at &lt;a href="https://github.com/mk-Denver/btc_wallet" rel="noopener noreferrer"&gt;https://github.com/mk-Denver/btc_wallet&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Learning by building continues.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>bitcoin</category>
      <category>learning</category>
      <category>programming</category>
      <category>rust</category>
    </item>
  </channel>
</rss>
