11---
22id : guide-http-402
3- title : " HTTP 402 Payment Gating"
3+ title : ' HTTP 402 Payment Gating'
44kind : guide
5- version : " 1.0.1"
6- last_updated : " 2026-07-26"
7- last_verified : " 2026-07-26"
5+ version : ' 1.0.1'
6+ last_updated : ' 2026-07-26'
7+ last_verified : ' 2026-07-26'
88review_cadence_days : 30
99status : stable
1010tags : [guide, payments, http-402, brc-121, monetization]
@@ -20,6 +20,7 @@ tags: [guide, payments, http-402, brc-121, monetization]
2020## What you'll build
2121
2222A complete end-to-end payment system:
23+
2324- ** Server** : Express middleware that gates endpoints by payment, validates transactions
2425- ** Client** : Auto-paying fetch wrapper that transparently handles 402 responses
2526- ** Flow** : Client requests resource → server responds 402 with payment details → client signs & pays → access granted
@@ -85,31 +86,31 @@ async function setupServer() {
8586 network: ' main' ,
8687 storageUrl: ' https://store-us-1.bsvb.tech'
8788 })
88-
89+
8990 // 2. Create payment middleware
9091 const paymentMiddleware = createPaymentMiddleware ({
9192 wallet ,
92- calculatePrice : ( path ) => {
93+ calculatePrice : path => {
9394 // Dynamic pricing by route
94- if (path === ' /api/premium' ) return 1000 // 1000 satoshis
95- if (path === ' /api/free' ) return 0 // Free
96- return 100 // Default 100 satoshis
95+ if (path === ' /api/premium' ) return 1000 // 1000 satoshis
96+ if (path === ' /api/free' ) return 0 // Free
97+ return 100 // Default 100 satoshis
9798 }
9899 })
99-
100+
100101 // 3. Build Express app
101102 const app = express ()
102103 app .use (bodyParser .json ())
103104 app .use (paymentMiddleware )
104-
105+
105106 // 4. Define routes
106107 app .get (' /api/free' , (req , res ) => {
107108 res .json ({
108109 message: ' Free content' ,
109110 paid: false
110111 })
111112 })
112-
113+
113114 app .get (' /api/premium' , (req , res ) => {
114115 res .json ({
115116 message: ' Premium content — paid!' ,
@@ -118,15 +119,15 @@ async function setupServer() {
118119 sender: req .payment ?.senderIdentityKey
119120 })
120121 })
121-
122+
122123 return app
123124}
124125
125126// Start server
126127async function main() {
127128 const app = await setupServer ()
128129 const port = process .env .PORT || 3000
129-
130+
130131 app .listen (port , () => {
131132 console .log (` Payment server running on http://localhost:${port } ` )
132133 console .log (' - GET /api/free: No payment required' )
@@ -138,6 +139,7 @@ main().catch(console.error)
138139```
139140
140141The middleware chain:
142+
1411431 . ** Payment middleware** checks if payment is required and validates provided payment headers
1421442 . ** Route handler** receives the request only if payment was accepted or the route is free
143145
@@ -146,6 +148,7 @@ The middleware chain:
146148When client requests a paid endpoint without payment, the server responds with 402 and payment details.
147149
148150The payment middleware automatically:
151+
149152- Calculates required satoshis via ` calculatePrice() `
150153- Returns 402 status if unpaid
151154- Includes headers:
@@ -166,18 +169,19 @@ dotenv.config()
166169export async function setupPaymentClient() {
167170 // Initialize wallet for signing payments
168171 const wallet = new WalletClient ()
169-
172+
170173 // Create a fetch wrapper that auto-handles 402
171174 const fetch402 = create402Fetch ({
172175 wallet ,
173- cacheTimeoutMs: 30 * 60 * 1000 // Cache paid content for 30 minutes
176+ cacheTimeoutMs: 30 * 60 * 1000 // Cache paid content for 30 minutes
174177 })
175-
178+
176179 return fetch402
177180}
178181```
179182
180183` create402Fetch() ` returns a fetch-compatible function that:
184+
181185- Catches 402 responses
182186- Signs a payment transaction
183187- Retries with payment headers
@@ -191,7 +195,7 @@ Create `requests.ts` in client:
191195export async function accessFreeContent(fetch402 : any ) {
192196 const response = await fetch402 (' http://localhost:3000/api/free' )
193197 const data = await response .json ()
194-
198+
195199 console .log (' Free content:' , data )
196200 return data
197201}
@@ -200,7 +204,7 @@ export async function accessPremiumContent(fetch402: any) {
200204 // Automatically handles 402 and pays
201205 const response = await fetch402 (' http://localhost:3000/api/premium' )
202206 const data = await response .json ()
203-
207+
204208 console .log (' Premium content:' , data )
205209 return data
206210}
@@ -213,6 +217,7 @@ export async function clearPaymentCache(fetch402: any) {
213217```
214218
215219When client calls ` fetch402('/api/premium') ` :
220+
2162211 . Sends request (no payment)
2172222 . Receives 402 with ` x-bsv-sats: 1000 ` and ` x-bsv-server `
2182233 . Generates a fresh nonce and timestamp, then derives the payment address with BRC-29
@@ -235,20 +240,16 @@ export async function manualPayment(
235240 serverIdentityKey : string
236241) {
237242 // Build payment headers manually
238- const headers = await constructPaymentHeaders (
239- wallet ,
240- serverUrl ,
241- satoshis ,
242- serverIdentityKey
243- )
244-
243+ const headers = await constructPaymentHeaders (wallet , serverUrl , satoshis , serverIdentityKey )
244+
245245 // Use with custom fetch
246246 const response = await fetch (serverUrl , { headers })
247247 return response
248248}
249249```
250250
251251Manual header construction gives you:
252+
252253- ` x-bsv-beef ` : Base64-encoded transaction proof
253254- ` x-bsv-sender ` : Your identity public key
254255- ` x-bsv-nonce ` : Derivation prefix (random 8 bytes)
@@ -272,35 +273,35 @@ async function main() {
272273 network: ' main' ,
273274 storageUrl: ' https://store-us-1.bsvb.tech'
274275 })
275-
276+
276277 // Setup middleware
277278 const paymentMiddleware = createPaymentMiddleware ({
278279 wallet ,
279- calculatePrice : ( path ) => {
280+ calculatePrice : path => {
280281 if (path === ' /api/premium' ) return 1000
281282 if (path === ' /api/free' ) return 0
282283 return 100
283284 }
284285 })
285-
286+
286287 // Create app
287288 const app = express ()
288289 app .use (bodyParser .json ())
289290 app .use (paymentMiddleware )
290-
291+
291292 // Routes
292293 app .get (' /api/free' , (req , res ) => {
293294 res .json ({ message: ' Free content' , paid: false })
294295 })
295-
296+
296297 app .get (' /api/premium' , (req , res ) => {
297298 res .json ({
298299 message: ' Premium content' ,
299300 paid: true ,
300301 satoshisPaid: req .payment ?.satoshisPaid || 0
301302 })
302303 })
303-
304+
304305 // Start
305306 app .listen (3000 , () => {
306307 console .log (' Server on http://localhost:3000' )
@@ -319,23 +320,23 @@ import { WalletClient } from '@bsv/sdk'
319320async function main() {
320321 // Setup wallet
321322 const wallet = new WalletClient ()
322-
323+
323324 // Create auto-paying fetch
324325 const fetch402 = create402Fetch ({
325326 wallet ,
326327 cacheTimeoutMs: 30 * 60 * 1000
327328 })
328-
329+
329330 console .log (' Accessing free content...' )
330331 let response = await fetch402 (' http://localhost:3000/api/free' )
331332 let data = await response .json ()
332333 console .log (' Free:' , data )
333-
334+
334335 console .log (' \n Accessing premium content (will auto-pay 1000 sats)...' )
335336 response = await fetch402 (' http://localhost:3000/api/premium' )
336337 data = await response .json ()
337338 console .log (' Premium:' , data )
338-
339+
339340 console .log (' \n Accessing premium again (cached, no new payment)...' )
340341 response = await fetch402 (' http://localhost:3000/api/premium' )
341342 data = await response .json ()
@@ -378,7 +379,11 @@ npx ts-node main.ts
378379→ If cache timeout is very long, client may use stale payments for different resources. Use moderate timeout (30 min is safe)
379380
380381** "Replay protection"**
381- → Server wallet rejects duplicate payment internalization. Each payment must use a fresh nonce and transaction.
382+ → For ` @bsv/402-pay ` , freshness fields bind the proof but the wallet must also
383+ avoid newly accepting the same transaction twice. For
384+ ` @bsv/payment-express-middleware ` , inject a durable atomic
385+ transaction-ID replay store in multi-process deployments; its derivation
386+ prefix is not a single-use replay database.
382387
383388## What to read next
384389
0 commit comments