BIP 77: Async Payjoin
2023-08-08
View on GitHub
  BIP: 77
  Layer: Applications
  Title: Async Payjoin
  Author: Dan Gould <d@ngould.dev>
          Yuval Kogman <nothingmuch@woobling.org>
  Comments-URI: https://github.com/bitcoin/bips/wiki/Comments:BIP-0077
  Post-History: https://github.com/bitcoin/bips/pull/1483
                https://gnusha.org/pi/bitcoindev/7B11AE34-27A7-46ED-95BF-66CA13BA26F3@ngould.dev/#t
                https://gnusha.org/pi/bitcoindev/3C0A6E4C-444E-4E75-829C-1A21D8EE40E0@ngould.dev/#t
  Status: Draft
  Type: Standards Track
  Created: 2023-08-08
  License: BSD-2-Clause
  Requires: 21, 78, 173, 174

This BIP is licensed under the 2-clause BSD license.

Abstract

Payjoin lets Bitcoin senders and receivers interact to make batched transactions.

This document proposes a second, backwards-compatible, asynchronous version of the Payjoin protocol ("Version 2") relative to and described in BIP 78 ("Version 1"). An untrusted third-party "directory server" replaces the requirement for a receiver to host a secure public endpoint for interactions. HTTP clients access the directory server using an asynchronous protocol and authenticated, encrypted payloads. The design preserves complete Payjoin receiver functionality, including payment output substitution. Authenticated encryption depends only on cryptographic primitives available in Bitcoin Core. Requests use Oblivious HTTP (OHTTP) to prevent the directory and other Payjoin clients from linking requests to client IP addresses.

Motivation

Satoshi Nakamoto pointed out one specific privacy risk in the whitepaper, that transactions with multiple inputs "necessarily reveal that their inputs were owned by the same owner." Payjoin addresses that risk, the common-input-ownership heuristic, by making it practical to spend inputs owned by multiple parties in one transaction.

While addressing Bitcoin's primal privacy risk, Payjoin input batching also improves on the widespread non-interactive output batching practice deployed by exchanges. When combined, the same movement of funds can use less block weight and save fees.

A natural application of Payjoin would be to combine getting paid with consolidating UTXOs into one transaction. But Payjoin can also secure transaction cut-through, allowing a sender to transfer funds to a receiver who also transfers funds to a third party in the same transaction. For example, deposits to an exchange may "cut through" in a single transaction that also satisfies withdrawals instead of with a second transaction that spends the deposited funds. Payjoin enables more blockspace-efficient transactions that reduce fees while addressing privacy risks.

However, BIP 78's requirements for Payjoin Version 1 have proven to be an obstacle to adoption. Version 1 receivers must host a secured public-facing HTTP server. Mobile and web environments limit the ability to fulfil such a requirement. Version 1 also requires synchronous communication. Both sender and receiver must be online simultaneously. Wallet developers regard these requirements as barriers to Payjoin adoption.

To address these limitations, our goal is to specify a practical coordination mechanism suitable for widespread implementation. This proposal leverages mature solutions to common problems, building on established web standards and proven Bitcoin primitives.

Overview

A Payjoin sender and receiver interact so that they may both contribute to a transaction. In this proposal, they exchange asynchronous end-to-end encrypted messages by relaying them to a store-and-forward directory server using OHTTP.

Before initiating the protocol, the receiver must secure communications with the directory by bootstrapping.

At any point, either party may choose to broadcast the fallback transaction described by the Original PSBT instead of proceeding. Because the Original PSBT and Proposal PSBT spend the same input(s) they are mutually exclusive and only one can be confirmed.

Messages are buffered in the directory, allowing both parties to tolerate temporary disconnections and resume communication by polling.

Sequence Diagram

sequenceDiagram
    title Async Payjoin Sequence Diagram
    participant R as Receiver
    participant D as Directory
    participant S as Sender
    participant N as Network

    R-)S:  Payjoin URI (BIP 21) out of band
    
    R-->>D: Poll GET Requests<br/>for Original PSBT
    activate D 
    S->>D: POST Request<br/>Original PSBT
    D->>R: GET Response<br/>Original PSBT
    deactivate D

    S-->>D: Poll GET Requests<br/>for Proposal PSBT
    activate D
    R->>D: POST Request<br/>Proposal PSBT
    D->>S: GET Response<br/>Proposal PSBT
    deactivate D

    S->>N: Broadcast Payjoin

Specification

OHTTP Bootstrapping

Before initiating a Payjoin Session a receiver must first discover the directory's OHTTP Key Configuration, via an authenticated bootstrap mechanism. The key configuration contains information to establish Hybrid Public Key Encryption (HPKE) in order to secure communications between the client and the directory in lieu of TLS.

The bootstrap mechanism may vary by implementation but must follow OHTTP Consistency Requirements and should not reveal a receiver IP address to the directory. Some examples of suitable mechanisms include getting a key configuration from a Payjoin URI, a trusted application binary, or fetching using https-in-http CONNECT method, https-in-WebSocket, Tor, or a VPN.

Directory OHTTP Gateways MUST support RFC 9540 Key Configuration Fetching via GET request. RFC 9540 defines the gateway location as /.well-known/ohttp-gateway.

Session Initiation

A receiver initiates a session by sharing a Payjoin URI. Because a URI contains sensitive information, such as a receiver address, it should be shared over a confidential channel.

Payjoin URI

Bitcoin URIs (BIP 21 or BIP 321) are a standard way to request bitcoin.

A Payjoin URI is a Bitcoin URI that contains a pj parameter. The pj parameter value is a URL in both BIP 78 and BIP 77.

Senders that understand Bitcoin URI but don't support Payjoin will just ignore the pj parameter and proceed to typical address-based transaction flows.

A req-pj parameter may be used as a BIP 21 forwards compatibility reqparam instead of pj to signal that Payjoin is required.

The parameter value must be uppercased and the parameter should be placed last in the URI.

Since BIP 78 payloads are neither encrypted nor authenticated, a directory used for backwards-compatible payloads is known as an "unsecured payjoin server" in BIP 78 parlance. Backwards-compatible receivers MUST disable output substitution by setting pjos=0 to prevent modification by a malicious directory.

Mailbox endpoint

In this proposal the URL in the pj parameter value is the mailbox endpoint URL. Mailboxes are shared HTTP resources hosted by the directory and serve as OHTTP Target Resources. Clients use these endpoints to relay encrypted messages. They POST messages to and GET messages from mailbox endpoints via OHTTP.

Senders that support BIP 78 but not this proposal may POST messages directly to mailbox endpoints for backwards compatibility.

Short ID

A Short ID identifies a mailbox based on its associated public key. The Short ID is the path component of the mailbox endpoint. One is derived by hashing the 33-byte compressed public key encoding with SHA-256, truncating it to 8 bytes (64 bits), and encoding it in uppercase using the bech32 character set (like a bech32 string without the HRP, separator and checksum).

Receiver fragment parameters

This proposal introduces session-specific parameters which the receiver shares encoded in the URI.

Instead of defining new Bitcoin URI parameters, the session-specific parameters are encoded in the fragment of the mailbox endpoint URL.

The # fragment separator character must be RFC 3986 percent-encoded as %23, because it separates the fragment of the mailbox endpoint URL included in the pj parameter, not the fragment of the Bitcoin URI.

These session-specific parameters use a bech32-inspired encoding. The HRP is used as the parameter key, followed by the '1' separator, followed by the parameter value encoded using the bech32 character set in uppercase. No checksum is used. Parameters are separated by a + character.

The following parameters are defined, and must be provided in reverse lexicographical order:

  • RK: encodes the receiver key as a 33-byte compressed public key. Senders will initiate HPKE with the receiver using this key.
  • OH: encodes an alternate format of the OHTTP Key Configuration of the directory. It consists of a 33-byte compressed public key of the directory's OHTTP Gateway, prefixed by the 2-byte Key Identifier. A RFC 9458 Key Configuration is reconstructed by assuming the HPKE KEM ID and Symmetric Algorithms are fixed.
  • EX: specifies a session expiration in unix time.

For example, a properly encoded endpoint Bitcoin URI looks like this bitcoin:tb1q6q6de88mj8qkg0q5lupmpfexwnqjsr4d2gvx2p?amount=0.00666666&pjos=0&pj=HTTPS://PAYJO.IN/TXJCGKTKXLUUZ%23RK1Q0DJS3VVDXWQQTLQ8022QGXSX7ML9PHZ6EDSF6AKEWQG758JPS2EV+OH1QYPM59NK2LXXS4890SUAXXYT25Z2VAPHP0X7YEYCJXGWAG6UG9ZU6NQ+EX1WKV8CEC

Sender Original PSBT Messaging

The sender constructs the fallback transaction, a typical transaction spending funds to the receiver's address specified in the Payjoin URI. This transaction is serialized as a BIP 174 PSBTv0, satisfying the receiver checklist.

The Original PSBT MUST:

  • Include complete UTXO data.
  • Be fully signed.
  • Exclude unnecessary fields such as global xpubs or keypath information.
  • Be broadcastable.

The Original PSBT MAY:

  • Include outputs unrelated to the sender-receiver transfer for batching purposes.

This Original PSBT is encoded as base64, followed by the query parameter string on a new line containing optional sender parameters.

The sender generates an ephemeral mailbox key. The corresponding public key is known as the reply key, and it is prepended to the base64 plaintext string, serialized in compressed form as 33 bytes.

This plaintext string is encrypted to the receiver key according to HPKE Base mode. The HPKE info string, used for domain separation, is PjV2MsgA. The ciphertext ensures message secrecy and integrity when passed to the receiver using the mailbox endpoint. The 16-byte authentication tag is appended to the ciphertext.

RFC 9180 does not specify the wire format encoding of HPKE messages. To construct an HPKE payload, the secp256k1 public key from the DHKEM is encoded using ElligatorSwift in 64 bytes. Note that ElligatorSwift is only the wire format; when deriving shared secrets, the curve point is re-serialized in uncompressed form.

PjV2MsgA Byte Representation (7168 bytes total)
+---------------------------------------------------------------------------------------+
| ElligatorSwift |                             Ciphertext                               |
|   (64 bytes)   |                            (7104 bytes)                              |
|                +-----------------------+---------------------------------+------------+
|                |       Reply Key       |         Padded Plaintext        |  AEAD Tag  |
|                |       (33 bytes)      |   (7055 bytes = 7168-64-33-16)  | (16 bytes) |
+---------------------------------------------------------------------------------------+

The resulting HPKE payload is the body of a POST request to the receiver's mailbox. This request is then encapsulated according to Oblivious HTTP to the directory's OHTTP Gateway. OHTTP serializes the inner request as BHTTP, and provides another layer of HPKE encryption, between the client and directory.

Upon receipt, the directory's OHTTP Gateway decapsulates the OHTTP request and handles the inner POST request at the receiver's mailbox endpoint, which stores the HPKE encrypted payload to be forwarded to the receiver.

The sender then polls OHTTP encapsulated GET requests to the sender's mailbox endpoint until it receives a response from the directory containing the receiver's Proposal PSBT, and proceeds to sign and broadcast. It stops polling after expiration.

Optional sender parameters

BIP 78's optional sender parameters may be used in this proposal, but must be included in the body as part of the ciphertext rather than as a query string.

HPKE binds ciphertexts to application-specific info strings. Because of this domain separation, BIP 78's v parameter is redundant and should be omitted for this proposal.

Receiver Proposal PSBT Messaging

After sharing the Payjoin URI with the sender, the receiver polls via OHTTP encapsulated GET requests to the receiver's mailbox endpoint. So long as the mailbox contains no message, the directory responds with status 202 ACCEPTED. Once a mailbox contains a message, the directory returns it in the response body with status 200 OK.

Upon receiving an encapsulated 200 OK response, the receiver decrypts the payload and checks the Original PSBT therein according to the receiver checklist.

The receiver then updates the Original PSBT to include new signed inputs and outputs, invalidating the sender's signature(s). The receiver may also adjust the transaction fee. The result, called the Proposal PSBT, must satisfy the sender checklist

The Proposal PSBT MUST:

  • Include complete UTXO data.
  • Include all inputs from the Original PSBT.
  • Include all outputs which do not belong to the receiver from the Original PSBT.
  • Use a random index if additional inputs or outputs are added.

The Proposal PSBT sender MAY:

  • Add inputs at random indices.
  • Add outputs at random indices.
  • Remove or modify Original PSBT outputs under the control of the receiver (i.e. not sender change).

The Proposal PSBT MUST NOT:

  • Shuffle the order of inputs or outputs contained in the Original PSBT.
  • Decrease the absolute fee of the Original PSBT.

The receiver encrypts the Proposal PSBT to the sender's reply key according to HPKE Auth mode, using the receiver's key for authentication. The HPKE info string is PjV2MsgB. The HPKE wire format is the same as in the sender's message.

PjV2MsgB Byte Representation (7168 bytes total)
+---------------------------------------------------------------------------------------+
| ElligatorSwift |                             Ciphertext                               |
|   (64 bytes)   |                            (7104 bytes)                              |
|                +---------------------------------------------------------+------------+
|                |           Padded Plaintext                              |  AEAD Tag  |
|                |       (7088 bytes = 7168-64-16)                         | (16 bytes) |
+---------------------------------------------------------------------------------------+

The receiver makes the resulting HPKE payload the body of a POST request to the sender's mailbox whose Short ID is derived from the sender's reply key. This request is then encapsulated according to Oblivious HTTP to the directory's OHTTP Gateway. OHTTP serializes the inner request as BHTTP, and provides another layer of HPKE encryption, between the client and directory.

Once the receiver makes this request, they wait for either transaction from the Original PSBT or Proposal PSBT to be broadcast to the Bitcoin network.

Receiver's Original PSBT checklist

The receiver checklist is the same as the BIP 78 receiver checklist.

Sender signing and broadcast

The sender validates the Proposal PSBT it receives against a checklist. If the checks pass, it may sign and broadcast the resulting Payjoin transaction.

Sender's Proposal PSBT checklist

This proposal's sender checklist is the same as the BIP 78 sender checklist.

Client/Directory interactions

The Payjoin Directory provides a rendezvous point for senders and receivers to exchange messages. The directory stores Payjoin payloads to support asynchronous communication. Async Payjoin requests must be submitted as encapsulated messages to the directory's OHTTP Gateway.

The wire format OHTTP request is specified in RFC 9458. HPKE requires the directory's OHTTP key configuration. The plaintext is a binary encoded HTTP request (RFC 9292) intended for the OHTTP target resource, usually a mailbox endpoint, padded to 8104 bytes with random data.

OHTTP Encapsulated Request Byte Representation (8192 bytes total)
+--------------+-------------------------+------------------------------------------+
| OHTTP Header |         HPKE KEM        |               Ciphertext                 |
|  (7 bytes)   | Uncompressed Public Key |        (8120 bytes = 8192-65-7)          +
|              |        (65 bytes)       +-----------------------------+------------+
|              |                         |     Padded BHTTP Request    |  AEAD Tag  |
|              |                         | (8104 bytes = 8192-65-16-7) | (16 bytes) |
+--------------+-------------------------+------------------------------------------+

Response encryption uses the Export functionality of the request HPKE context to establish a shared secret, and therefore consists of a 32 byte nonce followed by the AEAD ciphertext and tag.

OHTTP Encapsulated Response Byte Representation (8192 bytes total)
+---------------------+------------------------------------------+
|        Nonce        |               Ciphertext                 |
|      (32 bytes)     |          (8160 bytes = 8192-32)          +
|                     +-----------------------------+------------+
|                     |     Padded BHTTP Response   |  AEAD Tag  |
|                     |   (8144 bytes = 8192-32-16) | (16 bytes) |
+---------------------+------------------------------------------+

GET requests on an empty mailbox should block until a message is posted or a timeout occurs. The timeout should be 30 seconds because that will not exceed the default timeout for most HTTP clients.

The directory may optionally accept HTTP/1.1 POST requests without OHTTP to mailbox endpoint URLs for backwards compatibility with BIP 78 senders.

OHTTP Sequence Diagram

sequenceDiagram
  title OHTTP Sequence Diagram
  participant C as Client
  participant R as OHTTP Relay

  box PaleVioletRed Payjoin Directory
    participant G as OHTTP Gateway
    participant D as HTTP Resource
  end

  C->>R: Relay Request<br/>FROM: Client IP<br/>[+ Encapsulated Request]
  R->>G: Gateway Request<br/>FROM: Relay IP<br/>[+ Encapsulated Request]
  G->>D: Request
  D->>G: Response
  G->>R: Gateway Response<br/>TO: Relay IP<br/>[+ Encapsulated Response]
  R->>C: Relay Response<br/>TO: Client IP<br/>[+ Encapsulated Response]

Relay/Directory interactions

RFC 9458 requires each OHTTP Relay to be configured to forward requests to exactly one OHTTP Gateway. This requirement prevents receivers from being able to choose any directory, and senders from choosing relays independently. Without addressing this limitation, senders would have to know which relays are appropriate to use for each directory, creating a tendency for one directory and its affiliated relays to monopolize the protocol.

In order to allow OHTTP Relays to be used with any directory, a directory's OHTTP Gateway may advertise this allowed purpose. This advertisement prevents OHTTP Relays from acting as open internet proxies, which would otherwise allow anonymized access to arbitrary resources and expose them to denial-of-service attacks, as well as other forms of abuse. When the directory receives a GET request to the /.well-known/ohttp-gateway path with an allowed_purposes query parameter, its response body should contain a magic string in the same format as a TLS ALPN protocol list (a U16BE length encoded list of U8 length encoded strings). The magic string is BIP77 454403bb-9f7b-4385-b31f-acd2dae20b7e, offering an unambiguous signal to relays that this OHTTP Gateway will accept requests associated with this purpose from any relay.

By supporting this allowed_purposes parameter, the directory signals to OHTTP Relays that it is willing to handle requests related to BIP 77, removing the RFC 9458's requirement that relays and Gateways be configured in a one-to-one relationship.

Rationale

Uppercase URL

In order to simplify parsing and allow QR encoders to use Alphanumeric QR mode, which is more compact than Byte mode, the mailbox endpoint URL, including the fragment parameters, is encoded in uppercase.

Unlike Bitcoin URI parameters, which require switching back to Byte mode, the use of the URL fragment for session-specific parameters makes it possible to stay in Alphanumeric mode.

Parameter Ordering

The order of fragment parameters, Bitcoin URI parameters, as well as in the sender's optional parameters have no defined meaning.

In the BIP 21 URI, the pj parameter mailbox endpoint URL SHOULD be the last parameter to avoid QR mode switching.

Since variations might create a fingerprint for particular wallet software, this document requires that fragment parameters MUST appear in reverse lexicographical order.

Session Expiration

The directory may hold a message for an offline Payjoin client until that client comes online. However, the BIP 78 spec recommends broadcasting Original PSBTs in the case of an offline counterparty. Doing so exposes a naïve, surveillance-vulnerable transaction, which Payjoin intends to avoid.

Because BIP 78 is a synchronous protocol without a standard expiration mechanism, and automated receivers are vulnerable to probing attacks, BIP 78 encourages receivers to broadcast the Original PSBT after some undefined expiration time.

Because BIP 77 is an asynchronous protocol, it requires an explicit session-specific fragment parameter, EX, to communicate this expiration time to the sender.

There is no way for a sender to prevent a receiver from broadcasting the fallback transaction extracted from the Original PSBT before the receiver-specified expiration time.

64-bit Short ID Length

64 bits are sufficient to make the probability of experiencing a random collision negligible. As of writing, the UTXO set has ~2^28 elements. This is a very loose upper bound for the number of concurrent (non-spam) sessions, for which the probability of a random collision will be less than 1%. The actual number of sessions will of course be (orders of magnitudes) lower given that sessions are short-lived. With ~2^21 sessions (a loose bound on number of transactions that can be confirmed in 24 hours) the probability is less than 1e-6. These figures bound the probability of a collision existing anywhere in the entire set, whereas the probability for an individual session to experience a collision is << 1e-10 in either case.

Complete UTXO Data

Complete UTXO data is required because this information is required for signing and calculating fees for some input types.

HTTP

HTTP is ubiquitous. Using simple HTTP polling allows even Bitcoin Core to consider an implementation. Unlike a WebSockets protocol, plain HTTP can benefit from metadata protection by using Oblivious HTTP.

Oblivious HTTP

OHTTP protects sender and receiver IP addresses both from one another and from the directory. This makes it more difficult for a directory to correlate many Payjoin transactions with specific IP addresses.

OHTTP relays can be run as basic HTTP proxies from wallet providers or third parties.

Uniform Payloads

Encapsulated OHTTP payloads seen by the relay and directory, and encrypted messages seen by the directory, are constructed to be uniform so that these third-party services are unable to distinguish between them.

Encapsulated OHTTP messages are 8192 bytes long, and begin with a cleartext OHTTP header and an uncompressed key which is distinguishable from random bytes but uniform across different encapsulated requests.

End-to-end encrypted messages are 7168 bytes long, and should be indistinguishable from uniformly random bytes. ElligatorSwift as defined in BIP 324 is used to encode encapsulated HPKE public keys prepended to the HPKE ciphertext so that the directory can't distinguish between key material, the ciphertext, and randomness. This ensures the two different protocol messages are indistinguishable from each other as well as any protocol extensions.

These padded sizes are sufficient for most PSBTs without exceeding the 8KB limit of many HTTP/1.1 web servers. 8KB is also too small for image sharing, making misuse of the directory impractical.

Random Padding

The typical zero padding recommended by the BHTTP specification would make future use of multi-hop OHTTP inspired by the Sphinx mix format detectable from the point of view of the directory. Random padding is allowed so long as the BHTTP encoded request is not truncated.

By randomly padding OHTTP messages, any future use of such techniques would be indistinguishable from clients that only implement standardized OHTTP. Since this would limit a malicious directory's ability to censor any such requests in the future, and such requests significantly bolster the privacy threat model against malicious OHTTP relays or traffic analysis by a global passive adversary, it is desirable to do so for standard OHTTP requests as well.

Secp256k1 Hybrid Public Key Encryption

RFC 9180 Hybrid Public Key Encryption (HPKE) is a modern IETF standard for secure message exchange without TLS, since TLS is not available in Bitcoin Core.

This proposal uses DHKEM(Secp256k1, HKDF-SHA256) and ChaCha20Poly1305 AEAD for both OHTTP encapsulation and for end-to-end encryption between the sender and receiver.

The receiver transmits its receiver key in receiver fragment parameters. The sender shares its reply key along with the Original PSBT. These keys are ephemeral and must only be used for a single Payjoin Session.

Secp256k1-based DHKEM

Secp256k1-based DHKEM for HPKE is most appropriate because of secp256k1's availability in bitcoin contexts.

ChaCha20Poly1305 AEAD

This authenticated encryption with additional data algorithm is standardized in RFC 8439 and has high performance. ChaCha20Poly1305 AEAD has been implemented in Bitcoin Core for BIP 324 Encrypted Transport as well. This has widespread support in browsers and common cryptographic libraries. AES-GCM is more widespread but slower without hardware support and not typically already a dependency in bitcoin software.

HKDF-SHA256

SHA-256 is necessarily available in bitcoin contexts.

Attack vectors

In addition to the attack vectors and mitigations in BIP 78, this proposal has the following attack vectors.

Directory Denial of Service

Since each mailbox stores arbitrary encrypted payloads, directories are vulnerable to flooding. To mitigate such denial of service attacks, directory operators may respond with 401 unauthorized unless an authorization token is provided. Authorization tokens must be unlinkable to preserve client privacy. A specific unlinkable authorization token mechanism is out of the scope of this proposal.

Network privacy

Oblivious HTTP must be used to protect the IP addresses of both sender and receiver from the directory. This requires an OHTTP Key Configuration to be shared in the Payjoin URI and for the directory to support Oblivious HTTP.

Unlike BIP 78 implementations, sender and receiver clients will only see the IP address of the directory and not that of the client they are interacting with.

Senders that submit requests directly to the directory, without using an OHTTP Relay, may reveal their IP address to the receiver since that receiver also specifies the directory.

Backwards compatibility

Senders not supporting Payjoin will just ignore the pj parameter and proceed to typical address-based transaction flows.

All Payjoin versions use Bitcoin URIs. Receivers may choose to accept BIP 78 payloads at their discretion.

A BIP 78 sender posts their request to the directory, which stores and forwards it to the BIP 77 receiver. A backwards-compatible receiver proceeds with the BIP 78 checks if the encapsulated response body is UTF-8 plaintext, signifying BIP 78. In order to service the request, a BIP 78 response must be returned to the sender within 30 seconds or else the directory should respond with an unavailable JSON error code as defined in BIP 78.

Reference implementation

A production reference implementation client can be found at https://crates.io/crates/payjoin-cli. Source code for the clients, the directory, and development kit may be found here: https://github.com/payjoin/rust-payjoin. Source code for an Oblivious HTTP relay implementation may be found here: https://github.com/payjoin/ohttp-relay.


Updated

2025-06-06

See an issue with rendering or formatting? Submit an issue on GitHub

Do you find this site useful? Please consider donating some sats to support ongoing development.

bips.dev is presented by nickmonad

All content is owned and licensed by the respective author(s). This website makes no claim of ownership.