Monetize your API with Bitcoin SV micropayments. You'll set up a payment-gated Express server and build a client that auto-pays for content using HTTP 402 Payment Required.
Time: ~25 minutes Prerequisites: Node.js ≥ 22, basic Express.js, understanding of Bitcoin transactions
A complete end-to-end payment system:
By the end, you'll have a production-ready micropayment system that doesn't require user friction.
Set up two separate projects:
# Server
mkdir payment-server && cd payment-server
npm init -y
npm install express body-parser @bsv/sdk @bsv/402-pay @bsv/simple dotenv
npm install -D typescript ts-node @types/express @types/node
# Client (in separate directory)
mkdir ../payment-client && cd ../payment-client
npm init -y
npm install @bsv/402-pay @bsv/sdk dotenv
npm install -D typescript ts-node @types/nodeBoth need TypeScript config:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"strict": true,
"esModuleInterop": true
}
}In the server project, create server.ts:
import express from 'express'
import bodyParser from 'body-parser'
import { createPaymentMiddleware } from '@bsv/402-pay/server'
import { ServerWallet } from '@bsv/simple/server'
import dotenv from 'dotenv'
dotenv.config()
async function setupServer() {
// 1. Initialize wallet
const wallet = await ServerWallet.create({
privateKey: process.env.SERVER_PRIVATE_KEY!,
network: 'main',
storageUrl: 'https://store-us-1.bsvb.tech'
})
// 2. Create payment middleware
const paymentMiddleware = createPaymentMiddleware({
wallet,
calculatePrice: path => {
// Dynamic pricing by route
if (path === '/api/premium') return 1000 // 1000 satoshis
if (path === '/api/free') return 0 // Free
return 100 // Default 100 satoshis
}
})
// 3. Build Express app
const app = express()
app.use(bodyParser.json())
app.use(paymentMiddleware)
// 4. Define routes
app.get('/api/free', (req, res) => {
res.json({
message: 'Free content',
paid: false
})
})
app.get('/api/premium', (req, res) => {
res.json({
message: 'Premium content — paid!',
paid: true,
satoshisPaid: req.payment?.satoshisPaid || 0,
sender: req.payment?.senderIdentityKey
})
})
return app
}
// Start server
async function main() {
const app = await setupServer()
const port = process.env.PORT || 3000
app.listen(port, () => {
console.log(`Payment server running on http://localhost:${port}`)
console.log('- GET /api/free: No payment required')
console.log('- GET /api/premium: Requires 1000 satoshis')
})
}
main().catch(console.error)The middleware chain:
When client requests a paid endpoint without payment, the server responds with 402 and payment details.
The payment middleware automatically:
calculatePrice()x-bsv-sats: satoshis owedx-bsv-server: server identity key used by the client for payment derivationIn the client project, create client.ts:
import { create402Fetch } from '@bsv/402-pay/client'
import { WalletClient } from '@bsv/sdk'
import dotenv from 'dotenv'
dotenv.config()
export async function setupPaymentClient() {
// Initialize wallet for signing payments
const wallet = new WalletClient()
// Create a fetch wrapper that auto-handles 402
const fetch402 = create402Fetch({
wallet,
cacheTimeoutMs: 30 * 60 * 1000 // Cache paid content for 30 minutes
})
return fetch402
}create402Fetch() returns a fetch-compatible function that:
Create requests.ts in client:
export async function accessFreeContent(fetch402: any) {
const response = await fetch402('http://localhost:3000/api/free')
const data = await response.json()
console.log('Free content:', data)
return data
}
export async function accessPremiumContent(fetch402: any) {
// Automatically handles 402 and pays
const response = await fetch402('http://localhost:3000/api/premium')
const data = await response.json()
console.log('Premium content:', data)
return data
}
export async function clearPaymentCache(fetch402: any) {
// Reset cache between sessions
fetch402.clearCache()
console.log('Payment cache cleared')
}When client calls fetch402('/api/premium'):
x-bsv-sats: 1000 and x-bsv-serverFor custom workflows, construct payment headers manually:
import { constructPaymentHeaders } from '@bsv/402-pay/client'
export async function manualPayment(
wallet: any,
serverUrl: string,
satoshis: number,
serverIdentityKey: string
) {
// Build payment headers manually
const headers = await constructPaymentHeaders(wallet, serverUrl, satoshis, serverIdentityKey)
// Use with custom fetch
const response = await fetch(serverUrl, { headers })
return response
}Manual header construction gives you:
x-bsv-beef: Base64-encoded transaction proofx-bsv-sender: Your identity public keyx-bsv-nonce: Derivation prefix (random 8 bytes)x-bsv-time: Unix millisecond timestampx-bsv-vout: Output index (usually 0)Server main.ts:
import express from 'express'
import bodyParser from 'body-parser'
import { createPaymentMiddleware } from '@bsv/402-pay/server'
import { ServerWallet } from '@bsv/simple/server'
async function main() {
// Setup wallet
const wallet = await ServerWallet.create({
privateKey: process.env.SERVER_PRIVATE_KEY!,
network: 'main',
storageUrl: 'https://store-us-1.bsvb.tech'
})
// Setup middleware
const paymentMiddleware = createPaymentMiddleware({
wallet,
calculatePrice: path => {
if (path === '/api/premium') return 1000
if (path === '/api/free') return 0
return 100
}
})
// Create app
const app = express()
app.use(bodyParser.json())
app.use(paymentMiddleware)
// Routes
app.get('/api/free', (req, res) => {
res.json({ message: 'Free content', paid: false })
})
app.get('/api/premium', (req, res) => {
res.json({
message: 'Premium content',
paid: true,
satoshisPaid: req.payment?.satoshisPaid || 0
})
})
// Start
app.listen(3000, () => {
console.log('Server on http://localhost:3000')
})
}
main().catch(console.error)Client main.ts:
import { create402Fetch } from '@bsv/402-pay/client'
import { WalletClient } from '@bsv/sdk'
async function main() {
// Setup wallet
const wallet = new WalletClient()
// Create auto-paying fetch
const fetch402 = create402Fetch({
wallet,
cacheTimeoutMs: 30 * 60 * 1000
})
console.log('Accessing free content...')
let response = await fetch402('http://localhost:3000/api/free')
let data = await response.json()
console.log('Free:', data)
console.log('\nAccessing premium content (will auto-pay 1000 sats)...')
response = await fetch402('http://localhost:3000/api/premium')
data = await response.json()
console.log('Premium:', data)
console.log('\nAccessing premium again (cached, no new payment)...')
response = await fetch402('http://localhost:3000/api/premium')
data = await response.json()
console.log('Premium (cached):', data)
}
main().catch(console.error)Run:
# Terminal 1: Server
cd payment-server
npx ts-node main.ts
# Terminal 2: Client
cd payment-client
npx ts-node main.ts"Auth middleware must run first"
→ This applies to @bsv/payment-express-middleware, not @bsv/402-pay. The @bsv/402-pay middleware is independent and does not require BRC-31 auth.
"Wallet must implement getPublicKey() and internalizeAction()"
→ Server wallet must expose an identity key and accept wallet-payment internalization. Use ServerWallet.create() from @bsv/simple/server or another BRC-100 wallet implementation.
"Payment keeps getting requested"
→ The retried request must include x-bsv-beef, x-bsv-sender, x-bsv-nonce, x-bsv-time, and x-bsv-vout. create402Fetch() adds those automatically.
"Transaction format error"
→ Client sends the payment transaction as base64 BEEF in x-bsv-beef. Ensure the wallet returned tx from createAction().
"Timestamp must be fresh"
→ x-bsv-time must be within ~30 seconds of server time. Check system clocks if repeatedly failing
"Cache timeout too long" → If cache timeout is very long, client may use stale payments for different resources. Use moderate timeout (30 min is safe)
"Replay protection"
→ For @bsv/402-pay, freshness fields bind the proof but the wallet must also
avoid newly accepting the same transaction twice. For
@bsv/payment-express-middleware, inject a durable atomic
transaction-ID replay store in multi-process deployments; its derivation
prefix is not a single-use replay database.