@bsv/p2p is a toolkit for peer-to-peer messaging and payments on the BSV blockchain. It leverages a server-side store-and-forward system for message delivery (via MessageBoxClient
) and also includes a higher-level peer-to-peer payment flow (via PeerPayClient
). Both functionalities build on BRC-103 for mutual authentication and identity key management, allowing secure and authenticated exchanges of data and BSV.
The @bsv/p2p library provides two main tools for peer-to-peer interaction:
- MessageBoxClient – A store-and-forward messaging system backed by a "message box" server. It supports authenticated sending, listing, and acknowledging (deleting) messages with a mutual-auth approach.
-
PeerPayClient – A higher-level payment client built on top of
MessageBoxClient
, enabling real-time, peer-to-peer Bitcoin payments on the BSV blockchain.
Both clients use the BRC-103-based authentication model. By integrating with a WalletClient, they can sign and verify messages, ensuring only authorized parties can send and receive.
npm install @bsv/p2p
The package exports both MessageBoxClient
and PeerPayClient
. You can import them individually in your JavaScript/TypeScript applications.
The P2P integration tests verify live communication between the MessageBoxClient and a running instance of the MessageBox Server. To run them, make sure the following are set up:
Prerequisites
- MessageBox Server Running Locally You must have a local instance of the MessageBox Server running. Follow these steps inside the messagebox-server project:
git clone https://github.com/bitcoin-sv/messagebox-server.git
cd messagebox-server
cp .env.example .env
npm install
npm run start # Starts LARS (overlay service on http://localhost:8080)
npm run dev # Starts MessageBox Server (HTTP API on http://localhost:5001)
- Ensure Environment Configuration Your .env file in the MessageBox Server must have:
NODE_ENV=development
BSV_NETWORK=local
ENABLE_WEBSOCKETS=true
and valid values for:
- SERVER_PRIVATE_KEY
- WALLET_STORAGE_URL
- MONGO_URI and MONGO_DB
- (Optional) If using MySQL locally, ensure KNEX_DB_CONNECTION is properly set.
- Wallet Storage Running (optional) If your .env points to a local wallet-storage instance, make sure it is also running.
Running the Tests Once the MessageBox Server and overlay service are running:
npm run test:integration
This will execute all integration tests under src/tests/integration/, including HTTP, WebSocket, encryption, and overlay scenarios.
Notes
- If the server is not running, tests will fail with network or timeout errors.
- Integration tests use the local network preset (networkPreset: 'local') and assume the default MessageBox API endpoints (e.g., http://localhost:8080).
- Some tests may require clearing the database manually between runs if data conflicts occur.
- Unit tests (non-integration) still run by default with npm test. Integration tests are separated.
Quick Summary
- npm run test → unit tests only
- npm run test:integration → integration tests against a running MessageBox server
MessageBoxClient
uses a store-and-forward architecture for Peer-to-Peer messages:
- Store-and-forward: Messages are posted to a MessageBoxServer under a named "message box" (like an inbox).
- Ephemeral storage: Once the recipient acknowledges the messages, they are removed from the server.
- Mutual authentication: Ensures only authorized peers can read or post messages, using AuthFetch and AuthSocketClient.
- Flexible transport: Supports both WebSockets (for live, push-style delivery) and HTTP (for polling or fallback).
- Extensible: Can be the foundation for more advanced workflows (e.g., token-based messaging, invoice/ticket systems, etc.).
- Secure by default using Auth libraries for signing/verification.
- Real-time or delayed delivery with sockets or HTTP.
- Easy integration with higher-level protocols and services.
Important:
Starting with version @bsv/p2p@1.2.0
, MessageBoxClient
automatically initializes itself if needed.
- If you provide a host during construction, the client will auto-initialize on first use.
- If no host is provided, the client will anoint a host from the overlay network automatically during initialization.
You may still manually call await init()
if you want to control when initialization happens.
Example:
const client = new MessageBoxClient({ walletClient, host: 'https://messagebox.babbage.systems' })
// Manual init (optional but supported)
await client.init()
// Or just start sending — init will auto-run if needed
await client.sendMessage({ recipient, messageBox: 'inbox', body: 'Hello' })
Example:
const client = new MessageBoxClient({ walletClient, host: 'https://messagebox.babbage.systems' })
await client.init() // Must always call init() before using the client
await client.sendMessage({ recipient, messageBox: 'inbox', body: 'Hello' })
The init()
method will:
-
Anoint your messagebox host onto the overlay network if necessary.
-
Initialize the client’s internal identity and network information.
-
Ensure that your client can receive messages securely from peers.
PeerPayClient
builds on MessageBoxClient
to enable peer-to-peer Bitcoin payments:
- Secure Payment Delivery: Utilizes the same store-and-forward or live WebSocket approach for delivering payment instructions.
- Derivation & Signing: Creates a unique output script for each payment, derived from sender + recipient keys.
- Live or Delayed: Works with web sockets for immediate notifications, or HTTP for an asynchronous flow.
- Wallet Integration: Accept or reject incoming payments. If accepted, the payment is “internalized” into your BRC-100 compatible wallet automatically.
- Deterministic derivation of payment information using the SPV-compliant BRC-29 protocol.
-
Secure transaction passing using the
MessageBoxClient
infrastructure. - Live or offline support for receiving payments.
- Easy acceptance/refunds with built-in methods.
Below are two condensed examples: one for basic messaging (MessageBoxClient) and another for peer-to-peer payments (PeerPayClient).
const { WalletClient } = require('@bsv/sdk')
const { MessageBoxClient } = require('@bsv/p2p')
// Example identity key of the recipient (public key in hex)
const johnSmithKey = '022600d2ef37d123fdcac7d25d7a464ada7acd3fb65a0daf85412140ee20884311'
async function main() {
// 1) Create your WalletClient (this obtains your identity key)
const myWallet = new WalletClient()
// 2) Create a MessageBoxClient, pointing to a MessageBoxServer
const msgBoxClient = new MessageBoxClient({
host: 'https://messagebox.babbage.systems',
walletClient: myWallet
})
// 3) (Optional) Initialize the client manually
await msgBoxClient.init()
// (Optional) Initialize a WebSocket connection (for real-time listening)
await msgBoxClient.initializeConnection()
// 4) Send a message to John's "demo_inbox"
await msgBoxClient.sendMessage({
recipient: johnSmithKey,
messageBox: 'demo_inbox',
body: 'Hello John! This is a test message.'
})
// 5) List messages in "demo_inbox"
const messages = await msgBoxClient.listMessages({ messageBox: 'demo_inbox' })
console.log(messages[0].body) // --> "Hello John! This is a test message."
// 6) Acknowledge (and delete) them from the server
await msgBoxClient.acknowledgeMessage({
messageIds: messages.map(msg => msg.messageId.toString())
})
}
main().catch(console.error)
Note:
- If you do not manually call
await init()
, the client will automatically initialize itself when you use it. - You can still call
await init()
manually if you want explicit control. -
init()
ensures your identity is properly registered and discoverable via overlay advertisement if necessary.
Listening for Live Messages
If you want push-style message notifications instead of polling:
await msgBoxClient.listenForLiveMessages({
messageBox: 'demo_inbox',
onMessage: (msg) => {
console.log('Received live message in "demo_inbox":', msg.body)
}
})
import { WalletClient } from '@bsv/sdk'
import { PeerPayClient } from '@bsv/p2p'
async function paymentDemo() {
// 1) Create your wallet instance
const wallet = new WalletClient()
// 2) Create a PeerPayClient
const peerPay = new PeerPayClient({
walletClient: wallet
})
// 3) (Optional) Listen for incoming payments
await peerPay.listenForLivePayments({
onPayment: async (payment) => {
console.log('Received payment:', payment)
// Accept it into the wallet
await peerPay.acceptPayment(payment)
}
})
// 4) Send a payment of 50,000 sats to the recipient
await peerPay.sendLivePayment({
recipient: '0277a2b...e3f4', // recipient's public key
amount: 50000
})
}
paymentDemo().catch(console.error)
Note: sendLivePayment
will try WebSocket first and fall back to HTTP if unavailable.
### 5.1. MessageBoxClient API
<!--#region ts2md-api-merged-here-->
Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes)
##### Interfaces
| |
| --- |
| [AcknowledgeMessageParams](#interface-acknowledgemessageparams) |
| [EncryptedMessage](#interface-encryptedmessage) |
| [ListMessagesParams](#interface-listmessagesparams) |
| [MessageBoxClientOptions](#interface-messageboxclientoptions) |
| [PeerMessage](#interface-peermessage) |
| [SendMessageParams](#interface-sendmessageparams) |
| [SendMessageResponse](#interface-sendmessageresponse) |
Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes)
---
###### Interface: AcknowledgeMessageParams
Defines the structure of a request to acknowledge messages.
Example
```ts
{
messageIds: ["abc123", "def456"]
}
export interface AcknowledgeMessageParams {
messageIds: string[];
}
Links: API, Interfaces, Classes
Encapsulates an AES-256-GCM encrypted message body.
Used when transmitting encrypted payloads to the MessageBox server.
export interface EncryptedMessage {
encryptedMessage: Base64String;
}
Links: API, Interfaces, Classes
Defines the structure of a request to list messages.
Example
{
messageBox: "payment_inbox"
}
export interface ListMessagesParams {
messageBox: string;
}
Links: API, Interfaces, Classes
Configuration options for initializing a MessageBoxClient.
export interface MessageBoxClientOptions {
walletClient?: WalletClient;
host?: string;
enableLogging?: boolean;
networkPreset?: "local" | "mainnet" | "testnet";
}
Interface MessageBoxClientOptions Details
####### Property enableLogging
If true, enables detailed logging to the console.
enableLogging?: boolean
####### Property host
Base URL of the MessageBox server.
host?: string
####### Property networkPreset
Overlay network preset for routing resolution.
networkPreset?: "local" | "mainnet" | "testnet"
####### Property walletClient
Wallet instance used for auth, identity, and encryption. If not provided, a new WalletClient will be created.
walletClient?: WalletClient
Links: API, Interfaces, Classes
Represents a decrypted message received from a MessageBox. Includes metadata such as sender identity, timestamps, and optional acknowledgment status.
Used in both HTTP and WebSocket message retrieval responses.
export interface PeerMessage {
messageId: string;
body: string;
sender: string;
created_at: string;
updated_at: string;
acknowledged?: boolean;
}
Links: API, Interfaces, Classes
Parameters required to send a message. Message content may be a string or object, and encryption is enabled by default.
Example
{
recipient: "03abc...",
messageBox: "payment_inbox",
body: { type: "ping" },
skipEncryption: false
}
export interface SendMessageParams {
recipient: string;
messageBox: string;
body: string | object;
messageId?: string;
skipEncryption?: boolean;
}
Links: API, Interfaces, Classes
Server response structure for successful message delivery.
Returned by both sendMessage
and sendLiveMessage
.
export interface SendMessageResponse {
status: string;
messageId: string;
}
Links: API, Interfaces, Classes
Logger |
MessageBoxClient |
Links: API, Interfaces, Classes
export class Logger {
static enable(): void
static disable(): void
static log(...args: unknown[]): void
static warn(...args: unknown[]): void
static error(...args: unknown[]): void
}
Links: API, Interfaces, Classes
Example
const client = new MessageBoxClient({ walletClient, enableLogging: true })
// Manual init is optional — client will auto-initialize if needed
await client.init()
await client.sendMessage({ recipient, messageBox: 'payment_inbox', body: 'Hello world' })
export class MessageBoxClient {
public readonly authFetch: AuthFetch;
constructor(options: MessageBoxClientOptions = {})
async init(targetHost: string = this.host): Promise<void>
public getJoinedRooms(): Set<string>
public async getIdentityKey(): Promise<string>
public get testSocket(): ReturnType<typeof AuthSocketClient> | undefined
async initializeConnection(): Promise<void>
async resolveHostForRecipient(identityKey: string): Promise<string>
async joinRoom(messageBox: string): Promise<void>
async listenForLiveMessages({ onMessage, messageBox }: {
onMessage: (message: PeerMessage) => void;
messageBox: string;
}): Promise<void>
async sendLiveMessage({ recipient, messageBox, body, messageId, skipEncryption }: SendMessageParams): Promise<SendMessageResponse>
async leaveRoom(messageBox: string): Promise<void>
async disconnectWebSocket(): Promise<void>
async sendMessage(message: SendMessageParams, overrideHost?: string): Promise<SendMessageResponse>
async anointHost(host: string): Promise<{
txid: string;
}>
async listMessages({ messageBox }: ListMessagesParams): Promise<PeerMessage[]>
async acknowledgeMessage({ messageIds }: AcknowledgeMessageParams): Promise<string>
}
See also: AcknowledgeMessageParams, ListMessagesParams, MessageBoxClientOptions, PeerMessage, SendMessageParams, SendMessageResponse
Class MessageBoxClient Details
####### Constructor
constructor(options: MessageBoxClientOptions = {})
See also: MessageBoxClientOptions
Argument Details
-
options
- Initialization options for the MessageBoxClient.
Example
const client = new MessageBoxClient({
host: 'https://messagebox.example',
walletClient,
enableLogging: true,
networkPreset: 'testnet'
})
await client.init()
####### Method acknowledgeMessage
async acknowledgeMessage({ messageIds }: AcknowledgeMessageParams): Promise<string>
See also: AcknowledgeMessageParams
Returns
- A string indicating the result, typically
'success'
.
Argument Details
-
params
- An object containing an array of message IDs to acknowledge.
Throws
If the message ID array is missing or empty, or if the request to the server fails.
Example
await client.acknowledgeMessage({ messageIds: ['msg123', 'msg456'] })
####### Method anointHost
async anointHost(host: string): Promise<{
txid: string;
}>
Returns
- The transaction ID of the advertisement broadcast to the overlay network.
Argument Details
-
host
- The full URL of the server you want to designate as your MessageBox host (e.g., "https://mybox.com").
Throws
If the URL is invalid, the PushDrop creation fails, or the overlay broadcast does not succeed.
Example
const { txid } = await client.anointHost('https://my-messagebox.io')
####### Method disconnectWebSocket
async disconnectWebSocket(): Promise<void>
Returns
Resolves when the WebSocket connection is successfully closed.
Example
await client.disconnectWebSocket()
####### Method getIdentityKey
public async getIdentityKey(): Promise<string>
Returns
The identity public key of the user
####### Method getJoinedRooms
public getJoinedRooms(): Set<string>
Returns
A set of currently joined WebSocket room IDs
####### Method init
async init(targetHost: string = this.host): Promise<void>
Argument Details
-
targetHost
- Optional host to set or override the default host.
Throws
If no valid host is provided, or anointing fails.
Example
const client = new MessageBoxClient({ host: 'https://mybox.example', walletClient })
await client.init()
await client.sendMessage({ recipient, messageBox: 'inbox', body: 'Hello' })
####### Method initializeConnection
async initializeConnection(): Promise<void>
Throws
If the identity key is unavailable or authentication fails
Example
const mb = new MessageBoxClient({ walletClient })
await mb.initializeConnection()
// WebSocket is now ready for use
####### Method joinRoom
async joinRoom(messageBox: string): Promise<void>
Argument Details
-
messageBox
- The name of the WebSocket room to join (e.g., "payment_inbox").
Example
await client.joinRoom('payment_inbox')
// Now listening for real-time messages in room '028d...-payment_inbox'
####### Method leaveRoom
async leaveRoom(messageBox: string): Promise<void>
Argument Details
-
messageBox
- The name of the WebSocket room to leave (e.g.,
payment_inbox
).
- The name of the WebSocket room to leave (e.g.,
Example
await client.leaveRoom('payment_inbox')
####### Method listMessages
async listMessages({ messageBox }: ListMessagesParams): Promise<PeerMessage[]>
See also: ListMessagesParams, PeerMessage
Returns
- Returns an array of decrypted
PeerMessage
objects.
Argument Details
-
params
- Contains the name of the messageBox to read from.
Throws
If no messageBox is specified, the request fails, or the server returns an error.
Example
const messages = await client.listMessages({ messageBox: 'inbox' })
messages.forEach(msg => console.log(msg.sender, msg.body))
####### Method listenForLiveMessages
async listenForLiveMessages({ onMessage, messageBox }: {
onMessage: (message: PeerMessage) => void;
messageBox: string;
}): Promise<void>
See also: PeerMessage
Argument Details
-
params
- Configuration for the live message listener.
Example
await client.listenForLiveMessages({
messageBox: 'payment_inbox',
onMessage: (msg) => console.log('Received live message:', msg)
})
####### Method resolveHostForRecipient
async resolveHostForRecipient(identityKey: string): Promise<string>
Returns
- A fully qualified host URL for the recipient's MessageBox server.
Argument Details
-
identityKey
- The public identity key of the intended recipient.
Example
const host = await resolveHostForRecipient('028d...') // → returns either overlay host or this.host
####### Method sendLiveMessage
async sendLiveMessage({ recipient, messageBox, body, messageId, skipEncryption }: SendMessageParams): Promise<SendMessageResponse>
See also: SendMessageParams, SendMessageResponse
Returns
A success response with the generated messageId.
Argument Details
-
param0
- The message parameters including recipient, box name, body, and options.
Throws
If message validation fails, HMAC generation fails, or both WebSocket and HTTP fail to deliver.
Example
await client.sendLiveMessage({
recipient: '028d...',
messageBox: 'payment_inbox',
body: { amount: 1000 }
})
####### Method sendMessage
async sendMessage(message: SendMessageParams, overrideHost?: string): Promise<SendMessageResponse>
See also: SendMessageParams, SendMessageResponse
Returns
- Resolves with
{ status, messageId }
on success.
Argument Details
-
message
- Contains recipient, messageBox name, message body, optional messageId, and skipEncryption flag.
-
overrideHost
- Optional host to override overlay resolution (useful for testing or private routing).
Throws
If validation, encryption, HMAC, or network request fails.
Example
await client.sendMessage({
recipient: '03abc...',
messageBox: 'notifications',
body: { type: 'ping' }
})
Links: API, Interfaces, Classes
import { PeerPayClient } from '@bsv/p2p'
new PeerPayClient({
walletClient: WalletClient,
messageBoxHost?: string,
enableLogging?: boolean
})
- walletClient: (Required) Your identity/signing wallet.
-
messageBoxHost: (Optional) Base URL of the MessageBoxServer. Defaults to
https://messagebox.babbage.systems
. - enableLogging: (Optional) Enables verbose debug output.
await peerPay.sendPayment({
recipient: '0277a2b...',
amount: 10000
})
- Sends a payment using HTTP.
- Internally derives a public key for the recipient and builds a transaction.
await peerPay.sendLivePayment({
recipient: '0277a2b...',
amount: 15000
})
- Sends a payment using WebSockets, falling back to HTTP if needed.
await peerPay.listenForLivePayments({
onPayment: (payment) => {
console.log('New live payment:', payment)
}
})
- Subscribes to live payments in the
payment_inbox
. - Invokes
onPayment
callback with anIncomingPayment
object:
interface IncomingPayment {
messageId: number;
sender: string;
token: {
customInstructions: {
derivationPrefix: string;
derivationSuffix: string;
};
transaction: any; // typically your BSV transaction format
amount: number;
};
}
await peerPay.acceptPayment(payment)
- Accepts (and "internalizes") the payment into your wallet.
- Acknowledges the message, removing it from the inbox.
await peerPay.rejectPayment(payment)
- Rejects the payment, returning a refund to the sender (minus a small fee, e.g. 1000 sats).
- If the amount is too small to refund, the payment is simply acknowledged and dropped.
const payments = await peerPay.listIncomingPayments()
- Lists all incoming payments in the
payment_inbox
. - Returns an array of
IncomingPayment
objects.
- Clone this repository.
- Install dependencies with
npm install
. - Make your changes, write tests, and open a PR.
We welcome bug reports, feature requests, and community contributions!
This code is licensed under the Open BSV License. See LICENSE for details.