@bsv/payment-express-middleware
v2.1.8
Published
BSV Blockchain service monetization express middleware
Readme
@bsv/payment-express-middleware
Express middleware for the legacy authenticated x-bsv-payment JSON flow. It
runs after @bsv/auth-express-middleware, issues an HTTP 402 challenge,
validates an Atomic BEEF transaction, atomically rejects reused transaction
IDs, internalizes output zero, and exposes a verified payment receipt.
This protocol is distinct from the newer BRC-121 implementation in
@bsv/402-pay. Choose one protocol deliberately; their headers and client
contracts 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.
Requirements
- Node.js 22 or newer
- Express 4.18 or newer, including Express 5
@bsv/auth-express-middlewareearlier in the middleware chain- A BRC-100 wallet implementing
internalizeAction
The package ships native ESM and CommonJS entry points with declarations for both module systems.
Install
npm install @bsv/payment-express-middleware @bsv/auth-express-middleware @bsv/sdk expressExpress is a peer dependency, so the middleware uses the application's single
Express runtime and type graph. This prevents duplicate Express installations
from making PaymentRequest or the returned middleware incompatible with the
application's route types.
Basic use
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
if (req.path === '/premium') return 500
return 100
},
replayStore
})
)
app.get('/premium', (req: PaymentRequest, res) => {
res.json({
accepted: req.payment?.accepted,
satoshisPaid: req.payment?.satoshisPaid,
txid: req.payment?.txid
})
})Prices must be 0 or a positive safe integer. Zero-cost requests continue
with an accepted zero-value receipt. Invalid or failed pricing returns a stable
500 response and never authorizes the request.
Flow
Auth middleware verifies the peer and supplies a compressed
req.auth.identityKey.calculateRequestPricereturns the required satoshis.If no
x-bsv-paymentheader is present, the middleware returns402with:x-bsv-payment-version: 1.0x-bsv-payment-satoshis-requiredx-bsv-payment-derivation-prefix
The client retries with one JSON header:
{ "derivationPrefix": "<canonical-base64>", "derivationSuffix": "<canonical-base64>", "transaction": "<base64-atomic-beef>" }The middleware parses the received header, verifies the derivation prefix, parses the Atomic BEEF transaction, reduces legacy overinclusive envelopes to the declared subject and its dependency closure, and requires output zero to cover the current price.
The wallet must validate and newly accept the remittance with
{ accepted: true }frominternalizeAction. Merge/replay-like or malformed results do not authorize the route.Only after wallet validation does the middleware atomically claim the transaction ID. This prevents invalid derivation material paired with a public transaction from poisoning the replay store.
next()runs withreq.payment, andx-bsv-payment-satoshis-paidreports the actual output value.
Wallet verdicts must expose accepted and optional isMerge as own data
properties; inherited or accessor-backed authority is rejected without
invoking accessors. Malformed, duplicate, underfunded, rejected, or ambiguous
payments never call next.
Options
const payment = createPaymentMiddleware({
wallet,
calculateRequestPrice,
replayStore,
logger
})walletis required and must provideinternalizeAction.calculateRequestPricemay be synchronous or asynchronous and defaults to 100 satoshis.replayStoremust implement an atomicclaim(transactionId): boolean | Promise<boolean>. It returnsfalseif the transaction has already been used.maxPaymentHeaderBytesis deprecated and ignored; header budgets belong to the HTTP server or edge.loggermay provideerrorandwarnmethods. Internal failures are sent to it as structured context but are never exposed in HTTP responses.
Invalid option types fail during startup.
Replay storage
InMemoryPaymentReplayStore is the safe single-process default. It:
- atomically claims each transaction ID once within one process;
- records only transactions the wallet reported as newly accepted;
- refuses new claims when its fixed capacity is reached rather than evicting an older replay marker; and
- loses all claims when the process restarts.
Its default capacity is 100,000 claims. It is appropriate for tests and bounded single-process services, not a horizontally scaled or durable deployment.
Production services should inject a shared durable store backed by a database or cache primitive with atomic insert-if-absent semantics:
const replayStore = {
async claim(transactionId: string) {
return await database.insertPaymentClaimIfAbsent(transactionId)
}
}Do not implement claim as separate read and write operations. Keep replay
claims for at least as long as a transaction could otherwise be accepted
again. A derivation nonce proves that the server created the prefix; it is not
an expiring, single-use replay database.
Payment receipt
After authorization:
interface PaymentReceipt {
satoshisPaid: number
accepted: true
tx: string
txid: string
}satoshisPaid is the actual value of output zero, which may be greater than
the required price. For free requests it is zero and tx/txid are empty.
Error behavior
| Status | Code | Meaning |
| ------ | ------------------------------- | ------------------------------------------------------------------------ |
| 400 | ERR_MALFORMED_PAYMENT | The header is duplicated, oversized, invalid JSON, or has invalid fields |
| 400 | ERR_INVALID_DERIVATION_PREFIX | The server did not create the supplied prefix |
| 400 | ERR_INVALID_PAYMENT | Atomic BEEF is invalid or output zero is underfunded |
| 400 | ERR_PAYMENT_FAILED | The wallet could not accept the payment |
| 402 | ERR_PAYMENT_REQUIRED | A payment challenge was issued |
| 409 | ERR_PAYMENT_REPLAYED | The transaction was already claimed or was not newly accepted |
| 500 | ERR_SERVER_MISCONFIGURED | Auth middleware did not provide a valid identity |
| 500 | ERR_PAYMENT_INTERNAL | Pricing failed or returned an invalid value |
| 503 | ERR_PAYMENT_UNAVAILABLE | Challenge creation or replay storage is unavailable |
Public errors deliberately omit wallet, replay-store, and pricing exception messages.
Public services and browser access
This middleware does not impose CORS or CSP. Public payment services can remain
cross-origin by default while operators optionally configure an allowlist at
the application or edge layer. Browser clients need the x-bsv-payment-*
response headers exposed through CORS. Never pair a wildcard origin with
credentialed CORS.
Security notes
- Use HTTPS; payment and identity headers are not a confidentiality layer.
- Run authentication first and authorization/payment routes after both middleware functions.
- Use a durable atomic replay store for multiple processes or replicas.
- Wallet errors and rejected remittances are never inserted into the replay store; investigate an unavailable replay store promptly because the wallet may already have accepted the transaction before that independent gate.
- Monitor
409and503rates and replay-store capacity. - Treat pricing as security-sensitive, deterministic request policy.
- The wallet remains responsible for validating and safely internalizing the supplied BRC-29 remittance.
- Apply normal request/header limits and rate limiting at the service edge.
- Logger failures are contained and cannot interrupt payment authorization or change the HTTP result.
API
Runtime exports:
createPaymentMiddlewareInMemoryPaymentReplayStore
Type exports:
BSVPaymentPaymentLoggerPaymentMiddlewareOptionsPaymentReceiptPaymentReplayStorePaymentRequest
See API.md for generated signatures.
Development
pnpm typecheck
pnpm lint
pnpm format:check
pnpm test:coverage
pnpm pack:checkThe test suite is deterministic and does not call public APIs.
pack:check builds and validates the exact npm tarball in ESM and CommonJS
consumer probes. Tests do not rebuild the package as a side effect.
License
Current TS Stack changes are licensed under the Open BSV License Version 6; see
LICENSE.txt. This package also retains pre-uniformization code
under the Open BSV License Version 4. Redistributors must preserve
THIRD_PARTY_NOTICES.md and the applicable text in
LICENSES/.
