BRC-29 defines how to send a peer-to-peer payment: derive a unique payment address from sender and recipient identity keys (using BRC-42 key derivation), construct a transaction with one or more P2PKH outputs to those addresses, and transmit the transaction via any transport (HTTP, WebSocket, message box). The receiver then internalizes the transaction into their wallet.
| Field | Value |
|---|---|
| Format | AsyncAPI 3.0 |
| Version | 1.0.0 |
| Status | stable |
| Implementations | @bsv/ts-paymail, @bsv/message-box-client |
Deriving unique payment addresses per payer without revealing the recipient's private key. With BRC-42/43 key derivation, a recipient can issue a new address to every payer (avoiding address reuse) without managing thousands of keys. The address is derived deterministically from sender+recipient keys, so only the recipient can receive to that address.
Authenticated peer payments without a central service. Traditional P2P payment requires a service to map paymail addresses to payment outputs. BRC-29 enables peer-to-peer: sender and recipient negotiate payment parameters directly (no intermediary), then sender constructs and transmits the transaction.
Transmit flexibility. BRC-29 specifies the transaction format (Atomic BEEF + BRC-100 envelope) but not how to send it. Payments can travel via HTTP POST, WebSocket message, message box, Paymail, or any transport layer.
Three-step flow:
Sender → Recipient (capability discovery) — Sender queries recipient's Paymail domain for supported capabilities (BRC-121). Learns recipient's public key and payment output derivation instructions.
Sender → Recipient (payment message) — Sender derives payment address using BRC-42:
invoiceNumber = "2-3241645161d8-<prefix> <suffix>"prefix = server-provided nonce (BRC-29 format)suffix = derivation suffix (e.g., base64-encoded timestamp or counter)Recipient → Sender (acknowledgment) — Recipient calls wallet.internalizeAction() to import transaction outputs. If the wallet accepts the payment, the recipient responds with acknowledgment (ACCEPTED or REJECTED).
| Channel | Direction | Message | Purpose |
|---|---|---|---|
payment/send | Send | PaymentMessage | Sender delivers Atomic BEEF transaction to recipient |
payment/acknowledge | Receive | PaymentAck | Recipient confirms acceptance or rejection |
PaymentMessage schema:
tx: AtomicBEEF — Transaction in BRC-95 Atomic BEEF formatamount: number — Total satoshis in outputs (metadata)derivationPrefix: string — Server-provided nonce (BRC-29 format)derivationSuffix: string — Client-provided derivation suffixmetadata: object — Optional sender metadata (memo, reference ID, etc.)Wallet implementations may return the AtomicBEEF created for this message as
either the historical number[] representation or a binary Uint8Array.
Remittance producers accept both BRC-100 representations and normalize the
transport artifact to a JSON-safe byte array without changing its bytes.
For compatibility with older senders, recipients may prune unrelated BEEF branches from the AtomicBEEF envelope before internalization. The transaction identified by the BRC-95 prefix, all of its recursive dependencies, every proof, and the derived BRC-29 locking script remain mandatory validation inputs.
PaymentAck schema:
status: "ACCEPTED" | "REJECTED" — Whether recipient accepted the transactiontxid: string — Transaction ID (if accepted)reason?: string — Explanation if rejectedimport { PeerPayClient } from '@bsv/message-box-client'
import { WalletClient } from '@bsv/sdk'
const wallet = new WalletClient('auto', 'example.com')
const peerPay = new PeerPayClient({
walletClient: wallet,
messageBoxHost: 'https://message-box-us-1.bsvb.tech'
})
// Get recipient's identity key from Paymail, registry lookup, or direct exchange.
const recipientKey = '025706528f0f6894b2ba505007267ccff1133e004452a1f6b72ac716f246216366'
// Builds the BRC-29 settlement, sends the AtomicBEEF through MessageBox,
// and includes the derivation remittance the receiver needs to internalize it.
await peerPay.sendPayment({
recipient: recipientKey,
amount: 10000
})Recipient receives and internalizes:
import { PeerPayClient } from '@bsv/message-box-client'
const peerPay = new PeerPayClient({
walletClient: recipientWallet,
messageBoxHost: 'https://message-box-us-1.bsvb.tech'
})
const payments = await peerPay.listIncomingPayments()
for (const payment of payments) {
const result = await peerPay.acceptPayment(payment)
if (typeof result === 'string') throw new Error(result)
console.log('Payment accepted:', result.paymentResult)
}BRC-29 conformance is tested in conformance/vectors/wallet/brc29/payment-derivation.json:
| Package | Notes |
|---|---|
| @bsv/ts-paymail | Paymail client/server for capability discovery and payment routing |
| @bsv/message-box-client | Message box transport for peer-to-peer payments |
| @bsv/wallet-toolbox | internalizeAction() method for receiving transactions |
internalizeAction() for receiving payments