Skip to main content

The bolthub payments SDK for Python: charge for an MCP tool or HTTP endpoint, and pay for them, over Lightning (L402)

Project description

bolthub

The bolthub payments SDK for Python, mirroring @bolthub/pay:

  • Buyer, HTTP: L402Client handles 402 Payment Required challenges, pays the Lightning invoice, and retries with proof of payment.
  • Buyer, MCP: ToolClient pays Tool Payment Profile (TPP) challenges on the MCP wire.
  • Seller: create_paywall + rails turn any MCP tool handler into a paid one, wire-compatible with the TypeScript SDK and the bolthub gateway.

Install

pip install bolthub

# Optional: Nostr Wallet Connect (NwcWallet.from_uri)
pip install 'bolthub[nwc]'

The only required runtime dependency is httpx. NWC support pulls in websockets and cryptography via the nwc extra.

Quick Start

from bolthub import L402Client, LndWallet

wallet = LndWallet(
    host="https://your-lnd-node:8080",
    macaroon="0201036c6e...",
)

client = L402Client(wallet, budget_sats=10_000)

resp = client.get(
    "https://acme.gw.bolthub.ai/v1/market-data",
    params={"symbol": "BTC"},
)
data = resp.json()

Wallet Adapters

LND (recommended)

Full Lightning node. Self-host or use the bolthub Node Launcher, Umbrel, or Start9. Fastest payment path (<200ms) and full control.

from bolthub import LndWallet

wallet = LndWallet(
    host="https://your-lnd-node:8080",
    macaroon="admin-macaroon-hex",
    timeout_seconds=30,
)

For agent deployments, use a scoped pay-only macaroon instead of admin.macaroon:

lncli bakemacaroon uri:/lnrpc.Lightning/SendPaymentSync \
  uri:/lnrpc.Lightning/DecodePayReq \
  --save_to=pay-only.macaroon

NWC (Nostr Wallet Connect)

Easiest to set up but slower (1-3s per payment). No node required. Get a free NWC connection from CoinOS or use Alby Hub, Zeus, or Primal.

Configure directly from the connection URI (requires the nwc extra, pip install 'bolthub[nwc]'):

from bolthub import NwcWallet

wallet = NwcWallet.from_uri(
    "nostr+walletconnect://<wallet_pubkey>?relay=wss://relay.example.com&secret=<hex>"
)

For the async client, use AsyncNwcWallet.from_uri(...). You can still pass your own callback if you prefer to drive NWC yourself:

wallet = NwcWallet(pay_fn=lambda bolt11: my_nwc_pay(bolt11))  # returns preimage hex

LNbits

Supported if you already run LNbits. Multi-wallet accounts system; create a dedicated wallet for your agent.

from bolthub import LnbitsWallet

wallet = LnbitsWallet(
    url="https://lnbits.example.com",
    admin_key="your-admin-key",
)

Phoenixd

Supported if you already run Phoenixd for outbound payments.

from bolthub import PhoenixdWallet

wallet = PhoenixdWallet(
    url="https://your-phoenixd:9740",
    password="your-phoenixd-password",
    timeout_seconds=35,
)

Custom Wallet

Implement the WalletAdapter protocol:

class MyWallet:
    def pay_invoice(self, bolt11: str) -> str:
        preimage = my_payment_logic(bolt11)
        return preimage

Async

AsyncL402Client mirrors L402Client on httpx.AsyncClient. Existing (synchronous) wallets work unchanged — they are run in a worker thread — or use the async adapters (AsyncLndWallet, AsyncLnbitsWallet, AsyncPhoenixdWallet, AsyncNwcWallet) for a fully non-blocking path.

from bolthub import AsyncL402Client, LndWallet

async def main():
    async with AsyncL402Client(LndWallet(host=host, macaroon=mac), budget_sats=10_000) as client:
        resp = await client.get("https://acme.gw.bolthub.ai/v1/market-data")
        return resp.json()

Use with your own httpx client (L402Auth)

L402Auth plugs the L402 flow into a client you own, so you keep your transport, pooling, and retries. It works with both httpx.Client and httpx.AsyncClient:

import httpx
from bolthub import L402Auth, LndWallet

auth = L402Auth(LndWallet(host=host, macaroon=mac), budget_sats=10_000)

with httpx.Client(auth=auth) as client:
    resp = client.get("https://acme.gw.bolthub.ai/v1/market-data")

print(auth.total_spent)

Budget Guards

client = L402Client(
    wallet,
    max_per_request_sats=100,  # reject invoices over 100 sats
    budget_sats=10_000,         # total spending cap
)

print(client.total_spent)       # sats spent so far
print(client.remaining_budget)  # sats remaining

The price of each invoice is determined from the response body (amountSats), the BOLT11 invoice itself, or an optional price_header. If it still cannot be determined, on_unknown_amount controls what happens — by default ("cap") the client pays only up to max_per_request_sats and refuses outright if no ceiling is set, so a price-less challenge is never paid blind. Use "refuse" to always refuse, or "allow" for the legacy pay-blind behaviour.

Thread Safety

A single L402Client (or L402Auth) may be shared across threads. Budget accounting is atomic, so total_spent is always exact and the budget is never exceeded under concurrent requests; the lock is held only around the budget check, not across the network or payment, so requests still run in parallel.

Session Persistence

By default sessions are kept in memory. Use FileSessionStore to persist tokens across process restarts (stored in ~/.bolthub/sessions.json):

from bolthub import L402Client, LndWallet, FileSessionStore

client = L402Client(
    LndWallet(host=host, macaroon=macaroon),
    session_store=FileSessionStore(),
)

Selling: paywall an MCP tool (TPP)

create_paywall wraps any tool handler so a call must carry a valid payment proof. It implements the bolthub Tool Payment Profile: an unpaid call returns a payment_required challenge in result["_meta"]["ai.bolthub/payment"]; a call carrying a verified proof runs the real handler. Framework-agnostic: handlers take (args, extra) and return a dict-shaped ToolResult, so there is no MCP SDK dependency (both def and async def handlers work).

from bolthub import create_paywall, l402_rail

class MyInvoices:
    def create_invoice(self, amount_sat: int, memo: str) -> tuple[str, str]:
        """Return (bolt11_invoice, payment_hash_hex) from your node/wallet."""
        ...

pay = create_paywall(rails=[l402_rail(SECRET, MyInvoices())])

# Wrap a handler directly...
paid_handler = pay(get_image, price={"amount": 2000, "asset": "sat"},
                   resource="get_satellite_image")

# ...or register on an MCP-style server (resource defaults to the tool name):
pay.tool(server, "get_satellite_image", "Recent imagery", schema, get_image,
         price={"amount": 2000})

pay.advertise({"amount": 2000})  # discovery-time price advertisement

To run on the hosted path instead of minting locally, swap the rail: facilitator_rail("l402", ["sat"], http_facilitator(base_url, api_key)).

Buying: pay for MCP tool calls (ToolClient)

ToolClient is the buyer-side counterpart: it calls a tool, and when the result is a payment_required challenge it picks an offer it has a payer for, budget-gates it, pays, and retries the call with the proof in _meta.

from bolthub import Budget, ToolClient, l402_payer, NwcWallet

buyer = ToolClient(
    [l402_payer(NwcWallet.from_uri(NWC_URI))],
    max_total={"sat": 10_000},     # per-asset lifetime ceiling
    max_per_call={"sat": 500},     # per-asset per-call ceiling
)
result = buyer.call_tool(mcp_client, "get_satellite_image", {"lat": 47.5})
buyer.spent_for("sat")             # 2000

Pass one shared Budget(max_total={"sat": 10_000}) as budget= to several clients to enforce a single spending pool across them. Budget violations raise PaymentBudgetError; failed payments roll the reservation back and raise PaymentError.

Token primitives (sign_l402_token, verify_l402_token, verify_preimage, sha256_hex, random_preimage) are exported too, and produce byte-identical tokens to the TypeScript SDK (asserted by shared golden vectors in tests/fixtures/tpp_vectors.json).

Delegation (attenuation)

A paid L402 macaroon can be narrowed offline and handed to a sub-agent, so a parent agent can delegate a restricted credential without re-paying. Needs the optional pymacaroons dependency (pip install bolthub[delegation]):

import time
from bolthub import attenuate

# `macaroon` is the value from `Authorization: L402 <macaroon>:<preimage>`.
restricted = attenuate(
    macaroon,
    method="GET",
    valid_until=int(time.time() * 1000) + 60_000,  # 60s, tighter than the original
)
# Give `restricted` plus the SAME preimage to the sub-agent.

The gateway enforces every caveat down the chain (most restrictive wins).

API Reference

Export Description
L402Client HTTP client with automatic L402 challenge handling
AsyncL402Client Async client on httpx.AsyncClient
L402Auth httpx.Auth for plugging L402 into your own client
LndWallet / AsyncLndWallet Wallet adapter for LND REST API
LnbitsWallet / AsyncLnbitsWallet Wallet adapter for LNbits
PhoenixdWallet / AsyncPhoenixdWallet Wallet adapter for Phoenixd
NwcWallet / AsyncNwcWallet NWC wallet; from_uri(...) for NIP-47 (needs bolthub[nwc])
SyncWalletAdapter Run a sync wallet under the async client
WalletAdapter / AsyncWalletAdapter Protocols for custom wallets
FileSessionStore / InMemorySessionStore Session token storage
SessionStore Protocol for custom session storage
L402Error Base exception for L402 failures
L402BudgetError Raised when budget limits are exceeded
attenuate(...) Narrow a macaroon offline to delegate a restricted credential (needs bolthub[delegation])
create_paywall(rails=...) / Paywall Seller: wrap MCP tool handlers behind a TPP paywall
l402_rail(secret, invoice_provider) L402 settlement rail (mints invoice + signed token, verifies proofs)
facilitator_rail(...) / http_facilitator(...) Rail that delegates mint/verify to a hosted bolthub facilitator
ToolClient Buyer: pay-and-retry client for TPP challenges on the MCP wire
l402_payer(wallet) Buyer-side L402 payer (<token>:<preimage> proofs)
Budget Per-asset reserve/rollback spending pool, shareable across clients
PaymentError / PaymentBudgetError Buyer-side payment failures / budget violations
get_payment_challenge(result) Extract a payment_required challenge from a tool result
sign_l402_token / verify_l402_token HMAC-signed L402 token primitives (wire-compatible with @bolthub/pay)
verify_preimage / sha256_hex / random_preimage Preimage and hash helpers
PAYMENT_META_KEY / SPEC_VERSION TPP _meta key (ai.bolthub/payment) and spec version (0.1)
PaymentRail / PaymentPayer / InvoiceProvider / FacilitatorTransport Protocols for custom rails, payers, and invoice backends

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

bolthub-0.4.0.tar.gz (59.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

bolthub-0.4.0-py3-none-any.whl (47.8 kB view details)

Uploaded Python 3

File details

Details for the file bolthub-0.4.0.tar.gz.

File metadata

  • Download URL: bolthub-0.4.0.tar.gz
  • Upload date:
  • Size: 59.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for bolthub-0.4.0.tar.gz
Algorithm Hash digest
SHA256 5190a8ad32adcbd62449be7f9e769ca4972489ef61a576aeb70eb086270e40cb
MD5 78a532e9f1a5b4e17e7ed793f0bde9d9
BLAKE2b-256 e6a963d53bad29e3f979b90b489af779645dc76ec5a35ab5b901237d041feccc

See more details on using hashes here.

Provenance

The following attestation bundles were made for bolthub-0.4.0.tar.gz:

Publisher: publish.yml on signaltech-org/bolthub-sdk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file bolthub-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: bolthub-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 47.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for bolthub-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a417b4e4598362bffc46734d18fb8da8aaa0636e3dc19396533d87f683e9474a
MD5 faae6743a8e2cee24b55dc597edfe91c
BLAKE2b-256 05dd20830ce53521511f0c331afac04c9902114aa93872b6ac0d3d8d3402c93c

See more details on using hashes here.

Provenance

The following attestation bundles were made for bolthub-0.4.0-py3-none-any.whl:

Publisher: publish.yml on signaltech-org/bolthub-sdk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page