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:
L402Clienthandles402 Payment Requiredchallenges, pays the Lightning invoice, and retries with proof of payment. - Buyer, MCP:
ToolClientpays 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 — including the HTTP
L402Client/AsyncL402Client (0.4.1+), so the MCP and HTTP-402 payment
paths can never jointly overspend. Budget violations raise
PaymentBudgetError (L402BudgetError on the HTTP clients); failed payments
roll the reservation back. The HTTP clients also take a per-request
max_cost_sats= ceiling and on_paid= callbacks (client-level and
per-request) for exact cost attribution.
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file bolthub-0.4.1.tar.gz.
File metadata
- Download URL: bolthub-0.4.1.tar.gz
- Upload date:
- Size: 62.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
14830b3a8acc32f15a159c367c936a081ebc7e29815518e3ebb944cab0ad5dd7
|
|
| MD5 |
e84f2e4f0b4f759455c8b9d7f3883c87
|
|
| BLAKE2b-256 |
a723b4c8de6221158ca15d992c9278e32ffeb0c209d965edf214a724f660ea23
|
Provenance
The following attestation bundles were made for bolthub-0.4.1.tar.gz:
Publisher:
publish.yml on signaltech-org/bolthub-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
bolthub-0.4.1.tar.gz -
Subject digest:
14830b3a8acc32f15a159c367c936a081ebc7e29815518e3ebb944cab0ad5dd7 - Sigstore transparency entry: 2092235269
- Sigstore integration time:
-
Permalink:
signaltech-org/bolthub-sdk@ec8ea6023996ec46a4b2e93aeb363bdc6714fda4 -
Branch / Tag:
refs/tags/v0.4.1 - Owner: https://github.com/signaltech-org
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ec8ea6023996ec46a4b2e93aeb363bdc6714fda4 -
Trigger Event:
push
-
Statement type:
File details
Details for the file bolthub-0.4.1-py3-none-any.whl.
File metadata
- Download URL: bolthub-0.4.1-py3-none-any.whl
- Upload date:
- Size: 49.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3369462f439b49b99d413090e52f067353000d553a363ebd8d16c21bbe6d17a2
|
|
| MD5 |
73945125a66c0cb1d960266c47b278fc
|
|
| BLAKE2b-256 |
3a90e58a1c25cd78afa3653001c23ed0d061aa98b48394099b2f42d9bfd4d630
|
Provenance
The following attestation bundles were made for bolthub-0.4.1-py3-none-any.whl:
Publisher:
publish.yml on signaltech-org/bolthub-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
bolthub-0.4.1-py3-none-any.whl -
Subject digest:
3369462f439b49b99d413090e52f067353000d553a363ebd8d16c21bbe6d17a2 - Sigstore transparency entry: 2092235377
- Sigstore integration time:
-
Permalink:
signaltech-org/bolthub-sdk@ec8ea6023996ec46a4b2e93aeb363bdc6714fda4 -
Branch / Tag:
refs/tags/v0.4.1 - Owner: https://github.com/signaltech-org
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ec8ea6023996ec46a4b2e93aeb363bdc6714fda4 -
Trigger Event:
push
-
Statement type: