BRC-121 HTTP 402 Payment Required
BRC-121 monetizes HTTP API endpoints with a single-round-trip payment flow: client requests a resource, server responds with 402 and payment instructions (a nonce), client derives a payment address, constructs a BSV transaction, and re-sends the original request with payment headers. Server validates and serves the resource (or rejects with another 402).
At a glance
| Field | Value |
|---|---|
| Format | OpenAPI 3.1 |
| Version | 1.0.0 |
| Status | stable |
| Implementations | @bsv/402-pay, @bsv/payment-express-middleware |
What problem this solves
Monetize APIs without subscriptions or token systems. Traditional APIs require accounts, credit cards, or API keys. BRC-121 enables instant monetization: each request is micro-paid in satoshis. No signup, no account needed—just pay per request and immediate access.
Replay-proof payment derivation. The server provides a fresh nonce for each 402 response. The client must use that exact nonce to derive the payment address. This prevents replay attacks: a payment for one request cannot be reused for another.
Stateless server operation. The server doesn't maintain client sessions or payment history. Every request is independent. The payment derivation is deterministic (nonce + timestamp), so the server can validate payments without database lookups.
Protocol overview
Single round-trip with payment:
-
Client → Server
GET /protected-resource(no payment headers)- Server checks if resource is free (price = 0) → serve it
- Otherwise, server sends 402 Payment Required
-
Server → Client HTTP 402 response
x-bsv-server: <serverPublicKey>— Server's identity keyx-bsv-sats: <amount>— Satoshis required
-
Client (constructs payment)
- Generates a fresh random nonce and millisecond timestamp
- Calls
wallet.createAction()with one P2PKH output - Derives address using key ID
<nonce> <base64(timestamp)> - Uses BRC-42 to derive
(pubKeyA, pubKeyB)from sender+server keys - Locks the output to the derived server payment key
- Signs and constructs Atomic BEEF (BRC-95)
-
Client → Server same request with payment headers
x-bsv-beef: <beefBase64>— Atomic BEEF transactionx-bsv-sender: <clientPublicKey>— Sender's identity keyx-bsv-nonce: <nonce>— Client-generated derivation prefixx-bsv-time: <timestamp>— Unix millisecond timestampx-bsv-vout: <outputIndex>— Which output contains the payment
-
Server (validates & serves)
- Calls
wallet.internalizeAction()with BEEF - If
accepted === true→ payment accepted, serve resource (HTTP 200) - If the transaction, nonce, amount, or output index is invalid → reject (HTTP 402)
- If amount < required → reject (HTTP 402)
- Calls
Key types / endpoints
| Method | Path | Purpose | Request | Response |
|---|---|---|---|---|
| GET/POST/etc. | /{resourcePath} | First request (unpaid) | (none) | 402 + x-bsv-sats, x-bsv-server |
| GET/POST/etc. | /{resourcePath} | Second request with payment | x-bsv-beef, x-bsv-sender, x-bsv-nonce, x-bsv-time, x-bsv-vout | 200 + resource body, or 402 on failure |
Example: Monetized Express endpoint
import express from 'express'
import { createPaymentMiddleware } from '@bsv/402-pay/server'
import { ServerWallet } from '@bsv/simple/server'
const wallet = await ServerWallet.create({
privateKey: process.env.SERVER_PRIVATE_KEY!,
network: 'main',
storageUrl: 'https://store-us-1.bsvb.tech'
})
const app = express()
app.use(createPaymentMiddleware({
wallet,
calculatePrice: (path) => {
// Pricing logic
if (path === '/premium') return 500 // 500 sats
if (path === '/free') return 0 // Free
return 100 // Default 100 sats
}
}))
// Protected endpoint
app.get('/premium', (req, res) => {
// If we reach here, payment was accepted
res.json({
content: 'Premium article here',
paidBy: req.payment.senderIdentityKey,
amount: req.payment.satoshisPaid,
txid: req.payment.txid
})
})
app.listen(3000)Client-side (using create402Fetch):
import { create402Fetch } from '@bsv/402-pay/client'
import { WalletClient } from '@bsv/sdk'
const wallet = new WalletClient()
// Fetch wrapper that auto-handles 402
const fetch402 = create402Fetch({ wallet })
const response = await fetch402('https://api.example.com/premium')
const data = await response.json()
console.log(data.content) // Automatic payment + resource retrievalConformance vectors
There is no standalone BRC-121 vector directory in the current conformance corpus. Related portable fixtures live in conformance/vectors/wallet/brc29/payment-derivation.json for payment derivation and conformance/vectors/wallet/brc100/ for wallet methods used by payment clients and servers.
Implementations in ts-stack
| Package | Notes |
|---|---|
| @bsv/402-pay/server | Server-side middleware for independent 402 handling without BRC-31 auth |
| @bsv/402-pay/client | Client-side fetch wrapper that auto-detects 402 and constructs payment headers |
| @bsv/payment-express-middleware | Express middleware for payment gating that requires auth middleware first and uses x-bsv-payment JSON |
Related specs
- BRC-29 Peer Payment — Key derivation and payment format
- BRC-42/43 — Key derivation for payment address
- BRC-95 / BRC-62 — Atomic BEEF format
- BRC-31 Auth — Often combined with 402 for dual auth + payment
- BRC-100 Wallet —
internalizeAction()for payment validation