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.
1. Ask the wallet to pay
Section titled “1. Ask the wallet to pay”With @bwalletx/connect (easiest)
Section titled “With @bwalletx/connect (easiest)”import { pay, tip } from '@bwalletx/connect';
await pay(wallet, { to: 'alice@bwalletx.com', sats: 5000, memo: 'Album' }); // a paymailawait pay(wallet, { to: '1Abc…', usd: 1 }); // an address, priced in dollarsconst r = await pay(wallet, { to: identityKeyHex, sats: 100 }); // BRC-29 to an identity keyawait 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.
With plain BRC-100 createAction
Section titled “With plain BRC-100 createAction”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.
2. Check it on your server
Section titled “2. Check it on your server”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.
A paywall in three steps
Section titled “A paywall in three steps”- Quote on your server: price in sats, the address, and an order id you store with a short expiry.
- Pay in the browser with
pay()orcreateAction, including the order id (for example as a 0-satOP_RETURNoutput), and posttxback. - Unlock on your server only after
verifyPaymentmatches 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.
Paying a bWalletX name from a server
Section titled “Paying a bWalletX name from a server”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.
Pricing in dollars
Section titled “Pricing in dollars”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.
Status
Section titled “Status”- ✓
pay,tip,buyTokenin@bwalletx/connect0.5.0 - ✓
verifyPaymenton the server (verified against mainnet, 17 Oct 2026) - ✓ BRC-100
createActionwith one approval sheet - ◻
@bwalletx/paywallpackage (not published yet)