lazysite supports the x402 HTTP payment protocol. Pages can require
payment before being served. When an unpaid request arrives, the
processor returns 402 Payment Required with an X-Payment-Response
header describing payment terms. An x402-compatible wallet or browser
extension reads this header, makes the on-chain payment, and retries
the request with proof.
Payment proof validation is delegated to an upstream proxy. The
processor trusts an X-Payment-Verified header, the same pattern
as X-Remote-User for authentication.
---
title: Premium Article
payment: required
payment_amount: 0.01
payment_currency: USD
payment_network: base
payment_address: 0x1234...your-wallet-address
payment_description: Access to premium article
---
payment: requiredrequired.
payment_amount0.01 for one cent USD).
payment_currencyUSD.
payment_networkbase, ethereum, polygon.
payment_addresspayment_assetpayment_descriptionMembers in specified groups skip payment entirely. Add auth_groups:
to the payment-gated page:
---
title: Members Content
payment: required
payment_amount: 0.01
payment_currency: USD
payment_network: base
payment_address: 0x1234...
auth_groups:
- members
- sponsors
---
Authenticated users in members or sponsors see the content
without paying. All other users (including unauthenticated) see the
402 payment page. On a paid page this is what auth_groups means; on
any other page it makes the page a signed-in page for those groups.
Create 402.md in the docroot. These TT variables are available:
[% payment_amount %] - amount from front matter[% payment_currency %] - currency from front matter[% payment_network %] - network from front matter[% payment_address %] - wallet address[% payment_description %] - description textIf 402.md does not exist, a minimal built-in page is shown.
After payment is verified:
[% payment_paid %] - 1 if payment proof verified[% payment_payer %] - payer wallet address if available[% payment_bypassed %] - 1 if access granted via group membership[% payment_amount %] - amount from front matter[% payment_currency %] - currency from front matter[% payment_address %] - wallet addressThe X-Payment-Response header returned with 402 responses contains
JSON:
{
"version": "1.0",
"accepts": [{
"scheme": "exact",
"network": "base",
"maxAmountRequired": "10000",
"to": "0x1234...",
"asset": "0x...",
"extra": {
"name": "USDC",
"version": "1"
}
}]
}
maxAmountRequired is in the asset's smallest unit (USDC has 6
decimals, so 0.01 USD = 10000).
In production, configure a payment proxy to:
X-Payment-Proof headerX-Payment-Verified: 1 and optionally X-Payment-PayerConfigure header names in lazysite/lazysite.conf:
auth_proxy_trusted: true
payment_header_verified: X-Payment-Verified
payment_header_payer: X-Payment-Payer
The engine believes these headers only from a proxy it trusts. Set
auth_proxy_trusted: true whenever a proxy supplies them, and make sure the
proxy strips them from incoming requests before setting its own: without the
flag the engine discards them and logs it, because a visitor can send any
header they like.
payment-demo.pl simulates payment via signed cookies for
testing. Not for production use.
Simulate payment:
/cgi-bin/payment-demo.pl?action=pay&page=/premium&amount=0.01
Clear payment:
/cgi-bin/payment-demo.pl?action=unpay&page=/premium
Demo payments expire after 1 hour.
The dev server auto-detects the demo script and routes requests through it when present.
Payment-gated pages are never cached to disk and always include
Cache-Control: no-store, private. This prevents paid content from
being served without payment verification.