This source candidate declares SDK peer ^2.1.6 || ^3.0.0. SDK3 remains
a coordinated proposal; see the qualification and migration limits
before adopting it.
Express middleware for the legacy authenticated x-bsv-payment JSON flow. It
runs after @bsv/auth-express-middleware, validates an Atomic BEEF payment,
atomically rejects transaction-ID reuse, internalizes output zero, and exposes
a verified receipt.
This contract is distinct from the newer BRC-121 implementation in
@bsv/402-pay. Their headers and clients are not interchangeable.
Version 2.1.8 removes middleware payment-header size ceilings. A header already
received from the client is parsed and its payment validated regardless of size.
HTTP server, CDN, proxy and WAF configuration own transport budgets; configure
and validate the full route for supported payment proofs. The deprecated
maxPaymentHeaderBytes option remains accepted for source compatibility but is
ignored, including when an older application still supplies a value. Move any
intended transport policy to the HTTP server or edge and remove the option.
Atomic BEEF validation, canonical base64, derivation verification, payment pricing, wallet acceptance and atomic replay protection still apply. Invalid payment contents are rejected. SDK 2.8.5 separately raises client header capacity; that does not constrain what this middleware accepts from other compatible clients. No BRC100 call, wire or wallet-data migration is required. Source publication is a separate protected release step.
npm install @bsv/payment-express-middleware @bsv/auth-express-middleware @bsv/sdk expressNode.js 22 or newer is required. The package provides native ESM and CommonJS entry points with matching declarations.
import express from 'express'
import { createAuthMiddleware } from '@bsv/auth-express-middleware'
import { createPaymentMiddleware, type PaymentRequest } from '@bsv/payment-express-middleware'
const app = express()
app.use(express.json())
app.use(createAuthMiddleware({ wallet }))
app.use(
createPaymentMiddleware({
wallet,
calculateRequestPrice(req) {
if (req.path === '/free') return 0
return 100
},
replayStore
})
)
app.get('/paid', (req: PaymentRequest, res) => {
res.json({
satoshisPaid: req.payment?.satoshisPaid,
txid: req.payment?.txid
})
})Prices must be zero or a positive safe integer. Zero-cost requests receive an accepted zero-value receipt. Invalid pricing fails closed.
402 plus the version, required satoshis, and a
server-created derivation prefix.x-bsv-payment JSON with canonical-base64
derivationPrefix, derivationSuffix, and transaction.{ accepted: true } from a newly internalized payment
authorizes the route; inherited and accessor-backed verdicts are rejected.req.payment.satoshisPaid and the response header report the actual output
value.The middleware does not impose a raw-header size ceiling. Duplicated, malformed,
underfunded, replayed, rejected, or ambiguous payments never call next().
PaymentReplayStore has one required operation:
interface PaymentReplayStore {
claim(transactionId: string): boolean | Promise<boolean>
}The claim must be a single atomic insert-if-absent operation. It returns true once and false for every reuse.
The default InMemoryPaymentReplayStore is bounded and fail-closed, but it is
process-local and loses claims on restart. Use a shared durable implementation
for replicas and production services:
const replayStore = {
async claim(transactionId: string) {
return await database.insertPaymentClaimIfAbsent(transactionId)
}
}Wallet errors and rejected remittances are not claimed. If the wallet accepts
but the subsequent replay claim is unavailable, the middleware returns 503;
operators must reconcile that ambiguous transaction before asking the payer to
spend again. A derivation nonce proves the server created a prefix; it is not
an expiring single-use replay database.
wallet — required BRC-100 wallet with internalizeActioncalculateRequestPrice — sync or async price; defaults to 100replayStore — atomic transaction-ID claim storemaxPaymentHeaderBytes — deprecated and ignored; configure transport budgets
on the HTTP server or edgelogger — optional structured error/warn sinkThe logger receives sanitized failure metadata, not exception messages, transaction IDs, wallet objects, or payment bodies. Logger failures are contained. HTTP responses are also stable and sanitized.
interface PaymentReceipt {
satoshisPaid: number
accepted: true
tx: string
txid: string
}For a free request, satoshisPaid is zero and tx/txid are empty.
The middleware does not hard-code CORS, CSP, or origin restrictions. Public
payment services may remain cross-origin by default, with an operator opt-in
allowlist at the application or edge. Browser clients need
x-bsv-payment-* response headers exposed through CORS. Do not combine a
wildcard origin with credentialed CORS.
503.409) and unavailable/capacity (503) responses.Runtime:
createPaymentMiddlewareInMemoryPaymentReplayStoreTypes:
BSVPaymentPaymentLoggerPaymentMiddlewareOptionsPaymentReceiptPaymentReplayStorePaymentRequest@bsv/auth-express-middleware — required payer authentication@bsv/402-pay — separate BRC-121 payment protocol@bsv/sdk — nonce, Atomic BEEF, and wallet primitives