# Accept a payment

> Take a BSV payment from the user's wallet and receive it straight into your server's wallet. One React button, one Express route, BRC-29 under the hood.

> Agents: search these docs with the `search_docs` tool on the MCP server at https://www.createbsvapp.com/mcp, or read everything at https://www.createbsvapp.com/llms-full.txt.

Your server names a price. The user's wallet pays to a fresh key that only your server can spend, after the user approves it. The browser hands the signed transaction to your server, and your server's wallet checks it and keeps the coins. No addresses to copy, no payment processor, and the user never leaves your app.

This builds on the full-stack starter. Everything on this page compiles against a fresh `--starter full-stack --capabilities wallet-login,signed-requests` scaffold. We couldn't approve a payment in a real wallet from a test script, so the final hop was checked with stand-in transactions: see [what was tested](#what-was-tested).

## How it works

1. **Quote.** The client calls `POST /api/pay` with an empty body. The server answers `402 Payment Required` with the price and a one-time `derivationPrefix`.
2. **Derive.** The client picks a random `derivationSuffix` and asks the wallet for a key derived from the server's identity key ([BRC-29](https://github.com/bitcoin-sv/BRCs/blob/master/payments/0029.md)). Only the server can spend from it.
3. **Pay.** `wallet.createAction` builds the transaction. The user approves it in their wallet.
4. **Receive.** The client posts the transaction (AtomicBEEF) with the prefix, the suffix and its identity key. The server's wallet calls `internalizeAction`, which checks the proofs and the key, and adds the coins to its balance.

## Give the server a wallet that can receive

The scaffold's `serverWallet` is a `ProtoWallet`. It signs and verifies, which is all login and signed requests need, but it has no storage, so it can't hold coins. To receive payments, add a real wallet from `@bsv/wallet-toolbox-client`:

```bash
cd server
npm install @bsv/wallet-toolbox-client
```

The wallet below uses the same `SERVER_PRIVATE_KEY`, so its identity key matches what `GET /api/identity` already returns. It needs:

- **Storage**, to remember which coins it owns. By default it uses Babbage's hosted wallet storage (the staging instance on testnet). Pass `storageUrl` to use your own.
- **A network**, from `BSV_NETWORK`. This page covers `test` and `main`.
- **No funding.** Receiving costs the server nothing. Coins it receives become its balance, which it can spend later with its own `createAction`.

::: warning Set `SERVER_PRIVATE_KEY` before you take a payment
Without it, the server makes a new random key on every restart, and coins sent to the old key are stuck with a key you no longer have. [Generate one](https://www.createbsvapp.com/docs/environment#generate-a-server-key) and keep it like a password: whoever has it owns the money.
:::

## The server route

Add this file next to `index.ts`:

```ts [server/src/payments.ts]
// Take a BRC-29 payment from the user's wallet and receive it into the server's wallet.
import type { Request, Response } from 'express'
import { PrivateKey, Transaction, Utils, createNonce, verifyNonce } from '@bsv/sdk'
import { SetupClient, type Wallet } from '@bsv/wallet-toolbox-client'
import { SERVER_PRIVATE_KEY, BSV_NETWORK } from './bsv/config.js'

const PRICE = 1000 // satoshis

// Same key as the ProtoWallet in index.ts, so the identity key clients see doesn't change.
let wallet: Promise<Wallet> | undefined
function paymentWallet (): Promise<Wallet> {
  wallet ??= SetupClient.createWalletClientNoEnv({
    chain: BSV_NETWORK === 'main' ? 'main' : 'test',
    rootKeyHex: PrivateKey.fromString(SERVER_PRIVATE_KEY).toHex()
  })
  return wallet
}

const usedTxids = new Set<string>() // use your database in production

export async function payRoute (req: Request, res: Response): Promise<void> {
  const server = await paymentWallet()
  const payment = req.body?.payment

  // 1. No payment yet: say what it costs, and hand out a one-time derivation prefix.
  if (payment == null) {
    const derivationPrefix = await createNonce(server)
    res.status(402).json({ satoshisRequired: PRICE, derivationPrefix })
    return
  }

  const { derivationPrefix, derivationSuffix, transaction, senderIdentityKey } = payment
  if (typeof derivationPrefix !== 'string' || typeof derivationSuffix !== 'string' ||
      typeof transaction !== 'string' || typeof senderIdentityKey !== 'string') {
    res.status(400).json({ error: 'malformed payment' })
    return
  }

  // 2. The prefix must be one this server issued.
  if (!await verifyNonce(derivationPrefix, server).catch(() => false)) {
    res.status(400).json({ error: 'unknown derivation prefix' })
    return
  }

  // 3. Output 0 must pay at least the price, and each transaction only counts once.
  let txid: string
  try {
    const tx = Transaction.fromAtomicBEEF(Utils.toArray(transaction, 'base64'))
    txid = tx.id('hex')
    if ((tx.outputs[0]?.satoshis ?? 0) < PRICE) throw new Error('underpaid')
  } catch {
    res.status(402).json({ error: 'payment does not cover the price', satoshisRequired: PRICE })
    return
  }
  if (usedTxids.has(txid)) {
    res.status(409).json({ error: 'payment already used' })
    return
  }
  usedTxids.add(txid)

  // 4. The wallet checks the proofs, checks output 0 is locked to its own BRC-29 key, and keeps it.
  try {
    await server.internalizeAction({
      tx: Utils.toArray(transaction, 'base64'),
      outputs: [{
        outputIndex: 0,
        protocol: 'wallet payment',
        paymentRemittance: { derivationPrefix, derivationSuffix, senderIdentityKey }
      }],
      description: 'Payment for premium content'
    })
  } catch {
    usedTxids.delete(txid)
    res.status(400).json({ error: 'payment rejected' })
    return
  }

  res.json({ paid: PRICE, txid, from: senderIdentityKey })
}
```

Then mount it:

```ts [server/src/index.ts]
import { consumeNonce } from './bsv/nonceStore.js'
import { payRoute } from './payments.js' // ← add this line
// ...
app.post('/api/login', loginRoute(serverWallet))
app.post('/api/pay', payRoute) // ← add this line
```

`internalizeAction` throws if output 0 isn't locked to the key the server derives from that prefix, suffix and sender. So a successful payment also proves who sent it: only the holder of `senderIdentityKey` could have derived that key. You get the payer's identity without a separate login.

::: deep Why not `@bsv/payment-express-middleware`?
It does the same job (it's the [BRC-105](https://github.com/bitcoin-sv/BRCs/blob/master/payments/0105.md) flow, with headers instead of a JSON body), but it must run after `@bsv/auth-express-middleware`, which authenticates every request with BRC-103 sessions. The scaffold signs individual requests instead, so a plain route is the smaller change. If you move to BRC-103 auth, switch to the middleware, and the client can use `AuthFetch` from `@bsv/sdk`, which pays `402` responses for you.
:::

## The client button

Drop this component anywhere inside `WalletProviders` (next to `<ConnectWallet />` works well). It uses the scaffold's `useWallet`, `getServerIdentity` and `apiFetch`:

```tsx [client/src/PayButton.tsx]
import { useState } from 'react'
import { P2PKH, PublicKey, Random, Utils } from '@bsv/sdk'
import { useWallet } from './bsv/WalletContext.js'
import { getServerIdentity } from './bsv/serverIdentity.js'
import { apiFetch, readApiJson } from './bsv/apiClient.js'

const post = (body: unknown) => apiFetch('/api/pay', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify(body)
})

export function PayButton () {
  const { wallet, identityKey } = useWallet()
  const [status, setStatus] = useState('')

  async function pay () {
    if (wallet === null || identityKey === null) return
    try {
      // 1. Ask the server for the price and a one-time derivation prefix.
      const quote = await readApiJson(await post({})) as { satoshisRequired: number, derivationPrefix: string }
      const { satoshisRequired, derivationPrefix } = quote

      // 2. Derive a fresh key that only the server can spend (BRC-29).
      const derivationSuffix = Utils.toBase64(Random(16))
      const { publicKey } = await wallet.getPublicKey({
        protocolID: [2, '3241645161d8'],
        keyID: `${derivationPrefix} ${derivationSuffix}`,
        counterparty: await getServerIdentity()
      })

      // 3. The wallet asks the user to approve, then builds and signs the transaction.
      const { tx } = await wallet.createAction({
        description: 'Unlock premium content',
        outputs: [{
          lockingScript: new P2PKH().lock(PublicKey.fromString(publicKey).toAddress()).toHex(),
          satoshis: satoshisRequired,
          outputDescription: 'Payment to the app'
        }],
        options: { randomizeOutputs: false } // keep the payment at output 0
      })
      if (tx === undefined) throw new Error('wallet returned no transaction')

      // 4. Hand the transaction to the server, which receives it into its own wallet.
      const res = await post({
        payment: { derivationPrefix, derivationSuffix, transaction: Utils.toBase64(tx), senderIdentityKey: identityKey }
      })
      setStatus(res.ok ? 'Paid. Thank you!' : `Payment failed (${res.status})`)
    } catch (e) {
      setStatus(e instanceof Error ? e.message : String(e))
    }
  }

  if (wallet === null) return null
  return (
    <>
      <button onClick={() => { void pay() }}>Pay 1000 satoshis</button>
      <p role="status">{status}</p>
    </>
  )
}
```

`[2, '3241645161d8']` is the protocol ID BRC-29 reserves for payments, and `options: { randomizeOutputs: false }` keeps your payment at output 0, where the server looks for it. The wallet adds the user's change after it.

::: warning On create-bsv-app 1.1.2, apply the client build fix
`npm run build` in `client/` fails type-checking until you apply the [three-line fix](https://www.createbsvapp.com/docs/troubleshooting#client-build-fails). With it, this component builds cleanly.
:::

## Try it on testnet

New projects target testnet (`BSV_NETWORK=test`), so the coins are free and worthless. Put some in your wallet from the [BSV Faucet](https://bsvfaucet.com/), then click the button. One day of faucet coins covers thousands of 1000-satoshi test payments.

Switch to real money by setting `BSV_NETWORK=main` on the server and `VITE_BSV_NETWORK=main` on the client. See [Networks](https://www.createbsvapp.com/docs/environment#networks).

## What `accepted` means

`internalizeAction` returns once the server's wallet has checked the transaction and stored it. It doesn't wait for a miner, and it doesn't prove the network has accepted the transaction: in our tests, a well-formed transaction that was never broadcast still came back accepted, and the wallet dropped it later. For small amounts that risk is usually fine. For something valuable, check the network has seen the transaction before you deliver:

```ts [server/src/payments.ts]
export async function seenByNetwork (txid: string): Promise<boolean> {
  const server = await paymentWallet()
  const { results } = await server.getServices().getStatusForTxids([txid])
  return results[0]?.status === 'known' || results[0]?.status === 'mined'
}
```

The user's wallet usually broadcasts as it signs, but give it a few seconds and retry before you call a payment missing.

## Common errors

| You see | Why | Fix |
| --- | --- | --- |
| `400 unknown derivation prefix` | The prefix came from a different server key, often after a dev restart without `SERVER_PRIVATE_KEY`. | Set `SERVER_PRIVATE_KEY`, then ask for a new quote. |
| `402 payment does not cover the price` | Output 0 pays less than `PRICE`, or the payment isn't at output 0. | Pay `satoshisRequired` and keep `randomizeOutputs: false`. |
| `400 payment rejected` | `internalizeAction` threw: the proofs didn't check out, or output 0 isn't locked to the key derived from the prefix, suffix and `senderIdentityKey`. | Send the connected wallet's own identity key, and the same prefix and suffix you derived with. |
| `409 payment already used` | That transaction was already counted. | Ask for a new quote and pay again. Keep `usedTxids` in a database so this survives restarts. |
| `413 Payload Too Large` | The transaction and its ancestors are bigger than the scaffold's `64kb` JSON limit. | Raise `limit` in `express.json` in `server/src/index.ts`. |
| The wallet rejects the action | Not enough coins, the user declined, or a description outside 5 to 50 characters. | Fund the wallet from the faucet on testnet, and keep descriptions short. |

## What was tested

On a fresh scaffold with create-bsv-app 1.1.2, both `client/` and `server/` build with this code. Against the running server, with throwaway keys standing in for the user's wallet: the quote returns `402`, an underpaid transaction gets `402`, a transaction locked to the wrong sender's key gets `400`, a correctly derived payment is accepted, and sending it again gets `409`. What we couldn't do from a script is approve a real payment in a real wallet, so try it once on testnet before you rely on it.

## Using an AI assistant?

Paste this:

```text
Read https://www.createbsvapp.com/docs/accept-payments.md and add a "Pay 1000 satoshis" button to my create-bsv-app full-stack project, with the /api/pay route on the server. Keep my existing login and signed requests working.
```
