Skip to content

Take payments

A payment has two halves: the wallet builds and signs it after the user approves one sheet, and your server checks it before you hand anything over. Never trust the browser’s word that it paid.

import { pay, tip } from '@bwalletx/connect';
await pay(wallet, { to: 'alice@bwalletx.com', sats: 5000, memo: 'Album' }); // a paymail
await pay(wallet, { to: '1Abc…', usd: 1 }); // an address, priced in dollars
const r = await pay(wallet, { to: identityKeyHex, sats: 100 }); // BRC-29 to an identity key
await tip(wallet, { to: 'author@bwalletx.com', usd: 0.05 });

wallet is the BRC-100 wallet from bWalletX.connect() or useBwalletX(). Each call makes exactly one createAction and never retries. A failure throws a PaymentError with a code: BAD_REQUEST, RESOLVE_FAILED, RATE_FAILED, WALLET_REJECTED (the user said no), WALLET_ERROR or NOT_SUPPORTED. When maybeSent is true the wallet may already have broadcast it: check before asking again.

Browsers often can’t resolve paymails themselves (CORS), so pass resolvePaymail to do it on your server.

import { P2PKH } from '@bsv/sdk';
const { txid, tx } = await wallet.createAction({
description: 'Buy a pixel',
outputs: [{ lockingScript: new P2PKH().lock('1YourAddress…').toHex(), satoshis: 1000, outputDescription: 'pixel' }],
});

The user sees one approval sheet. Nothing is sent until they tap Allow. tx is the payment as Atomic BEEF: send it to your server with the order.

import { verifyPayment } from '@bwalletx/connect/server';
const ok = await verifyPayment({
tx: body.tx ?? body.txid, // Atomic BEEF / BEEF / raw hex, or a txid (fetched from WhatsOnChain)
expected: [{ address: SELLER_ADDRESS, satoshis: price }],
requireSeen: true, // the network must have seen it
});

We ran this against a real mined payment on 17 Oct 2026:

await verifyPayment({
tx: 'b0cf3321646bc1f43946022f5e113fc1ab2d7270cf9e76b7018868174f4af159',
expected: [{ address: '192nuX6cz81MH3T2gwsam3FxYoDrvzDYpU', satoshis: 54705 }],
requireSeen: true,
});
accepted: {"ok":true,"txid":"b0cf3321…f159","matched":[{"vout":1,"satoshis":54705,"script":"76a9145817be01a12a09bbbf67ae306b5ed8064bf8afc888ac"}],"status":{…}}

Asking for more than was paid throws, so you never deliver on a short payment:

asked for more than was paid: OUTPUT_MISSING - No output pays 99999 sats to script 76a9145817be01a1…

Other codes: TX_NOT_FOUND, NOT_SEEN, REJECTED, SPV_FAILED, BAD_TX, NETWORK. Pass spv: chainTracker to check the merkle proofs in a BEEF too.

  1. Quote on your server: price in sats, the address, and an order id you store with a short expiry.
  2. Pay in the browser with pay() or createAction, including the order id (for example as a 0-sat OP_RETURN output), and post tx back.
  3. Unlock on your server only after verifyPayment matches the quote, then mark the order used so the same payment can’t unlock twice.

This is how the bWalletX Store sells themes: see Store receipts for a working example you can check on chain.

name@bwalletx.com is a normal paymail. Its capabilities are at https://bwalletx.com/.well-known/bsvalias; payments go by P2P (p2p-destination, then receive-beef). Check the name’s signed binding first: Name bindings.

Convert at payment time and leave room for the network fee inside your price, so a user holding exactly your price can pay. The Store asks 500 sats under $1 for this reason.

  • ✓ pay, tip, buyToken in @bwalletx/connect 0.5.0
  • ✓ verifyPayment on the server (verified against mainnet, 17 Oct 2026)
  • ✓ BRC-100 createAction with one approval sheet
  • ◻ @bwalletx/paywall package (not published yet)