Python SDK
bolthub: the bolthub payments SDK in Python — L402 client, wallet adapters, and the Tool Payment Profile (seller + buyer).
bolthub is the payments SDK in Python: the same two sides as
@bolthub/pay. The buyer side calls L402-gated HTTP APIs
(L402Client) and paid MCP tools (ToolClient); the seller side prices your
own tools (create_paywall). The seller and MCP-buyer APIs mirror
@bolthub/pay name-for-name (snake_case) and landed in bolthub 0.4.0.
Install
pip install bolthubMinimal dependencies. Only one runtime dependency (
httpx). No other third-party packages in your supply chain.
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
from bolthub import LndWallet
wallet = LndWallet(
host="https://your-lnd-node:8080",
macaroon="scoped-macaroon-hex", # bake payment-scoped, never admin.macaroon
timeout_seconds=30,
)Bake the scoped macaroon per the agent wallet security guide — paying needs send permissions, but admin.macaroon grants far more than any agent should hold.
NWC (Nostr Wallet Connect)
NwcWallet takes a callable that receives a BOLT11 invoice and must return the preimage as a hex string. Plug in any NWC library you already use to handle the payment:
from bolthub import NwcWallet
# pay_fn receives the BOLT11 invoice string and returns the preimage hex
def pay(bolt11: str) -> str:
preimage = my_nwc_client.pay_invoice(bolt11)
return preimage
wallet = NwcWallet(pay_fn=pay)LNbits
from bolthub import LnbitsWallet
wallet = LnbitsWallet(
url="https://lnbits.example.com",
admin_key="your-admin-key",
)Phoenixd
Use PhoenixdWallet when your agent pays via an existing Phoenixd HTTP API. Prefer LndWallet with the bolthub Node Launcher or your own LND for new setups.
from bolthub import PhoenixdWallet
wallet = PhoenixdWallet(
url="http://localhost: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 preimageBudget 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 remainingSeller: charge for an MCP tool
create_paywall wraps a tool handler so an unpaid call returns a
payment_required challenge and the handler runs only once a valid proof
verifies — the Python port of @bolthub/pay's seller side, speaking the same
Tool Payment Profile wire format. It is framework-agnostic: handlers take
(args, extra) and return a dict-shaped tool result; both def and
async def handlers work.
A rail needs a signing secret (32+ characters) and an invoice provider — any
object with create_invoice(amount_sat, memo) -> (bolt11, payment_hash_hex):
from bolthub import create_paywall, l402_rail
class MyInvoiceProvider:
def create_invoice(self, amount_sat: int, memo: str) -> tuple[str, str]:
inv = my_wallet.make_invoice(amount_sat, memo)
return inv.bolt11, inv.payment_hash
pay = create_paywall(rails=[l402_rail(os.environ["PAY_SECRET"], MyInvoiceProvider())])
# Register the tool; `resource` defaults to the tool name.
pay.tool(
server,
"get_satellite_image",
"Recent high-res satellite imagery for a lat/lon and date.",
schema,
fetch_image, # (args, extra) -> ToolResult dict
price={"amount": 2000}, # 2000 sats per call
)Or wrap just the handler and register it yourself:
paid_handler = pay(fetch_image, price={"amount": 2000}, resource="get_satellite_image")pay.advertise(price) builds the discovery-time advertisement for cost-aware
agents, exactly like the TypeScript pay.advertise.
To delegate mint/verify to the hosted facilitator
(at-most-once redemption, metering), use facilitator_rail with
http_facilitator:
from bolthub import create_paywall, facilitator_rail, http_facilitator
transport = http_facilitator("https://api.bolthub.ai/facilitator", api_key)
pay = create_paywall(rails=[facilitator_rail("l402", ["sat"], transport)])The token primitives are exported too, for building your own rail or
verifier: sign_l402_token, verify_l402_token, verify_preimage,
sha256_hex, random_preimage, plus the PAYMENT_META_KEY constant
("ai.bolthub/payment").
Buyer: pay for MCP tools automatically
ToolClient is the paywall's counterpart: it calls a tool, and if the result
carries a payment_required challenge it pays an offer within your budget
and retries. Free tools pass through untouched.
from bolthub import ToolClient, l402_payer
client = ToolClient(
payers=[l402_payer(wallet)], # any WalletAdapter, e.g. LndWallet
max_total={"sat": 10_000}, # per-asset lifetime cap
max_per_call={"sat": 500}, # per-asset per-call cap
on_paid=lambda i: print(f"paid {i['amount']} {i['asset']} via {i['scheme']}"),
)
# call_tool handles challenge → pay → retry transparently. `mcp_client` only
# needs a call_tool(name=..., arguments=..., meta=...) method.
result = client.call_tool(mcp_client, "get_satellite_image", {"lat": lat, "lon": lon})The budget is a hard gate: the amount is reserved (thread-safely) before the
payment happens and rolled back if it fails. When every matching offer
exceeds the budget, PaymentBudgetError is raised; when no configured payer
matches an offered rail, the unpaid challenge result is returned so you can
decide.
One budget across both buyer paths
Pass the same Budget to a ToolClient (MCP-wire payments) and an
L402Client or AsyncL402Client (HTTP-402 payments, bolthub >= 0.4.1) and
together they can never spend past max_total, even under concurrent calls —
the same guarantee as TypeScript's @bolthub/pay. On the clients, budget=
is mutually exclusive with budget_sats; the budget's max_per_call["sat"]
also caps each request when max_per_request_sats is unset.
from bolthub import Budget, L402Client, ToolClient, l402_payer
budget = Budget(max_total={"sat": 10_000}, max_per_call={"sat": 500})
tools = ToolClient(payers=[l402_payer(wallet)], budget=budget)
http = L402Client(wallet, budget=budget, on_paid=print)Both clients also take a per-request max_cost_sats= (a one-off ceiling that
tightens, never loosens, the configured caps) and a per-request on_paid=
for exact per-call cost attribution:
resp = http.get("https://acme.gw.bolthub.ai/v1/data", max_cost_sats=100)Payment receipts
Parity with @bolthub/pay: configure a receipt_store and the client records
one verifiable proof-of-payment receipt per paid call.
from bolthub import L402Client, FileReceiptStore, verify_receipt
client = L402Client(wallet, receipt_store=FileReceiptStore())
# ... paid calls happen ...
csv_report = client.export_receipts(format="csv", redact=True)verify_receipt(receipt) runs the offline checks (preimage hashes to
payment_hash, which equals the hash the BOLT11 invoice commits to, and the
amount matches). Receipt files carry live preimages: treat them like
credentials, and export with redact=True for shareable reports.
Prepaid credit (across a provider's endpoints)
Parity with @bolthub/pay. When you'll call several of one provider's endpoints,
buy_credit pays once for a sats budget spendable across all of them. Ordinary
calls to any of that provider's endpoints then draw the budget with no further
payment, until it runs out.
# One Lightning payment for a budget usable across the provider's endpoints.
client.buy_credit("https://acme.gw.bolthub.ai/v1/data", 10_000)
# Any endpoint of acme now draws the credit: no invoice is paid.
client.get("https://acme.gw.bolthub.ai/v1/data")
client.get("https://acme.gw.bolthub.ai/v1/reports")Credit is face-value: you pass a sats budget and the gateway charges exactly
that, with no discount tiers. The client verifies the server echoed the requested
budget before it pays, so a provider that hasn't enabled credit is refused with
nothing spent. Credit is scoped to the provider (cached per host), so buying
credit for one provider never covers another, and the same budget_sats and
max_cost_sats rules apply. AsyncL402Client exposes the same
await client.buy_credit(...). Unused credit at expiry is non-refundable, so
size it to what you expect to spend.
For URLs across several providers, batch_fetch groups them by provider, buys
one credit per provider, and fetches them all. Non-custodial by construction:
N providers means N payments, never a pooled balance. The async client fetches
the group concurrently.
results = client.batch_fetch(
[
"https://acme.gw.bolthub.ai/v1/data",
"https://acme.gw.bolthub.ai/v1/reports",
"https://bolt.gw.bolthub.ai/v1/prices",
],
credit_sats=10_000, # sized to cover your calls per provider
)Delegation (scoped sub-agent credentials)
A paid L402 macaroon can be narrowed offline and handed to another agent, so a
parent that paid for access delegates a restricted credential without
re-paying. The gateway enforces every caveat down the chain (most restrictive
wins). 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
n_uses=50, # at most 50 requests
max_sats=300, # at most 300 sats of spend
path_prefix="/v1/reports", # only paths at or under /v1/reports
)
# Hand `restricted` plus the SAME preimage to the sub-agent.Parity with @bolthub/pay: n_uses, max_sats, and path_prefix are backed by
a server-side grant, so they hold across processes. Attenuation is tighten-only:
each restriction is checked against the credential's existing caveats and raises
ValueError if it would widen scope (raise a cap, move the expiry later, or step
outside the parent's path). The gateway enforces the same folds, so a child can
never escalate. The credential you narrow is a prepaid-credit credential you
already hold (buy_credit first), so one payment funds every scoped child you
hand out across the provider's endpoints.