BoltHub logoBoltHub
Guides

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:

ComponentCost (per week)~Monthly equivalent
Base fee1,000 sats~4,000 sats
First 100 requestsFree (included in base)~400/month
101 – 12,5002 sats/request~401 – 50,000/month
12,501 – 125,0001 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=berlin

2. Receive the 402 challenge

The gateway responds with 402 Payment Required and a WWW-Authenticate header containing two values:

FieldDescription
macaroonBase64-encoded macaroon (access token bound to this payment)
invoiceBolt11 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: 1234abcd5678ef901234abcd5678ef901234abcd5678ef901234abcd5678ef90

Using 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=berlin

The 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:

ModelBehaviorExample
per_requestFixed price in sats for each request10 sats per call
per_kbDeposit-based session; each response deducts sats proportional to its size in KB100 sats deposit, 2 sats per KB
token_bucketPurchase a bucket of tokens; each request consumes one100 sats for 50 requests
time_passPay once for time-limited unlimited access500 sats for 60 minutes
meteredPrepay a balance, deducted per use at a per-request unit cost1000 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:

ModelAdditional fields
time_passdurationMinutes - session duration in minutes
meteredunitCostSats - cost per request deducted from balance
per_kbunitCostSats - cost per KB deducted from deposit
token_buckettokenBudget - 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=berlin

If 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: HIT header and X-Cache-Age header (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=berlin

Key flags:

FlagDescription
--max-cost <sats>Refuse to pay invoices above this amount
--no-payPreview mode - shows the invoice without paying
--verboseShow 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 500

Install globally or use with npx:

npm install -g @bolthub/cli
# or
npx @bolthub/cli search weather

The 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.

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), or PHOENIXD_URL + PHOENIXD_PASSWORD if you already run Phoenixd for outbound payments. You only need one wallet type. BUDGET_SATS is optional and caps total spending per session (remove for unlimited). See MCP Bridge docs for details.

The bridge:

  1. Fetches the gateway's OpenAPI spec on startup
  2. Exposes each endpoint as an MCP tool with proper inputSchema
  3. Handles L402 payment transparently (402 → pay invoice → retry)
  4. 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

  1. 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 payinvoice API endpoint
  2. L402 parsing logic: the agent must parse the WWW-Authenticate header 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 data

Example: 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_bucket pricing, 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_request endpoints, 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

HeaderWhen to sendFormat
AuthorizationOn retry after paymentL402 {macaroon_base64}:{preimage_hex}

Response headers

HeaderWhen returnedFormat
WWW-AuthenticateOn 402 responsesL402 macaroon="{base64}", invoice="{bolt11}"
X-Session-TokenOn session creation (time_pass, metered, token_bucket, per_kb)Opaque session token string
X-Session-ExpiresOn session creationISO 8601 timestamp
X-Session-BalanceOn session responsesRemaining balance (sats or tokens)
X-Free-TryWhen free try is consumedused
X-CacheOn cacheable GET responsesHIT or MISS
X-Cache-AgeOn cache hitsSeconds since response was cached
X-Data-Size-KBOn per_kb responsesResponse size in kilobytes
X-Data-Cost-SatsOn per_kb responsesSats deducted for this response
X-Request-IdOn all responsesUUID for request correlation

Rate limits

The gateway applies per-IP rate limits:

ScopeDefault limit
Per endpoint30 requests/minute per IP (configurable per endpoint by provider)
Global300 requests/minute per IP across all endpoints

When rate-limited, the gateway returns 429 Too Many Requests.

Status codes

CodeMeaning
200Payment verified, response from origin API
400Bad request - invalid path, method, or request body
402Payment required - inspect WWW-Authenticate for invoice
401Invalid or expired L402 token - start the flow again
403Tenant is not active or suspended
404Endpoint not found or not active
413Request body exceeds the 1 MB limit
429Rate limited - too many requests, retry after backoff
502Origin API unreachable or connection error
503Service 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:

HeaderValuePurpose
X-Gateway-SecretYour tenant's gateway secret (from the dashboard)Static shared secret - reject requests that don't include it
X-Gateway-SignatureHMAC-SHA256 signature of the requestCryptographic proof the request was proxied by bolthub
X-Gateway-TimestampUnix timestamp (ms) when the signature was createdReject stale requests to prevent replay
X-Gateway-NonceUUID generated per requestReject duplicate requests to prevent replay

The signature is computed over a canonical payload:

METHOD\nPATH\nTIMESTAMP\nNONCE\nBODY

Where 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-in crypto
  • 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)
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)

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:

  1. The current secrets become "previous" and remain valid for a grace period.
  2. New secrets are generated and used for all new requests.
  3. 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)