Until recently, every Lightning Service Provider built their own proprietary protocol. Loop (Lightning Labs), Boltz, Voltage, and others each defined their own API for channel opens, liquidity requests, and submarine swaps. A wallet integrating three LSPs wrote three different clients, handled three different error models, and maintained three separate integrations when any of the APIs changed.
The LSPS specifications (Lightning Service Provider Specifications) are an attempt to end this. They define a common protocol layer that any LSP can implement and any wallet can integrate against once.
As of July 2026, LSPS0, LSPS1, and LSPS2 are the active specifications. LSPS2 has been adopted into the BOLT process as bLIP-52. Understanding these specifications is relevant to any developer building Lightning wallets, LSPs, or the infrastructure between them.
What Problem LSPs Solve
A new Lightning wallet user faces the inbound liquidity problem immediately. Lightning requires you to receive a channel from someone before you can receive payments. If you have never received a channel, you cannot receive your first payment.
This is not a theoretical concern. It is the first failure mode every new Lightning user encounters.
Lightning Service Providers solve this by providing channels on demand. An LSP:
- Maintains a pool of liquidity (bitcoin in channels)
- Accepts requests from wallets for inbound channels
- Opens channels, charges fees, and manages the lifecycle of those channels
- Optionally provides Just-in-Time (JIT) channel opens; opening a channel in real time when a payment arrives for a wallet that does not yet have one
The operational complexity is on the LSP side. The wallet tells the LSP what it needs; the LSP figures out how to provide it.
LSPS0: The Transport Layer
LSPS0 is the foundation. It specifies how messages are exchanged between a client (wallet) and an LSP. Without a common transport, each LSPS spec would require its own communication mechanism.
The choice is JSON-RPC 2.0 over the existing Lightning peer-to-peer messaging layer. Specifically, LSPS0 uses Lightning's custom_message mechanism (message type 37913) to carry JSON-RPC requests and responses between two Lightning nodes.
This is significant. It means:
- No new network connections are required
- The LSP's Lightning node is the API endpoint
- Authentication is the Lightning key exchange — you are already authenticated as a peer
- The protocol is encrypted by the existing Lightning noise protocol
The message format follows JSON-RPC 2.0:
// Request from client to LSP
{
"jsonrpc": "2.0",
"method": "lsps1.get_info",
"params": {},
"id": "a1b2c3d4"
}
// Response from LSP to client
{
"jsonrpc": "2.0",
"result": {
"supported_versions": [1],
"website": "https://example-lsp.com",
"options": {
"minimum_channel_confirmations": 0,
"minimum_onchain_payment_confirmations": 1,
"supports_zero_channel_reserve": true,
"min_onchain_payment_size_sat": 100000,
"max_channel_expiry_blocks": 13140,
"min_initial_client_balance_sat": "0",
"min_initial_lsp_balance_sat": "100000",
"max_initial_client_balance_sat": "100000000",
"min_channel_balance_sat": "100000",
"max_channel_balance_sat": "100000000"
}
},
"id": "a1b2c3d4"
}
Implementing LSPS0 transport with LND:
package lsps
import (
"encoding/json"
"github.com/lightningnetwork/lnd/lnwire"
)
const LSPSMessageType = lnwire.MessageType(37913)
type JSONRPCRequest struct {
JSONRPC string `json:"jsonrpc"`
Method string `json:"method"`
Params json.RawMessage `json:"params"`
ID string `json:"id"`
}
type JSONRPCResponse struct {
JSONRPC string `json:"jsonrpc"`
Result json.RawMessage `json:"result,omitempty"`
Error *JSONRPCError `json:"error,omitempty"`
ID string `json:"id"`
}
type JSONRPCError struct {
Code int `json:"code"`
Message string `json:"message"`
}
// Error codes defined by LSPS0
const (
ErrParseError = -32700
ErrInvalidRequest = -32600
ErrMethodNotFound = -32601
ErrInvalidParams = -32602
ErrInternalError = -32603
// LSPS-specific error range: -32099 to -32000
ErrUnacceptableFee = 100
ErrPaymentRequired = 101
ErrOptionMismatch = 102
)
func SendLSPSMessage(lndClient LNDClient, peerPubkey string, req JSONRPCRequest) error {
data, err := json.Marshal(req)
if err != nil {
return err
}
return lndClient.SendCustomMessage(peerPubkey, LSPSMessageType, data)
}
LSPS0 also defines service discovery. A client can query lsps0.list_protocols to learn which LSPS versions the LSP supports:
// Request
{"jsonrpc": "2.0", "method": "lsps0.list_protocols", "params": {}, "id": "disc-1"}
// Response
{"jsonrpc": "2.0", "result": {"supported_protocols": [0, 1, 2]}, "id": "disc-1"}
LSPS1: Channel Purchase
LSPS1 covers the straightforward case: a client wants to buy a channel from an LSP. The client requests a channel with specific parameters, the LSP provides a quote, and the client pays for the channel on-chain.
The protocol has four methods:
| Method | Purpose |
|---|---|
lsps1.get_info |
Client discovers LSP capabilities and pricing parameters |
lsps1.create_order |
Client requests a channel with specific parameters |
lsps1.get_order |
Client polls for order status updates |
A typical LSPS1 flow:
Step 1: Get info
// Request
{"jsonrpc": "2.0", "method": "lsps1.get_info", "params": {}, "id": "info-1"}
// Response — LSP advertises its capabilities
{
"jsonrpc": "2.0",
"result": {
"supported_versions": [1],
"options": {
"min_channel_balance_sat": "100000",
"max_channel_balance_sat": "10000000",
"min_initial_client_balance_sat": "0",
"min_initial_lsp_balance_sat": "100000",
"max_initial_client_balance_sat": "5000000",
"min_channel_confirmations": 0,
"min_onchain_payment_confirmations": 1,
"supports_zero_channel_reserve": true,
"max_channel_expiry_blocks": 13140
}
},
"id": "info-1"
}
Step 2: Create order
// Client requests a 500,000 sat channel with 100,000 sat on their side
{
"jsonrpc": "2.0",
"method": "lsps1.create_order",
"params": {
"lsp_balance_sat": "400000",
"client_balance_sat": "100000",
"required_channel_confirmations": 0,
"funding_confirms_within_blocks": 6,
"channel_expiry_blocks": 4380,
"token": "",
"refund_onchain_address": "bc1q...",
"announce_channel": false
},
"id": "order-1"
}
// LSP responds with payment instructions
{
"jsonrpc": "2.0",
"result": {
"order_id": "Sl3HM9rvCy",
"lsp_balance_sat": "400000",
"client_balance_sat": "100000",
"required_channel_confirmations": 0,
"funding_confirms_within_blocks": 6,
"channel_expiry_blocks": 4380,
"announce_channel": false,
"order_state": "CREATED",
"payment": {
"state": "EXPECT_PAYMENT",
"fee_total_sat": "3000",
"order_total_sat": "103000",
"onchain_address": "bc1q...",
"onchain_payment": {
"min_fee_for_0conf": "253",
"min_onchain_payment_confirmations": 1
}
},
"channel": null,
"created_at": "2026-07-17T10:00:00.000Z",
"expires_at": "2026-07-17T11:00:00.000Z"
},
"id": "order-1"
}
Step 3: Client pays the on-chain address
The client sends order_total_sat to the LSP's on-chain address. Once confirmed, the LSP opens the channel.
Step 4: Poll for order status
import asyncio
import httpx
async def wait_for_channel(lsp_node, order_id: str, poll_interval: int = 30):
while True:
response = await send_lsps_message(lsp_node, {
"jsonrpc": "2.0",
"method": "lsps1.get_order",
"params": {"order_id": order_id},
"id": f"poll-{order_id}"
})
order = response["result"]
state = order["order_state"]
if state == "COMPLETED":
return order["channel"]
elif state == "FAILED":
raise Exception(f"Order failed: {order}")
else:
print(f"Order state: {state}. Waiting...")
await asyncio.sleep(poll_interval)
LSPS1 is the simple case. The client proactively requests a channel and pays for it. The complexity is in LSPS2, where channels are opened reactively when a payment arrives.
LSPS2: Just-in-Time Channels (bLIP-52)
LSPS2, now also published as bLIP-52, is the specification for Just-in-Time channel opens. The scenario: a user receives their first Lightning payment, but they have no inbound channel. The LSP intercepts the payment at the last hop, opens a channel to the wallet in real time, and routes the payment through that new channel.
This eliminates the bootstrapping problem. A new user can receive their first payment without any prior channel setup.
The protocol is more complex than LSPS1 because it coordinates a payment interception with a channel open, and both must happen atomically from the user's perspective.
The LSPS2 flow:
Sender ──────────────────────────────── LSP ─── (JIT channel open) ─── Wallet
HTLC to fake_scid channel open to wallet
(intercepted by LSP) HTLC routed through new channel
The key mechanism is a fake Short Channel ID (SCID). The LSP assigns the wallet a SCID that does not correspond to a real channel. When a sender routes a payment using that SCID, the LSP intercepts it, opens a real channel, and forwards the payment.
Step 1: Client gets a SCID-wrapped invoice
// Client requests a buy channel parameter set
{
"jsonrpc": "2.0",
"method": "lsps2.get_info",
"params": {"token": ""},
"id": "jit-info-1"
}
// LSP responds with fee structure
{
"jsonrpc": "2.0",
"result": {
"supported_versions": [1],
"min_payment_size_msat": "1000",
"max_payment_size_msat": "1000000000",
"opening_fee_params_menu": [
{
"min_fee_msat": "2000",
"proportional": 5000,
"valid_until": "2026-07-17T11:00:00Z",
"min_lifetime": 1008,
"max_client_to_self_delay": 2016,
"promise": "abc123..."
}
]
},
"id": "jit-info-1"
}
The promise is a server-side signed commitment to the fee parameters. It prevents the LSP from changing fees between the client's decision and the actual channel open.
Step 2: Client creates an invoice with a route hint
def create_jit_invoice(
amount_msat: int,
lsp_pubkey: str,
fake_scid: str, # assigned by LSP
opening_fee_params: dict
) -> str:
"""
Create a BOLT 11 invoice with a route hint pointing to the LSP's fake SCID.
The route hint tells the sender to route through the LSP.
The LSP intercepts, opens a channel, and forwards.
"""
route_hint = RouteHint(
pubkey=lsp_pubkey,
short_channel_id=fake_scid,
fee_base_msat=opening_fee_params["min_fee_msat"],
fee_proportional_millionths=opening_fee_params["proportional"],
cltv_expiry_delta=40
)
return create_bolt11_invoice(
amount_msat=amount_msat,
description="Payment",
route_hints=[route_hint]
)
Step 3: Sender pays the invoice
The sender routes through the LSP using the fake SCID in the route hint. The LSP's routing layer intercepts the HTLC because it knows that fake SCID belongs to a JIT channel.
Step 4: LSP opens the channel and forwards
The LSP must:
- Open a zero-confirmation channel to the wallet
- Route the HTLC through the new channel
- Deduct the opening fee from the payment amount
async def handle_jit_payment_interception(
htlc: InterceptedHTLC,
lsp_client,
channel_manager
) -> None:
wallet_pubkey = lsp_client.get_wallet_for_scid(htlc.short_channel_id)
if wallet_pubkey is None:
# Not a JIT SCID — pass through normally
await lsp_client.forward_htlc(htlc)
return
# Calculate fee deduction
opening_fee_msat = calculate_opening_fee(htlc.amount_msat)
payment_to_wallet_msat = htlc.amount_msat - opening_fee_msat
# Open zero-conf channel
channel = await channel_manager.open_channel(
peer_pubkey=wallet_pubkey,
local_amount_sat=payment_to_wallet_msat // 1000 + CHANNEL_RESERVE,
zero_conf=True
)
# Forward HTLC through new channel
await lsp_client.forward_htlc_via(htlc, channel.channel_id, payment_to_wallet_msat)
Zero-confirmation channels are essential here, the LSP cannot wait for block confirmations while holding an HTLC. BOLT 2 supports zero-conf channels via the option_zeroconf feature bit.
What LSPS2 Does Not Specify
Understanding specification boundaries is as important as understanding what the spec covers. LSPS2 does not specify:
LSP liquidity management: How the LSP maintains a pool of liquidity, how it manages its own channels to routing nodes, or how it prices liquidity over time. These are operator concerns.
Wallet fallback behavior: What the wallet does if the JIT channel open fails mid-payment. LSPS2 expects the LSP to fail the HTLC back upstream; the wallet receives a payment failure and must decide whether to retry.
Multi-LSP coordination: A wallet using two LSPs concurrently, with different fake SCIDs for different invoices, is outside scope.
Channel closure policy: When and how the LSP closes JIT channels. LSPS2 specifies min_lifetime (the LSP's commitment to keep the channel open for a minimum number of blocks), but LSP behavior after that point is implementation-defined.
Implementing a Minimal LSPS Client
A minimal client library that works across LSPS0, LSPS1, and LSPS2:
import json
import uuid
from typing import Any
class LSPSClient:
"""
Minimal LSPS client over the Lightning peer messaging layer.
Requires a Lightning node client that supports custom messages.
"""
def __init__(self, node_client, lsp_pubkey: str):
self.node_client = node_client
self.lsp_pubkey = lsp_pubkey
self._pending: dict[str, asyncio.Future] = {}
async def call(self, method: str, params: dict = {}) -> Any:
request_id = str(uuid.uuid4())
request = {
"jsonrpc": "2.0",
"method": method,
"params": params,
"id": request_id
}
future = asyncio.get_event_loop().create_future()
self._pending[request_id] = future
await self.node_client.send_custom_message(
peer_pubkey=self.lsp_pubkey,
message_type=37913,
data=json.dumps(request).encode()
)
response = await asyncio.wait_for(future, timeout=30.0)
if "error" in response:
raise LSPSError(
code=response["error"]["code"],
message=response["error"]["message"]
)
return response["result"]
def handle_incoming_message(self, data: bytes) -> None:
"""Call this when a custom message arrives from the LSP peer."""
response = json.loads(data)
request_id = response.get("id")
if request_id and request_id in self._pending:
future = self._pending.pop(request_id)
future.set_result(response)
# High-level methods
async def list_protocols(self) -> list[int]:
result = await self.call("lsps0.list_protocols")
return result["supported_protocols"]
async def get_lsps1_info(self) -> dict:
return await self.call("lsps1.get_info")
async def create_lsps1_order(self, params: dict) -> dict:
return await self.call("lsps1.create_order", params)
async def get_lsps1_order(self, order_id: str) -> dict:
return await self.call("lsps1.get_order", {"order_id": order_id})
async def get_lsps2_info(self, token: str = "") -> dict:
return await self.call("lsps2.get_info", {"token": token})
async def create_lsps2_channel(self, params: dict) -> dict:
return await self.call("lsps2.buy", params)
class LSPSError(Exception):
def __init__(self, code: int, message: str):
self.code = code
super().__init__(f"LSPS error {code}: {message}")
The State of the Specifications in July 2026
LSPS0 is stable. No implementations have reported protocol-breaking issues and the custom message transport has proven reliable across LND, CLN, and LDK implementations.
LSPS1 is implemented by several LSPs in production. The main implementation variation is in fee structures, the spec allows LSPs to define their own pricing models, which means interoperability exists at the protocol level but pricing comparison still requires manual interpretation.
LSPS2 (bLIP-52) has production implementations in LND and CLN. The primary open question in the specification is around privacy: the fake SCID assigned by the LSP can be used to identify the wallet's LSP relationship, which is a metadata leak. The specification currently requires this tradeoff but notes it as an area for future work.
Areas where the specifications are actively evolving:
- LSPS5 (Webhooks): Defines push notifications from LSP to client, allowing the LSP to notify the wallet when a JIT channel open has succeeded or when a channel is approaching expiry. At draft stage.
- LSPS3 (Channel Close): Standardizing cooperative channel close negotiation between client and LSP. Needed for clean LSP-initiated closes.
- Privacy improvements to LSPS2: Proposals exist to reduce the SCID-based LSP identification leak, but none have reached specification stage.
Why This Matters for Builders
If you are building a Lightning wallet, integrating LSPS means one integration covers multiple LSPs. The competitive differentiation between LSPs shifts to pricing, reliability, and geographic coverage, not API shape. This is better for the ecosystem.
If you are building an LSP, implementing LSPS means any LSPS-compatible wallet can use your service without custom integration work. The addressable market is larger.
If you are building Lightning infrastructure that manages channels, routing nodes, channel managers, node operators, LSPS2's JIT channel protocol is the standard to understand for how wallets expect channels to arrive. Implementations that do not understand LSPS2's fake SCID mechanism will not correctly route JIT payments.
The specifications are small enough to read in an afternoon. LSPS0 is 15 pages. LSPS1 is 20 pages. LSPS2/bLIP-52 is 25 pages. Reading them gives you a more accurate model of how Lightning liquidity works at the protocol level than any summary can.
Further Reading
- LSPS0 specification — https://github.com/lightning/blips/blob/master/blip-0050.md
- LSPS1 specification — https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS1/README.md
- LSPS2 / bLIP-52 specification — https://github.com/lightning/blips/blob/master/blip-0052.md
- Lightning BLIP repository — https://github.com/lightning/blips
- LSP specification repository — https://github.com/BitcoinAndLightningLayerSpecs/lsp
- Bitcoin Optech on zero-conf channels — https://bitcoinops.org/en/topics/zero-conf-channels/
Top comments (0)