Using Paywalled APIs
How to consume API endpoints behind a bolthub L402 paywall from HTTP clients and AI agents.
Non-custodial architecture
bolthub is fully non-custodial. When an AI agent or client pays a Lightning invoice for API access, the payment goes directly to the API provider's wallet. The gateway issues standard bolt11 invoices, so consumers can pay with any Lightning wallet: LND, Phoenix, Alby, a mobile wallet, WebLN, or any bolt11-compatible client. bolthub never holds, receives, or controls user funds.
API providers pay a small usage-based platform fee, billed weekly via Lightning:
| Component | Cost (per week) | ~Monthly equivalent |
|---|---|---|
| Base fee | 1,000 sats | ~4,000 sats |
| First 100 requests | Free (included in base) | ~400/month |
| 101 – 12,500 | 2 sats/request | ~401 – 50,000/month |
| 12,501 – 125,000 | 1 sat/request | ~50,001 – 500,000/month |
| 125,001+ | 0.5 sats/request | ~500,001+/month |
Note: Metering is per-week. The monthly figures are approximate 4× equivalents shown for convenience.
All new accounts include a 14-day free trial with no platform fees.
How it works
bolthub uses the L402 protocol to gate API access behind Lightning micropayments. When you hit a paywalled endpoint without paying, you get back a 402 Payment Required response containing a Lightning invoice. You pay the invoice, receive a preimage, and retry the request with proof of payment.
Client Gateway Origin API
│ │ │
│ GET /v1/weather │ │
│──────────────────────────────▶│ │
│ │ │
│ 402 Payment Required │ │
│ WWW-Authenticate: L402 ... │ │
│◀──────────────────────────────│ │
│ │ │
│ (pay Lightning invoice) │ │
│ │ │
│ GET /v1/weather │ │
│ Authorization: L402 ... │ │
│──────────────────────────────▶│ GET https://origin.com/weather│
│ │───────────────────────────────▶│
│ │ │
│ 200 OK + data │ 200 OK + data │
│◀──────────────────────────────│◀───────────────────────────────│Every paywalled endpoint is accessible at:
https://{slug}.gw.bolthub.ai{path}Where {slug} is the tenant's subdomain and {path} is the endpoint path (e.g. /v1/weather).
Step-by-step: calling a paywalled endpoint
1. Make the initial request
Send a normal HTTP request to the gateway URL. No authentication is needed for this first call.
curl -i https://acme.gw.bolthub.ai/v1/weather?city=berlin2. Receive the 402 challenge
The gateway responds with 402 Payment Required and a WWW-Authenticate header containing two values:
| Field | Description |
|---|---|
macaroon | Base64-encoded macaroon (access token bound to this payment) |
invoice | Bolt11 Lightning invoice to pay |
HTTP/1.1 402 Payment Required
WWW-Authenticate: L402 macaroon="AGIAJEemVQUTEyNCR0exk7ek90Cg==", invoice="lnbc1500n1pj9..."
Content-Type: application/json
{"error": "Payment Required", "paymentRequest": "lnbc1500n1pj9...", "amountSats": 10, "paymentHash": "abc123..."}3. Pay the Lightning invoice
Pay the bolt11 invoice using any Lightning wallet or programmatic Lightning client. After payment settles, you receive a preimage (a 32-byte hex string).
Using a CLI wallet (e.g. lncli):
lncli payinvoice lnbc1500n1pj9...
# Returns preimage: 1234abcd5678ef901234abcd5678ef901234abcd5678ef901234abcd5678ef90Using a web wallet:
Copy the invoice string and paste it into your wallet's "Send" field. The wallet will show you the preimage after payment succeeds.
4. Retry with the L402 token
Combine the macaroon and preimage into an Authorization header and resend your original request:
curl -i \
-H 'Authorization: L402 AGIAJEemVQUTEyNCR0exk7ek90Cg==:1234abcd5678ef901234abcd5678ef901234abcd5678ef90' \
https://acme.gw.bolthub.ai/v1/weather?city=berlinThe format is:
Authorization: L402 {macaroon_base64}:{preimage_hex}5. Receive the response
If the preimage is valid, the gateway proxies your request to the origin API and returns the response:
HTTP/1.1 200 OK
Content-Type: application/json
{"city": "berlin", "temp_c": 18, "condition": "partly cloudy"}Pricing models
The API provider chooses one of five pricing models per endpoint:
| Model | Behavior | Example |
|---|---|---|
per_request | Fixed price in sats for each request | 10 sats per call |
per_kb | Deposit-based session; each response deducts sats proportional to its size in KB | 100 sats deposit, 2 sats per KB |
token_bucket | Purchase a bucket of tokens; each request consumes one | 100 sats for 50 requests |
time_pass | Pay once for time-limited unlimited access | 500 sats for 60 minutes |
metered | Prepay a balance, deducted per use at a per-request unit cost | 1000 sats prepaid, 5 sats per call |
Endpoints without a pricing rule are free (no payment required). The invoice amount in the 402 response reflects the configured price.
402 response body
The 402 response body includes the invoice details. Session-based models (time_pass, metered, token_bucket, per_kb) also include a model field and model-specific parameters:
{
"error": "Payment Required",
"paymentRequest": "lnbc...",
"amountSats": 10,
"paymentHash": "abc123..."
}Session model-specific fields:
| Model | Additional fields |
|---|---|
time_pass | durationMinutes - session duration in minutes |
metered | unitCostSats - cost per request deducted from balance |
per_kb | unitCostSats - cost per KB deducted from deposit |
token_bucket | tokenBudget - number of requests included |
Free try
Some endpoints have free try enabled, allowing authenticated users to make one free request per endpoint per day without payment. Each endpoint has its own daily quota. To use it, include a Bearer token in the Authorization header:
curl -H 'Authorization: Bearer {supabase_jwt}' \
https://acme.gw.bolthub.ai/v1/weather?city=berlinIf the free try is available, the gateway proxies the request and returns an X-Free-Try: used header. If already used today, the normal L402 payment flow applies.
Time pass flow
With time_pass, the 402 response includes the access duration. After payment and verification, the gateway returns a session token in the X-Session-Token response header. Include this token in subsequent requests; all requests within the time window are proxied without additional payment.
1. GET /v1/data → 402 (includes durationMinutes in response body)
2. Pay invoice → get preimage
3. GET /v1/data with Authorization: L402 {mac}:{preimage}
→ 200 + X-Session-Token: {session_token} + X-Session-Expires: 2027-01-01T01:00:00Z
4. GET /v1/data with X-Session-Token: {session_token}
→ 200 (no payment needed until expiry)Metered / prepaid balance flow
With metered, you prepay a balance (e.g. 1000 sats) and each request deducts a fixed unit cost (e.g. 5 sats). When the balance is depleted, you receive a new 402 challenge.
1. GET /v1/compute → 402 (includes unitCostSats in response body)
2. Pay invoice → get preimage
3. GET /v1/compute with Authorization: L402 {mac}:{preimage}
→ 200 + X-Session-Token: {token} + X-Session-Balance: 995
4. GET /v1/compute with X-Session-Token: {token}
→ 200 + X-Session-Balance: 990
5. ... (repeat until balance depleted)
6. GET /v1/compute with X-Session-Token: {token}
→ 402 (session depleted, new invoice)Token bucket flow
With token_bucket, you purchase a fixed number of requests (e.g. 50 requests for 100 sats). Each request consumes one token. The response includes X-Session-Balance showing remaining tokens.
1. GET /v1/data → 402 (includes tokenBudget in response body)
2. Pay invoice → get preimage
3. GET /v1/data with Authorization: L402 {mac}:{preimage}
→ 200 + X-Session-Token: {token} + X-Session-Balance: 49
4. GET /v1/data with X-Session-Token: {token}
→ 200 + X-Session-Balance: 48
5. ... (repeat until tokens depleted)
6. GET /v1/data with X-Session-Token: {token}
→ 402 (bucket depleted, new invoice)Per-KB (data transfer) flow
With per_kb, you pay a deposit that creates a session. Each response deducts sats proportional to its size in kilobytes (unitCostSats per KB). The response includes X-Data-Size-KB and X-Data-Cost-Sats headers showing the deduction.
1. GET /v1/large-data → 402 (includes unitCostSats in response body)
2. Pay invoice (deposit) → get preimage
3. GET /v1/large-data with Authorization: L402 {mac}:{preimage}
→ 200 + X-Session-Token: {token} + X-Data-Size-KB: 15 + X-Data-Cost-Sats: 30 + X-Session-Balance: 70
4. GET /v1/large-data with X-Session-Token: {token}
→ 200 + X-Data-Size-KB: 8 + X-Data-Cost-Sats: 16 + X-Session-Balance: 54
5. ... (repeat until balance depleted)Response caching
API providers can enable response caching per endpoint. When caching is active:
- Only GET responses with 2xx status codes are cached
- Cached responses include an
X-Cache: HITheader andX-Cache-Ageheader (seconds since cached) - Cache misses include
X-Cache: MISS - Cache TTL is configured by the provider
Using lnget (Lightning Agent Tools)
lnget is a command-line HTTP client from Lightning Labs that handles L402 payments automatically. It works with any bolthub gateway out of the box, with no SDK or MCP setup required.
# Install lnget (requires Go 1.24+)
git clone https://github.com/lightninglabs/lightning-agent-tools.git
cd lightning-agent-tools && skills/lnget/scripts/install.sh
lnget config init
# Call any bolthub API - lnget pays the invoice automatically
lnget --max-cost 100 https://acme.gw.bolthub.ai/v1/weather?city=berlinKey flags:
| Flag | Description |
|---|---|
--max-cost <sats> | Refuse to pay invoices above this amount |
--no-pay | Preview mode - shows the invoice without paying |
--verbose | Show payment details and timing |
lnget caches L402 tokens, so repeated requests to the same endpoint reuse existing tokens when valid. This makes it efficient for token_bucket and time_pass endpoints.
Every API listed in the bolthub API Hub is accessible via lnget. The gateway URL follows the pattern https://{slug}.gw.bolthub.ai{path}.
Using the CLI
The bolthub CLI lets you search, explore, and call APIs directly from the terminal. It is useful for testing, scripting, and CI/CD pipelines.
# Search the marketplace
bolthub search weather
# Check pricing before paying
bolthub info acme-weather
# Call a paid API (Lightning payment handled automatically)
bolthub call acme-weather /v1/forecast --max-cost 20
# POST with a JSON body
bolthub call ai-text /v1/analyze --method POST \
--body '{"text": "Summarize this"}' --budget 500Install globally or use with npx:
npm install -g @bolthub/cli
# or
npx @bolthub/cli search weatherThe CLI uses the same wallet environment variables as the MCP tools (LND_REST_HOST, LNBITS_URL, NWC_URI, or PHOENIXD_URL). The search and info commands don't require a wallet; only call does.
Using paywalled endpoints from AI agents
AI agents (LLMs with tool use, autonomous agents, MCP clients, etc.) follow the same L402 protocol as any other client. The key difference is that the flow must be automated, and the agent needs a Lightning wallet it can call programmatically.
Using MCP (recommended)
The easiest way to connect an AI agent is via the @bolthub/mcp-bridge. Add this to your MCP client config (Cursor, Claude Desktop, or any MCP-compatible client):
{
"mcpServers": {
"acme-api": {
"command": "npx",
"args": ["@bolthub/mcp-bridge", "--gateway", "https://acme.gw.bolthub.ai"],
"env": {
"LND_REST_HOST": "https://your-lnd-node:8080",
"LND_MACAROON": "<hex-admin-or-pay-macaroon>",
"BUDGET_SATS": "1000"
}
}
}
}Wallet options: LND (recommended: bolthub Node Launcher or your own node) is shown above. You can also use
LNBITS_URL+LNBITS_ADMIN_KEY,NWC_URI(easiest but slower 1-3s), orPHOENIXD_URL+PHOENIXD_PASSWORDif you already run Phoenixd for outbound payments. You only need one wallet type.BUDGET_SATSis optional and caps total spending per session (remove for unlimited). See MCP Bridge docs for details.
The bridge:
- Fetches the gateway's OpenAPI spec on startup
- Exposes each endpoint as an MCP tool with proper
inputSchema - Handles L402 payment transparently (402 → pay invoice → retry)
- Returns the API response as the tool result
You can get the ready-made config from:
- The API Hub playground at
https://bolthub.ai/hub/{slug} - The MCP config API at
https://api.bolthub.ai/directory/{slug}/mcp-config - The gateway discovery at
https://{slug}.gw.bolthub.ai/.well-known/mcp.json
Using the CLI
For quick testing or terminal-based workflows, the CLI offers search, info, and call commands that handle L402 payments automatically.
Using SDKs directly
For custom agent implementations, use the TypeScript SDK or Python SDK.
Requirements
-
A programmable Lightning wallet: the agent needs access to a wallet that can pay invoices via API. Options:
- LND with REST access (recommended: spin up a node with the bolthub Node Launcher, or use your own LND)
- LNbits with its HTTP API
- Alby with the Alby API or NWC (Nostr Wallet Connect)
- Phoenixd if you already use it for programmatic outbound payments (fast, self-custodial)
- Any wallet that exposes a
payinvoiceAPI endpoint
-
L402 parsing logic: the agent must parse the
WWW-Authenticateheader from the 402 response to extract the macaroon and invoice. Or use@bolthub/agent(TypeScript) /bolthub(Python), which handle this automatically.
Agent flow
Agent decides to call a tool/API
│
▼
Send HTTP request to gateway URL
│
▼
Receive 402 → parse WWW-Authenticate header
│
├── Extract: macaroon (base64)
└── Extract: invoice (bolt11 string)
│
▼
Pay invoice via wallet API → receive preimage
│
▼
Retry request with Authorization: L402 {macaroon}:{preimage}
│
▼
Receive 200 → use the dataExample: Python agent with LND
import httpx
import re
LND_HOST = "https://your-lnd-node:8080"
LND_MACAROON = "0201036c6e..."
GATEWAY_URL = "https://acme.gw.bolthub.ai/v1/weather"
def call_paywalled_api(url: str, params: dict = None) -> dict:
resp = httpx.get(url, params=params)
if resp.status_code != 402:
return resp.json()
www_auth = resp.headers["WWW-Authenticate"]
macaroon = re.search(r'macaroon="([^"]+)"', www_auth).group(1)
invoice = re.search(r'invoice="([^"]+)"', www_auth).group(1)
pay_resp = httpx.post(
f"{LND_HOST}/v1/channels/transactions",
headers={"Grpc-Metadata-macaroon": LND_MACAROON},
json={"payment_request": invoice},
)
preimage = pay_resp.json()["payment_preimage"]
resp = httpx.get(
url,
params=params,
headers={"Authorization": f"L402 {macaroon}:{preimage}"},
)
return resp.json()
data = call_paywalled_api(GATEWAY_URL, {"city": "berlin"})
print(data)Example: TypeScript agent with Alby / NWC
const GATEWAY_URL = "https://acme.gw.bolthub.ai/v1/weather";
async function callPaywalledApi(url: string): Promise<unknown> {
let resp = await fetch(url);
if (resp.status !== 402) {
return resp.json();
}
const wwwAuth = resp.headers.get("WWW-Authenticate")!;
const macaroon = wwwAuth.match(/macaroon="([^"]+)"/)?.[1]!;
const invoice = wwwAuth.match(/invoice="([^"]+)"/)?.[1]!;
const { preimage } = await walletClient.payInvoice(invoice);
resp = await fetch(url, {
headers: { Authorization: `L402 ${macaroon}:${preimage}` },
});
return resp.json();
}
const data = await callPaywalledApi(`${GATEWAY_URL}?city=berlin`);Tips for agent implementations
- Budget guards: set a maximum sats-per-request and total sats budget to prevent runaway spending. Check the invoice amount before paying.
- Invoice expiry: bolt11 invoices expire (typically in 60 seconds to 24 hours). If you wait too long, you'll need to re-request to get a fresh invoice.
- Idempotency: each L402 token (macaroon + preimage pair) is single-use. Don't cache and reuse them across requests.
- Error handling: if the gateway returns
401 Unauthorized, your L402 token was invalid or already used. Start the flow from step 1 again. - Token bucket optimization: if the endpoint uses
token_bucketpricing, you pay once and get multiple requests. Parse the macaroon caveats to know how many requests remain. - Per-request payment is consumed before proxying: for
per_requestendpoints, the payment is marked as consumed before the request is forwarded to the origin. If the origin returns a 5xx error or times out, the payment is not refunded. A new invoice is required to retry. For more resilient billing, consider session-based models (token_bucket,time_pass,metered) where payment covers multiple requests.
HTTP reference
Request headers
| Header | When to send | Format |
|---|---|---|
Authorization | On retry after payment | L402 {macaroon_base64}:{preimage_hex} |
Response headers
| Header | When returned | Format |
|---|---|---|
WWW-Authenticate | On 402 responses | L402 macaroon="{base64}", invoice="{bolt11}" |
X-Session-Token | On session creation (time_pass, metered, token_bucket, per_kb) | Opaque session token string |
X-Session-Expires | On session creation | ISO 8601 timestamp |
X-Session-Balance | On session responses | Remaining balance (sats or tokens) |
X-Free-Try | When free try is consumed | used |
X-Cache | On cacheable GET responses | HIT or MISS |
X-Cache-Age | On cache hits | Seconds since response was cached |
X-Data-Size-KB | On per_kb responses | Response size in kilobytes |
X-Data-Cost-Sats | On per_kb responses | Sats deducted for this response |
X-Request-Id | On all responses | UUID for request correlation |
Rate limits
The gateway applies per-IP rate limits:
| Scope | Default limit |
|---|---|
| Per endpoint | 30 requests/minute per IP (configurable per endpoint by provider) |
| Global | 300 requests/minute per IP across all endpoints |
When rate-limited, the gateway returns 429 Too Many Requests.
Status codes
| Code | Meaning |
|---|---|
200 | Payment verified, response from origin API |
400 | Bad request - invalid path, method, or request body |
402 | Payment required - inspect WWW-Authenticate for invoice |
401 | Invalid or expired L402 token - start the flow again |
403 | Tenant is not active or suspended |
404 | Endpoint not found or not active |
413 | Request body exceeds the 1 MB limit |
429 | Rate limited - too many requests, retry after backoff |
502 | Origin API unreachable or connection error |
503 | Service unavailable - wallet not configured or billing issue |
Origin protection (for API providers)
When the gateway proxies a paid request to your origin, it injects headers you can use to verify the request is legitimate:
| Header | Value | Purpose |
|---|---|---|
X-Gateway-Secret | Your tenant's gateway secret (from the dashboard) | Static shared secret - reject requests that don't include it |
X-Gateway-Signature | HMAC-SHA256 signature of the request | Cryptographic proof the request was proxied by bolthub |
X-Gateway-Timestamp | Unix timestamp (ms) when the signature was created | Reject stale requests to prevent replay |
X-Gateway-Nonce | UUID generated per request | Reject duplicate requests to prevent replay |
The signature is computed over a canonical payload:
METHOD\nPATH\nTIMESTAMP\nNONCE\nBODYWhere METHOD is the HTTP verb, PATH is the gateway path (e.g. /v1/weather), TIMESTAMP and NONCE are the values from the respective headers, and BODY is the raw request body (empty string for GET/HEAD).
For TypeScript/Node.js and Python origins, use the official verification packages instead of writing manual checks:
- TypeScript:
npm install @bolthub/verify- zero runtime dependencies, uses only Node.js built-incrypto - Python:
pip install bolthub-verify- zero runtime dependencies, with optional Flask/Django/FastAPI middleware
Both packages handle HMAC signature verification, timestamp checks, replay prevention, and secret rotation out of the box.
Verify X-Gateway-Secret (simple)
EXPECTED_SECRET = "your-gateway-secret-from-dashboard"
def verify_request(request):
if request.headers.get("X-Gateway-Secret") != EXPECTED_SECRET:
return Response("Forbidden", status=403)Verify X-Gateway-Signature (recommended)
import hmac
import hashlib
import time
HMAC_SECRET = "your-hmac-secret-from-dashboard"
MAX_AGE_MS = 30_000
def verify_signature(request):
signature = request.headers.get("X-Gateway-Signature")
timestamp = request.headers.get("X-Gateway-Timestamp")
nonce = request.headers.get("X-Gateway-Nonce")
if not signature or not timestamp or not nonce:
return Response("Forbidden", status=403)
age_ms = int(time.time() * 1000) - int(timestamp)
if age_ms > MAX_AGE_MS or age_ms < 0:
return Response("Request expired", status=403)
body = request.body or b""
if isinstance(body, bytes):
body = body.decode("utf-8")
signing_payload = f"{request.method}\n{request.path}\n{timestamp}\n{nonce}\n{body}"
expected = hmac.new(
HMAC_SECRET.encode(), signing_payload.encode(), hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected):
return Response("Forbidden", status=403)Restrict by source IP (strongly recommended)
Header-based verification alone is insufficient if an attacker obtains your secrets. For defense in depth, restrict your origin to only accept traffic from bolthub gateway IPs:
- Fly.io egress IPs: the gateway runs on Fly.io. Configure your firewall, security group, or WAF to only allow inbound traffic from Fly.io's published IP ranges.
- Cloud providers: use security groups (AWS), firewall rules (GCP), or NSGs (Azure) to restrict your origin's listening port.
- Cloudflare/reverse proxies: if your origin is behind a reverse proxy, configure an IP allowlist at the proxy level.
Combining IP restriction with signature verification ensures that even if secrets are leaked, your origin cannot be accessed directly.
Secret rotation
bolthub supports rotating your gateway and HMAC secrets without downtime. When you rotate secrets from the dashboard:
- The current secrets become "previous" and remain valid for a grace period.
- New secrets are generated and used for all new requests.
- Your origin should accept either the current or previous secret during the transition.
Example dual-secret verification:
SECRETS = [
"your-new-hmac-secret",
"your-previous-hmac-secret",
]
def verify_signature(request):
signature = request.headers.get("X-Gateway-Signature")
timestamp = request.headers.get("X-Gateway-Timestamp")
nonce = request.headers.get("X-Gateway-Nonce")
if not signature or not timestamp or not nonce:
return Response("Forbidden", status=403)
body = request.body or b""
if isinstance(body, bytes):
body = body.decode("utf-8")
signing_payload = f"{request.method}\n{request.path}\n{timestamp}\n{nonce}\n{body}"
for secret in SECRETS:
expected = hmac.new(
secret.encode(), signing_payload.encode(), hashlib.sha256
).hexdigest()
if hmac.compare_digest(signature, expected):
return # valid
return Response("Forbidden", status=403)