Api

Node API

Channel and payment management

The Node API provides Lightning Network channel management and payment operations.

JSON-RPC namespace: node

Endpoints

Peer Management

Zero-Conf Whitelist

Peer Blacklist

Node Configuration

Channel Operations

Batch Channel Operations

Payment Operations

Invoice Management

Funding Allowances


Connect to Peer

Connect to a Lightning Network peer.

Method: ConnectToPeer

Parameters:

NameTypeRequiredDescription
networkNetworkYESNetwork to connect on
peer_urlstringYESPeer connection URL

Peer URL Format:

node_id@host:port

Response: Empty (success confirmation)

Example Request:

TypeScript
import { NodeServiceClient } from './proto/NodeServiceClientPb'
import { ConnectToPeerRequest } from './proto/node_pb'

const client = new NodeServiceClient('http://localhost:5003')

const request = new ConnectToPeerRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setPeerUrl('02abc123...@lightning.example.com:9735')

await client.connectToPeer(request, {})
console.log('Connected to peer')
Go
import (
    pb "github.com/hydra/hydra-go/proto"
)

client := pb.NewNodeServiceClient(conn)

req := &pb.ConnectToPeerRequest{
    Network: &pb.Network{
        Protocol: pb.Protocol_PROTOCOL_BITCOIN,
        Id:       "0a03cf40",
    },
    PeerUrl: "02abc123...@lightning.example.com:9735",
}

_, err := client.ConnectToPeer(context.Background(), req)
if err != nil {
    log.Fatal(err)
}

log.Println("Connected to peer")
Rust
use hydra_app::node_service_client::NodeServiceClient;
use hydra_app::{ConnectToPeerRequest, Network};

let mut client = NodeServiceClient::new(channel);

let request = tonic::Request::new(ConnectToPeerRequest {
    network: Some(Network {
        protocol: Protocol::Bitcoin as i32,
        id: "0a03cf40".to_string(),
    }),
    peer_url: "02abc123...@lightning.example.com:9735".to_string(),
});

client.connect_to_peer(request).await?;
println!("Connected to peer");

Get Connected Peers

Get list of currently connected peers.

Method: GetConnectedPeers

Parameters:

NameTypeRequiredDescription
networkNetworkYESNetwork to query

Response:

FieldTypeDescription
node_idsstring[]Array of connected node IDs

Example Request:

TypeScript
const request = new GetConnectedPeersRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })

const response = await client.getConnectedPeers(request, {})
const peers = response.getNodeIdsList()
console.log('Connected peers:', peers)
Go
req := &pb.GetConnectedPeersRequest{
    Network: &pb.Network{
        Protocol: pb.Protocol_PROTOCOL_BITCOIN,
        Id:       "0a03cf40",
    },
}

resp, err := client.GetConnectedPeers(context.Background(), req)
if err != nil {
    log.Fatal(err)
}

peers := resp.NodeIds
log.Println("Connected peers:", peers)
Rust
let request = tonic::Request::new(GetConnectedPeersRequest {
    network: Some(Network {
        protocol: Protocol::Bitcoin as i32,
        id: "0a03cf40".to_string(),
    }),
});

let response = client.get_connected_peers(request).await?;
let peers = response.into_inner().node_ids;
println!("Connected peers: {:?}", peers);

Disconnect from Peer

Disconnect from a connected peer.

Method: DisconnectFromPeer

Parameters:

NameTypeRequiredDescription
networkNetworkYESTarget network
node_idstringYESPeer public key (hex)

Response: Empty.

Example Request:

import { DisconnectFromPeerRequest } from './proto/node_pb'

const request = new DisconnectFromPeerRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setNodeId('02abc123...')

await client.disconnectFromPeer(request, {})

Is Peer Connected

Check whether a specific peer is currently connected.

Method: IsPeerConnected

Parameters:

NameTypeRequiredDescription
networkNetworkYESTarget network
node_idstringYESPeer public key (hex)

Response:

FieldTypeDescription
is_connectedboolTrue if currently connected

Example Request:

import { IsPeerConnectedRequest } from './proto/node_pb'

const request = new IsPeerConnectedRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setNodeId('02abc123...')

const response = await client.isPeerConnected(request, {})
console.log('Connected:', response.getIsConnected())

Connect to Watchtower

Added 2026-08-02.

Attach this node to a watchtower on a specific network. A watchtower holds the states and revocations this node signs, so that a node which loses its local history can be handed back the coverage it gave away, and so that a channel stays defended while this node is offline.

Method: ConnectToWatchtower

Parameters:

NameTypeRequiredDescription
networkNetworkYESTarget network
watchtower_urlstringYESThe watchtower's address in <node_id>@<host>:<port> form — the same form ConnectToPeer takes, since a watchtower is reached over the same transport

Response: Empty.

The watchtower session is independent of the ordinary peer session. The same remote node may be both a channel peer and a watchtower, and the two connections are tracked separately — connecting one does not connect or disconnect the other.

A watchtower cannot defend a channel whose counterparty is the watchtower. It acts on evidence against a counterparty, so there is nobody for it to act against. On mainnet the Hydranet node serves as both peer and watchtower on the same address — that coverage applies to your channels with other peers. See the Setup Guide.

You usually don't need this call. (2026-09-04) An EVM / Tron network block in config.yaml can declare a watchtowers list; the node dials those itself once its initial chain sync completes and keeps them attached across restarts. Use ConnectToWatchtower to attach one at runtime, and GetConnectedWatchtowers to see what is actually attached. See the Setup Guide.

Example Request:

import { ConnectToWatchtowerRequest } from './proto/node_pb'

const request = new ConnectToWatchtowerRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setWatchtowerUrl('02abc123...@watchtower.example.com:9735')

await client.connectToWatchtower(request, {})

Get Connected Watchtowers

Added 2026-09-04.

List the watchtowers this node currently has a session with — the counterpart to ConnectToWatchtower, which until now you could call but never verify.

Method: GetConnectedWatchtowers

Parameters:

NameTypeRequiredDescription
networkNetworkYESTarget network

Response:

FieldTypeDescription
node_idsstring[]Node ids of the attached watchtowers

Returns node ids, not the full <node_id>@<host>:<port> strings you connected with — match on the id.

Example Request:

import { GetConnectedWatchtowersRequest } from './proto/node_pb'

const request = new GetConnectedWatchtowersRequest()
request.setNetwork({ protocol: 2, id: '42161' })

const res = await client.getConnectedWatchtowers(request, {})
console.log(res.getNodeIdsList())

Empty is a meaningful answer. A node with watchtowers configured attaches only after its initial chain sync completes, so an empty list early in a boot is normal — poll it, or check it once the node reports synced. An empty list on a long-running node means your channels are undefended while it is offline.


Get Zero-Conf Whitelist

Returns the list of peers permitted to use zero-confirmation channels with this node.

Method: GetZeroConfWhitelist

Parameters:

NameTypeRequiredDescription
networkNetworkYESTarget network

Response:

FieldTypeDescription
node_idsstring[]Hex-encoded public keys of whitelisted peers

Example Request:

import { GetZeroConfWhitelistRequest } from './proto/node_pb'

const request = new GetZeroConfWhitelistRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })

const response = await client.getZeroConfWhitelist(request, {})
console.log('Whitelisted:', response.getNodeIdsList())

Add Peer To Zero-Conf Whitelist

Adds a peer to the zero-confirmation whitelist. Whitelisted peers can use channels before the funding transaction confirms on-chain.

Method: AddPeerToZeroConfWhitelist

Parameters:

NameTypeRequiredDescription
networkNetworkYESTarget network
node_idstringYESPeer public key to whitelist

Response: Empty.

Example Request:

import { AddPeerToZeroConfWhitelistRequest } from './proto/node_pb'

const request = new AddPeerToZeroConfWhitelistRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setNodeId('02abc123...')

await client.addPeerToZeroConfWhitelist(request, {})

Remove Peer From Zero-Conf Whitelist

Removes a peer from the zero-confirmation whitelist.

Method: RemovePeerFromZeroConfWhitelist

Parameters:

NameTypeRequiredDescription
networkNetworkYESTarget network
node_idstringYESPeer public key to remove

Response: Empty.

Example Request:

import { RemovePeerFromZeroConfWhitelistRequest } from './proto/node_pb'

const request = new RemovePeerFromZeroConfWhitelistRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setNodeId('02abc123...')

await client.removePeerFromZeroConfWhitelist(request, {})

Get Blacklist

Added 2026-07-20.

Returns the peer connection blacklist for a network — peers this node refuses connections with. The blacklist is also file-configurable; these RPCs control it at runtime.

This is the inverse of the zero-conf whitelist and independent of it: the whitelist grants a privilege to a peer you connect with, the blacklist refuses the connection outright.

Method: GetBlacklist

Parameters:

NameTypeRequiredDescription
networkNetworkYESTarget network

Response:

FieldTypeDescription
node_idsstring[]Hex-encoded public keys of blacklisted peers

Example Request:

import { GetBlacklistRequest } from './proto/node_pb'

const request = new GetBlacklistRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })

const response = await client.getBlacklist(request, {})
console.log('Blacklisted peers:', response.getNodeIdsList())

Add Peer To Blacklist

Added 2026-07-20.

Adds a peer to the connection blacklist. Subsequent connection attempts involving that peer are refused.

Method: AddPeerToBlacklist

Parameters:

NameTypeRequiredDescription
networkNetworkYESTarget network
node_idstringYESThe peer's public key, hex-encoded

Response: Empty.

Example Request:

import { AddPeerToBlacklistRequest } from './proto/node_pb'

const request = new AddPeerToBlacklistRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setNodeId('02abc123...')

await client.addPeerToBlacklist(request, {})

Remove Peer From Blacklist

Added 2026-07-20.

Removes a peer from the connection blacklist, allowing connections with it again.

Method: RemovePeerFromBlacklist

Parameters:

NameTypeRequiredDescription
networkNetworkYESTarget network
node_idstringYESThe peer's public key, hex-encoded

Response: Empty.

Example Request:

import { RemovePeerFromBlacklistRequest } from './proto/node_pb'

const request = new RemovePeerFromBlacklistRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setNodeId('02abc123...')

await client.removePeerFromBlacklist(request, {})

Get Node Policy

Added 2026-05-21.

Read this node's policy configuration for a specific asset on a network. A "node policy" aggregates every parameter another party needs to know before negotiating a channel with this node — reserve rules, routing limits, channel timing rules, etc. Used by liquidity providers and any third party that wants to size on-chain deposits accurately, predict runtime reserves, or know the routing-fee / payment ceilings the channel will be subject to.

The response is composed of sub-messages, one per policy category. New categories are added in a backwards-compatible way, so check for the presence of each sub-message rather than treating the response as a fixed shape.

Method: GetNodePolicy

Parameters:

NameTypeRequiredDescription
networkNetworkYESTarget network
asset_idstringYESAsset whose policy you want to read — required because several policy values are denominated in the asset's smallest unit

Response:

FieldTypeDescription
reserveReservePolicyChannel-reserve parameters. Drives the deposit sizing algebra deposit = (free + fixed) / (1 − rate) on the counterparty side.

ReservePolicy — channel reserve policy for a single (network, asset) pair. All DecimalString amounts are in the asset's display denomination.

FieldTypeDescription
counterparty_proportional_millionthsuint64Reserve ratio (in millionths) the node imposes on its counterparty when opening / accepting a channel. The runtime reserve locked on the counterparty side equals (free + fixed) × counterparty_proportional_millionths / 1_000_000. Range: [0, 1_000_000).
max_self_proportional_millionthsuint64Maximum reserve ratio (in millionths) the node will accept on its own side. Used by callers as an upper-bound clamp on the rate they expect at runtime. Range: [0, 1_000_000).
min_absoluteDecimalStringAbsolute minimum reserve in the asset's display denomination. The runtime reserve is at least this value regardless of the proportional rule.
fee_channel_reserveDecimalStringFixed-fee reserve (display denomination) added on top of the proportional reserve as a gas / dust buffer. Applies once per channel, regardless of how many assets the channel holds.

Example Request:

import { GetNodePolicyRequest } from './proto/node_pb'

const request = new GetNodePolicyRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setAssetId('0x0000000000000000000000000000000000000000000000000000000000000000')

const response = await client.getNodePolicy(request, {})
const reserve = response.getReserve()
if (reserve) {
  console.log('Counterparty reserve millionths:', reserve.getCounterpartyProportionalMillionths())
  console.log('Min absolute:', reserve.getMinAbsolute()?.getValue())
  console.log('Fee channel reserve:', reserve.getFeeChannelReserve()?.getValue())
}

Example Response:

{
  "reserve": {
    "counterparty_proportional_millionths": 10000,
    "max_self_proportional_millionths": 50000,
    "min_absolute": "0.0001",
    "fee_channel_reserve": "0.00005"
  }
}

Estimate Open Channel Fee

Estimate the onchain fee for opening a new channel.

Method: EstimateOpenChannelFee

Parameters:

NameTypeRequiredDescription
networkNetworkYESNetwork to open channel on
node_idstringYESPeer's node ID
asset_amountsmap<string, DepositAmount>YESMap of asset_id → amount to allocate
fee_optionFeeOptionYESFee priority, or a custom rate

DepositAmount — a oneof, exactly one set:

VariantPayloadMeaning
all{}Deposit the entire available balance
exact{ amount: DecimalString }Deposit this amount
dual_funded{ local, remote, self_deposit }Dual-funded open with an explicit split — local / remote are the channel balances assigned to each side, self_deposit is what actually leaves your wallet

FeeOption — also a oneof, exactly one set:

VariantPayloadMeaning
low / medium / high{}Use the network's current fee estimate at that priority
custom{ fee_rate: FeeRate }Your own rate — FeeRate { max_fee_per_unit, priority_fee_per_unit }, both U256String (base units: sat/vB, wei)

⚠️ DecimalString amounts are human-readable; U256String fee rates are not

Every amount here is a DecimalString: "0.1" is 0.1 BTC. Putting "10000000" there does not mean 0.1 BTC — it means ten million BTC. FeeRate is the opposite: it is U256String and is in base units. The two look identical on the wire ({value: "…"}), so check which one a field is before filling it in.

Both types above are oneofs, so the examples nest one level deeper than a flat field would. The examples spell the variant names the way the standard generators emit them — prost names the Rust enum after the oneof itself (deposit_amount::Amount::Exact), protoc-gen-go wraps it (pb.DepositAmount_Exact_). If your generator names them differently, the shape is what matters: an amount is always a variant wrapping { amount: DecimalString }, never a bare value. node.proto and balance.proto are authoritative.

Response:

FieldTypeDescription
feeDecimalStringEstimated onchain fee

Example Request:

TypeScript
const request = new EstimateOpenChannelFeeRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setNodeId('02abc123...')
request.setAssetAmountsMap({
  '0x0000000000000000000000000000000000000000000000000000000000000000': { exact: { amount: { value: '0.1' } } }
})
request.setFeeOption({ medium: {} })   // low | medium | high | custom

const response = await client.estimateOpenChannelFee(request, {})
console.log('Estimated fee:', response.getFee(), 'BTC')
Go
req := &pb.EstimateOpenChannelFeeRequest{
    Network: &pb.Network{
        Protocol: pb.Protocol_PROTOCOL_BITCOIN,
        Id:       "0a03cf40",
    },
    NodeId: "02abc123...",
    AssetAmounts: map[string]*pb.Amount{
        "0x0000000000000000000000000000000000000000000000000000000000000000": &pb.DepositAmount{Amount: &pb.DepositAmount_Exact_{Exact: &pb.DepositAmount_Exact{Amount: &pb.DecimalString{Value: "0.1"}}}},
    },
    FeeOption: &pb.FeeOption{FeeOption: &pb.FeeOption_Medium_{Medium: &pb.FeeOption_Medium{}}},
}

resp, err := client.EstimateOpenChannelFee(context.Background(), req)
if err != nil {
    log.Fatal(err)
}

log.Println("Estimated fee:", resp.Fee, "BTC")
Rust
let mut asset_amounts = HashMap::new();
asset_amounts.insert("0x0000000000000000000000000000000000000000000000000000000000000000".to_string(), DepositAmount {
    amount: Some(deposit_amount::Amount::Exact(deposit_amount::Exact {
        amount: Some(DecimalString { value: "0.1".to_string() }),
    })),
});

let request = tonic::Request::new(EstimateOpenChannelFeeRequest {
    network: Some(Network {
        protocol: Protocol::Bitcoin as i32,
        id: "0a03cf40".to_string(),
    }),
    node_id: "02abc123...".to_string(),
    asset_amounts,
    fee_option: Some(FeeOption {
        fee_option: Some(fee_option::FeeOption::Medium(Default::default())),
    }),
});

let response = client.estimate_open_channel_fee(request).await?;
println!("Estimated fee: {} BTC", response.into_inner().fee);

Open Channel

Open a new Lightning channel with a peer.

Method: OpenChannel

Parameters:

NameTypeRequiredDescription
networkNetworkYESNetwork to open on
node_idstringYESPeer's node ID
asset_amountsmap<string, DepositAmount>YESAssets to allocate
fee_optionFeeOptionYESFee priority, or a custom rate

Response:

FieldTypeDescription
txidstringFunding transaction ID
channel_idstringChannel identifier

Example Request:

TypeScript
const request = new OpenChannelRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setNodeId('02abc123...')
request.setAssetAmountsMap({
  '0x0000000000000000000000000000000000000000000000000000000000000000': { exact: { amount: { value: '0.1' } } }
})
request.setFeeOption({ medium: {} })

const response = await client.openChannel(request, {})
console.log('Channel opened!')
console.log('  TX:', response.getTxid())
console.log('  Channel ID:', response.getChannelId())
Go
req := &pb.OpenChannelRequest{
    Network: &pb.Network{
        Protocol: pb.Protocol_PROTOCOL_BITCOIN,
        Id:       "0a03cf40",
    },
    NodeId: "02abc123...",
    AssetAmounts: map[string]*pb.Amount{
        "0x0000000000000000000000000000000000000000000000000000000000000000": &pb.DepositAmount{Amount: &pb.DepositAmount_Exact_{Exact: &pb.DepositAmount_Exact{Amount: &pb.DecimalString{Value: "0.1"}}}},
    },
    FeeOption: &pb.FeeOption{FeeOption: &pb.FeeOption_Medium_{Medium: &pb.FeeOption_Medium{}}},
}

resp, err := client.OpenChannel(context.Background(), req)
if err != nil {
    log.Fatal(err)
}

log.Println("Channel opened!")
log.Println("  TX:", resp.Txid)
log.Println("  Channel ID:", resp.ChannelId)
Rust
let mut asset_amounts = HashMap::new();
asset_amounts.insert("0x0000000000000000000000000000000000000000000000000000000000000000".to_string(), DepositAmount {
    amount: Some(deposit_amount::Amount::Exact(deposit_amount::Exact {
        amount: Some(DecimalString { value: "0.1".to_string() }),
    })),
});

let request = tonic::Request::new(OpenChannelRequest {
    network: Some(Network {
        protocol: Protocol::Bitcoin as i32,
        id: "0a03cf40".to_string(),
    }),
    node_id: "02abc123...".to_string(),
    asset_amounts,
    fee_option: Some(FeeOption {
        fee_option: Some(fee_option::FeeOption::Medium(Default::default())),
    }),
});

let response = client.open_channel(request).await?;
let inner = response.into_inner();
println!("Channel opened!");
println!("  TX: {}", inner.txid);
println!("  Channel ID: {}", inner.channel_id);

Estimate Deposit Channel Fee

Estimate fee for depositing assets into an existing channel.

Method: EstimateDepositChannelFee

Parameters:

NameTypeRequiredDescription
networkNetworkYESNetwork
channel_idstringYESChannel to deposit to
asset_amountsmap<string, DepositAmount>YESAssets to deposit
fee_optionFeeOptionYESFee priority, or a custom rate

Response:

FieldTypeDescription
feeDecimalStringEstimated fee

Example Request:

TypeScript
const request = new EstimateDepositChannelFeeRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setChannelId('ch_abc123')
request.setAssetAmountsMap({
  '0x0000000000000000000000000000000000000000000000000000000000000000': { exact: { amount: { value: '0.05' } } }
})
request.setFeeOption({ medium: {} })

const response = await client.estimateDepositChannelFee(request, {})
console.log('Deposit fee:', response.getFee())
Go
req := &pb.EstimateDepositChannelFeeRequest{
    Network: &pb.Network{
        Protocol: pb.Protocol_PROTOCOL_BITCOIN,
        Id:       "0a03cf40",
    },
    ChannelId: "ch_abc123",
    AssetAmounts: map[string]*pb.Amount{
        "0x0000000000000000000000000000000000000000000000000000000000000000": &pb.DepositAmount{Amount: &pb.DepositAmount_Exact_{Exact: &pb.DepositAmount_Exact{Amount: &pb.DecimalString{Value: "0.05"}}}},
    },
    FeeOption: &pb.FeeOption{FeeOption: &pb.FeeOption_Medium_{Medium: &pb.FeeOption_Medium{}}},
}

resp, err := client.EstimateDepositChannelFee(context.Background(), req)
if err != nil {
    log.Fatal(err)
}

log.Println("Deposit fee:", resp.Fee)
Rust
let mut asset_amounts = HashMap::new();
asset_amounts.insert("0x0000000000000000000000000000000000000000000000000000000000000000".to_string(), DepositAmount {
    amount: Some(deposit_amount::Amount::Exact(deposit_amount::Exact {
        amount: Some(DecimalString { value: "0.05".to_string() }),
    })),
});

let request = tonic::Request::new(EstimateDepositChannelFeeRequest {
    network: Some(Network {
        protocol: Protocol::Bitcoin as i32,
        id: "0a03cf40".to_string(),
    }),
    channel_id: "ch_abc123".to_string(),
    asset_amounts,
    fee_option: Some(FeeOption {
        fee_option: Some(fee_option::FeeOption::Medium(Default::default())),
    }),
});

let response = client.estimate_deposit_channel_fee(request).await?;
println!("Deposit fee: {}", response.into_inner().fee);

Deposit Channel

Deposit additional assets into an existing channel.

Method: DepositChannel

Parameters:

NameTypeRequiredDescription
networkNetworkYESNetwork
channel_idstringYESChannel to deposit to
asset_amountsmap<string, DepositAmount>YESAssets to deposit
fee_optionFeeOptionYESFee priority, or a custom rate

Response:

FieldTypeDescription
txidstringDeposit transaction ID

Example Request:

TypeScript
const request = new DepositChannelRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setChannelId('ch_abc123')
request.setAssetAmountsMap({
  '0x0000000000000000000000000000000000000000000000000000000000000000': { exact: { amount: { value: '0.05' } } }
})
request.setFeeOption({ medium: {} })

const response = await client.depositChannel(request, {})
console.log('Deposit TX:', response.getTxid())
Go
req := &pb.DepositChannelRequest{
    Network: &pb.Network{
        Protocol: pb.Protocol_PROTOCOL_BITCOIN,
        Id:       "0a03cf40",
    },
    ChannelId: "ch_abc123",
    AssetAmounts: map[string]*pb.Amount{
        "0x0000000000000000000000000000000000000000000000000000000000000000": &pb.DepositAmount{Amount: &pb.DepositAmount_Exact_{Exact: &pb.DepositAmount_Exact{Amount: &pb.DecimalString{Value: "0.05"}}}},
    },
    FeeOption: &pb.FeeOption{FeeOption: &pb.FeeOption_Medium_{Medium: &pb.FeeOption_Medium{}}},
}

resp, err := client.DepositChannel(context.Background(), req)
if err != nil {
    log.Fatal(err)
}

log.Println("Deposit TX:", resp.Txid)
Rust
let mut asset_amounts = HashMap::new();
asset_amounts.insert("0x0000000000000000000000000000000000000000000000000000000000000000".to_string(), DepositAmount {
    amount: Some(deposit_amount::Amount::Exact(deposit_amount::Exact {
        amount: Some(DecimalString { value: "0.05".to_string() }),
    })),
});

let request = tonic::Request::new(DepositChannelRequest {
    network: Some(Network {
        protocol: Protocol::Bitcoin as i32,
        id: "0a03cf40".to_string(),
    }),
    channel_id: "ch_abc123".to_string(),
    asset_amounts,
    fee_option: Some(FeeOption {
        fee_option: Some(fee_option::FeeOption::Medium(Default::default())),
    }),
});

let response = client.deposit_channel(request).await?;
println!("Deposit TX: {}", response.into_inner().txid);

Estimate Withdraw Channel Fee

Estimate fee for withdrawing assets from a channel.

Method: EstimateWithdrawChannelFee

Parameters:

NameTypeRequiredDescription
networkNetworkYESNetwork
channel_idstringYESChannel to withdraw from
asset_amountsmap<string, WithdrawAmount>YESAmounts to withdraw
fee_optionFeeOptionYESFee priority, or a custom rate

WithdrawAmount Object:

FieldTypeDescription
self_withdrawalAmountAmount you withdraw
counterparty_withdrawalAmountAmount the counterparty withdraws

Response:

FieldTypeDescription
feeDecimalStringEstimated fee

Example Request:

TypeScript
const request = new EstimateWithdrawChannelFeeRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setChannelId('ch_abc123')
request.setAssetAmountsMap({
  '0x0000000000000000000000000000000000000000000000000000000000000000': {
    selfWithdrawal: { exact: { amount: { value: '0.03' } } },
    counterpartyWithdrawal: { exact: { amount: { value: '0.02' } } }
  }
})
request.setFeeOption({ medium: {} })

const response = await client.estimateWithdrawChannelFee(request, {})
console.log('Withdrawal fee:', response.getFee())
Go
req := &pb.EstimateWithdrawChannelFeeRequest{
    Network: &pb.Network{
        Protocol: pb.Protocol_PROTOCOL_BITCOIN,
        Id:       "0a03cf40",
    },
    ChannelId: "ch_abc123",
    AssetAmounts: map[string]*pb.WithdrawAmount{
        "0x0000000000000000000000000000000000000000000000000000000000000000": {
            SelfWithdrawal:         &pb.Amount{Amount: &pb.Amount_Exact_{Exact: &pb.Amount_Exact{Amount: &pb.DecimalString{Value: "0.03"}}}},
            CounterpartyWithdrawal: &pb.Amount{Amount: &pb.Amount_Exact_{Exact: &pb.Amount_Exact{Amount: &pb.DecimalString{Value: "0.02"}}}},
        },
    },
    FeeOption: &pb.FeeOption{FeeOption: &pb.FeeOption_Medium_{Medium: &pb.FeeOption_Medium{}}},
}

resp, err := client.EstimateWithdrawChannelFee(context.Background(), req)
if err != nil {
    log.Fatal(err)
}

log.Println("Withdrawal fee:", resp.Fee)
Rust
let mut asset_amounts = HashMap::new();
asset_amounts.insert("0x0000000000000000000000000000000000000000000000000000000000000000".to_string(), WithdrawAmount {
    self_withdrawal: Some(Amount { amount: Some(amount::Amount::Exact(amount::Exact { amount: Some(DecimalString { value: "0.03".to_string() }) })) }),
    counterparty_withdrawal: Some(Amount { amount: Some(amount::Amount::Exact(amount::Exact { amount: Some(DecimalString { value: "0.02".to_string() }) })) }),
});

let request = tonic::Request::new(EstimateWithdrawChannelFeeRequest {
    network: Some(Network {
        protocol: Protocol::Bitcoin as i32,
        id: "0a03cf40".to_string(),
    }),
    channel_id: "ch_abc123".to_string(),
    asset_amounts,
    fee_option: Some(FeeOption {
        fee_option: Some(fee_option::FeeOption::Medium(Default::default())),
    }),
});

let response = client.estimate_withdraw_channel_fee(request).await?;
println!("Withdrawal fee: {}", response.into_inner().fee);

Withdraw Channel

Withdraw assets from an existing channel.

Method: WithdrawChannel

Parameters:

NameTypeRequiredDescription
networkNetworkYESNetwork
channel_idstringYESChannel to withdraw from
asset_amountsmap<string, WithdrawAmount>YESAmounts to withdraw
fee_optionFeeOptionYESFee priority, or a custom rate

Response:

FieldTypeDescription
txidstringWithdrawal transaction ID

Example Request:

TypeScript
const request = new WithdrawChannelRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setChannelId('ch_abc123')
request.setAssetAmountsMap({
  '0x0000000000000000000000000000000000000000000000000000000000000000': {
    selfWithdrawal: { exact: { amount: { value: '0.03' } } },
    counterpartyWithdrawal: { exact: { amount: { value: '0.02' } } }
  }
})
request.setFeeOption({ medium: {} })

const response = await client.withdrawChannel(request, {})
console.log('Withdrawal TX:', response.getTxid())
Go
req := &pb.WithdrawChannelRequest{
    Network: &pb.Network{
        Protocol: pb.Protocol_PROTOCOL_BITCOIN,
        Id:       "0a03cf40",
    },
    ChannelId: "ch_abc123",
    AssetAmounts: map[string]*pb.WithdrawAmount{
        "0x0000000000000000000000000000000000000000000000000000000000000000": {
            SelfWithdrawal:         &pb.Amount{Amount: &pb.Amount_Exact_{Exact: &pb.Amount_Exact{Amount: &pb.DecimalString{Value: "0.03"}}}},
            CounterpartyWithdrawal: &pb.Amount{Amount: &pb.Amount_Exact_{Exact: &pb.Amount_Exact{Amount: &pb.DecimalString{Value: "0.02"}}}},
        },
    },
    FeeOption: &pb.FeeOption{FeeOption: &pb.FeeOption_Medium_{Medium: &pb.FeeOption_Medium{}}},
}

resp, err := client.WithdrawChannel(context.Background(), req)
if err != nil {
    log.Fatal(err)
}

log.Println("Withdrawal TX:", resp.Txid)
Rust
let mut asset_amounts = HashMap::new();
asset_amounts.insert("0x0000000000000000000000000000000000000000000000000000000000000000".to_string(), WithdrawAmount {
    self_withdrawal: Some(Amount { amount: Some(amount::Amount::Exact(amount::Exact { amount: Some(DecimalString { value: "0.03".to_string() }) })) }),
    counterparty_withdrawal: Some(Amount { amount: Some(amount::Amount::Exact(amount::Exact { amount: Some(DecimalString { value: "0.02".to_string() }) })) }),
});

let request = tonic::Request::new(WithdrawChannelRequest {
    network: Some(Network {
        protocol: Protocol::Bitcoin as i32,
        id: "0a03cf40".to_string(),
    }),
    channel_id: "ch_abc123".to_string(),
    asset_amounts,
    fee_option: Some(FeeOption {
        fee_option: Some(fee_option::FeeOption::Medium(Default::default())),
    }),
});

let response = client.withdraw_channel(request).await?;
println!("Withdrawal TX: {}", response.into_inner().txid);

Estimate Update Channel Fee

Estimates the on-chain fee for a generic channel update before executing it.

Method: EstimateUpdateChannelFee

Parameters: same as Update Channelnetwork, channel_id, asset_updates, fee_option.

Response:

FieldTypeDescription
feeDecimalStringEstimated fee in the network's native asset

Update Channel

Added 2026-09-11.

The general form of a channel update. Each side's wallet may deposit or withdraw independently, and part of the resulting balance may be credited across in the state co-signed alongside the transaction.

DepositChannel and WithdrawChannel are unchanged — they are the two shapes of this that are common enough to have their own call. UpdateChannel is how you reach every other combination: a deposit on one side against a withdrawal on the other, a withdrawal that hands part of itself to the peer, a pure credit with no wallet movement at all.

Method: UpdateChannel

Parameters:

NameTypeRequiredDescription
networkNetworkYESNetwork
channel_idstringYESChannel to update
asset_updatesmap<string, ChannelUpdateAmount>YESasset_id → what that asset's update asks for
fee_optionFeeOptionYESFee priority, or a custom rate

Response:

FieldTypeDescription
txidstringThe update transaction ID

ChannelUpdateAmount

Three parts, each independently optional:

FieldTypeDescription
self_contributionChannelContributionWhat your wallet contributes
peer_contributionChannelContributionWhat the counterparty's wallet contributes
creditBalanceCreditWhat crosses between the two sides once both have settled

ChannelContribution — a oneof over the direction that side's wallet moves:

ArmPayloadMeaning
depositAmountOut of the wallet, into the channel
withdrawAmountOut of the channel, into the wallet

BalanceCredit — a oneof over which way balance crosses:

ArmPayloadMeaning
to_peerDecimalStringCredit this much of your balance to the counterparty
to_selfDecimalStringCredit this much of theirs to you

Unset is not zero — it is cheaper

Leaving a contribution or a credit unset means that side's wallet does not move and nothing crosses. That is not the same as naming "0": an explicit zero is an amount the transaction still has to carry, and it costs more gas. Name only the part you care about.

A credit touches no wallet and reaches no chain

credit is settled in the co-signed state that follows the on-chain update, not in the transaction itself. It is how value moves between the two sides without either wallet paying for it — and it is what lets an exit carve its own fee out of what it withdraws.

Your side must have granted the peer room for it: a credit to_peer is bounded by allowed_payment on that peer's funding allowance.

The two contributions must not cancel

The chain refuses an update that leaves the channel holding exactly what it already held. If your deposit on one side equals the peer's withdrawal on the other, you have described a payment, not an update — send it as one.

All resolves differently per direction

Amount.all is relative to whatever pot that side is drawing from:

  • withdraw: { all: {} } — that side's channel balance.
  • deposit: { all: {} } — the local wallet. This makes it invalid on peer_contribution, where the peer's wallet is not visible to your node; it is rejected.
  • withdraw: { all: {} } on a side that also pays a credit — that side's withdrawable balance minus the credit. The on-chain movement settles first and the credit comes out of what is left, so the two never over-draw each other.

Example Request:

import { UpdateChannelRequest } from './proto/node_pb'

const BTC = '0x0000000000000000000000000000000000000000000000000000000000000000'

// Withdraw everything, handing 0.0001 to the peer out of what comes back —
// the shape a service-broadcast exit uses to pay its own fee.
const request = new UpdateChannelRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setChannelId('ch_abc123')
request.setAssetUpdatesMap({
  [BTC]: {
    selfContribution: { withdraw: { all: {} } },
    // peerContribution left unset — their wallet does not move
    credit: { toPeer: { value: '0.0001' } },
  },
})
request.setFeeOption({ medium: {} })

console.log('update TX:', (await client.updateChannel(request, {})).getTxid())

Example Response:

{ "txid": "abc123def456..." }

The same operation is available atomically alongside others — BatchChannelOperation.update carries a BatchUpdateChannel with the same channel_id + asset_updates pair.

Prefer DepositChannel / WithdrawChannel when they say what you mean. They are the same machinery with a narrower request, and a reader of your code can tell at a glance which way funds are going.


Estimate Close Channel Fee

Estimate fee for cooperatively closing a channel.

Method: EstimateCloseChannelFee

Parameters:

NameTypeRequiredDescription
networkNetworkYESNetwork
channel_idstringYESChannel to close
asset_idsstring[]YESAssets to settle
fee_optionFeeOptionYESFee priority, or a custom rate

Response:

FieldTypeDescription
feeDecimalStringEstimated fee

Example Request:

TypeScript
const request = new EstimateCloseChannelFeeRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setChannelId('ch_abc123')
request.setAssetIdsList(['0x0000000000000000000000000000000000000000000000000000000000000000'])
request.setFeeOption({ medium: {} })

const response = await client.estimateCloseChannelFee(request, {})
console.log('Close fee:', response.getFee())
Go
req := &pb.EstimateCloseChannelFeeRequest{
    Network: &pb.Network{
        Protocol: pb.Protocol_PROTOCOL_BITCOIN,
        Id:       "0a03cf40",
    },
    ChannelId: "ch_abc123",
    AssetIds:  []string{"0x0000000000000000000000000000000000000000000000000000000000000000"},
    FeeOption: &pb.FeeOption{FeeOption: &pb.FeeOption_Medium_{Medium: &pb.FeeOption_Medium{}}},
}

resp, err := client.EstimateCloseChannelFee(context.Background(), req)
if err != nil {
    log.Fatal(err)
}

log.Println("Close fee:", resp.Fee)
Rust
let request = tonic::Request::new(EstimateCloseChannelFeeRequest {
    network: Some(Network {
        protocol: Protocol::Bitcoin as i32,
        id: "0a03cf40".to_string(),
    }),
    channel_id: "ch_abc123".to_string(),
    asset_ids: vec!["0x0000000000000000000000000000000000000000000000000000000000000000".to_string()],
    fee_option: Some(FeeOption {
        fee_option: Some(fee_option::FeeOption::Medium(Default::default())),
    }),
});

let response = client.estimate_close_channel_fee(request).await?;
println!("Close fee: {}", response.into_inner().fee);

Close Channel

Cooperatively close a channel with your peer.

Method: CloseChannel

Parameters:

NameTypeRequiredDescription
networkNetworkYESNetwork
channel_idstringYESChannel to close
asset_idsstring[]YESAssets to settle
fee_optionFeeOptionYESFee priority, or a custom rate

Response:

FieldTypeDescription
txidstringClosing transaction ID

Example Request:

TypeScript
const request = new CloseChannelRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setChannelId('ch_abc123')
request.setAssetIdsList(['0x0000000000000000000000000000000000000000000000000000000000000000'])
request.setFeeOption({ medium: {} })

const response = await client.closeChannel(request, {})
console.log('Channel closed, TX:', response.getTxid())
Go
req := &pb.CloseChannelRequest{
    Network: &pb.Network{
        Protocol: pb.Protocol_PROTOCOL_BITCOIN,
        Id:       "0a03cf40",
    },
    ChannelId: "ch_abc123",
    AssetIds:  []string{"0x0000000000000000000000000000000000000000000000000000000000000000"},
    FeeOption: &pb.FeeOption{FeeOption: &pb.FeeOption_Medium_{Medium: &pb.FeeOption_Medium{}}},
}

resp, err := client.CloseChannel(context.Background(), req)
if err != nil {
    log.Fatal(err)
}

log.Println("Channel closed, TX:", resp.Txid)
Rust
let request = tonic::Request::new(CloseChannelRequest {
    network: Some(Network {
        protocol: Protocol::Bitcoin as i32,
        id: "0a03cf40".to_string(),
    }),
    channel_id: "ch_abc123".to_string(),
    asset_ids: vec!["0x0000000000000000000000000000000000000000000000000000000000000000".to_string()],
    fee_option: Some(FeeOption {
        fee_option: Some(fee_option::FeeOption::Medium(Default::default())),
    }),
});

let response = client.close_channel(request).await?;
println!("Channel closed, TX: {}", response.into_inner().txid);

Estimate Force Close Channel Fee

Estimate fee for force-closing an unresponsive channel.

Method: EstimateForceCloseChannelFee

Parameters:

NameTypeRequiredDescription
networkNetworkYESNetwork
channel_idstringYESChannel to force close
asset_idsstring[]YESAssets to settle
fee_optionFeeOptionYESFee priority, or a custom rate

Response:

FieldTypeDescription
feeDecimalStringEstimated fee

Example Request:

TypeScript
const request = new EstimateForceCloseChannelFeeRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setChannelId('ch_abc123')
request.setAssetIdsList(['0x0000000000000000000000000000000000000000000000000000000000000000'])
request.setFeeOption({ medium: {} })

const response = await client.estimateForceCloseChannelFee(request, {})
console.log('Force close fee:', response.getFee())
Go
req := &pb.EstimateForceCloseChannelFeeRequest{
    Network: &pb.Network{
        Protocol: pb.Protocol_PROTOCOL_BITCOIN,
        Id:       "0a03cf40",
    },
    ChannelId: "ch_abc123",
    AssetIds:  []string{"0x0000000000000000000000000000000000000000000000000000000000000000"},
    FeeOption: &pb.FeeOption{FeeOption: &pb.FeeOption_Medium_{Medium: &pb.FeeOption_Medium{}}},
}

resp, err := client.EstimateForceCloseChannelFee(context.Background(), req)
if err != nil {
    log.Fatal(err)
}

log.Println("Force close fee:", resp.Fee)
Rust
let request = tonic::Request::new(EstimateForceCloseChannelFeeRequest {
    network: Some(Network {
        protocol: Protocol::Bitcoin as i32,
        id: "0a03cf40".to_string(),
    }),
    channel_id: "ch_abc123".to_string(),
    asset_ids: vec!["0x0000000000000000000000000000000000000000000000000000000000000000".to_string()],
    fee_option: Some(FeeOption {
        fee_option: Some(fee_option::FeeOption::Medium(Default::default())),
    }),
});

let response = client.estimate_force_close_channel_fee(request).await?;
println!("Force close fee: {}", response.into_inner().fee);

Force Close Channel

Force close a channel without peer cooperation.

Method: ForceCloseChannel

Important Notes:

  • Use only when peer is unresponsive
  • Assets locked until dispute period expires
  • May require additional RedeemClosedChannel transaction
  • Higher fees than cooperative close

Parameters:

NameTypeRequiredDescription
networkNetworkYESNetwork
channel_idstringYESChannel to force close
asset_idsstring[]YESAssets to settle
fee_optionFeeOptionYESFee priority, or a custom rate

Response:

FieldTypeDescription
txidstringForce close transaction ID

Example Request:

TypeScript
const request = new ForceCloseChannelRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setChannelId('ch_abc123')
request.setAssetIdsList(['0x0000000000000000000000000000000000000000000000000000000000000000'])
request.setFeeOption({ medium: {} })

const response = await client.forceCloseChannel(request, {})
console.log('Force close initiated, TX:', response.getTxid())
console.log('Assets will be spendable after dispute period')
Go
req := &pb.ForceCloseChannelRequest{
    Network: &pb.Network{
        Protocol: pb.Protocol_PROTOCOL_BITCOIN,
        Id:       "0a03cf40",
    },
    ChannelId: "ch_abc123",
    AssetIds:  []string{"0x0000000000000000000000000000000000000000000000000000000000000000"},
    FeeOption: &pb.FeeOption{FeeOption: &pb.FeeOption_Medium_{Medium: &pb.FeeOption_Medium{}}},
}

resp, err := client.ForceCloseChannel(context.Background(), req)
if err != nil {
    log.Fatal(err)
}

log.Println("Force close initiated, TX:", resp.Txid)
log.Println("Assets will be spendable after dispute period")
Rust
let request = tonic::Request::new(ForceCloseChannelRequest {
    network: Some(Network {
        protocol: Protocol::Bitcoin as i32,
        id: "0a03cf40".to_string(),
    }),
    channel_id: "ch_abc123".to_string(),
    asset_ids: vec!["0x0000000000000000000000000000000000000000000000000000000000000000".to_string()],
    fee_option: Some(FeeOption {
        fee_option: Some(fee_option::FeeOption::Medium(Default::default())),
    }),
});

let response = client.force_close_channel(request).await?;
println!("Force close initiated, TX: {}", response.into_inner().txid);
println!("Assets will be spendable after dispute period");

Estimate Redeem Closed Channel Fee

Estimate fee for redeeming a force-closed channel.

Method: EstimateRedeemClosedChannelFee

Parameters:

NameTypeRequiredDescription
networkNetworkYESNetwork
channel_idstringYESClosed channel
asset_idsstring[]YESAssets to redeem
fee_optionFeeOptionYESFee priority, or a custom rate

Response:

FieldTypeDescription
feeDecimalStringEstimated fee

Example Request:

TypeScript
const request = new EstimateRedeemClosedChannelFeeRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setChannelId('ch_abc123')
request.setAssetIdsList(['0x0000000000000000000000000000000000000000000000000000000000000000'])
request.setFeeOption({ medium: {} })

const response = await client.estimateRedeemClosedChannelFee(request, {})
console.log('Redemption fee:', response.getFee())
Go
req := &pb.EstimateRedeemClosedChannelFeeRequest{
    Network: &pb.Network{
        Protocol: pb.Protocol_PROTOCOL_BITCOIN,
        Id:       "0a03cf40",
    },
    ChannelId: "ch_abc123",
    AssetIds:  []string{"0x0000000000000000000000000000000000000000000000000000000000000000"},
    FeeOption: &pb.FeeOption{FeeOption: &pb.FeeOption_Medium_{Medium: &pb.FeeOption_Medium{}}},
}

resp, err := client.EstimateRedeemClosedChannelFee(context.Background(), req)
if err != nil {
    log.Fatal(err)
}

log.Println("Redemption fee:", resp.Fee)
Rust
let request = tonic::Request::new(EstimateRedeemClosedChannelFeeRequest {
    network: Some(Network {
        protocol: Protocol::Bitcoin as i32,
        id: "0a03cf40".to_string(),
    }),
    channel_id: "ch_abc123".to_string(),
    asset_ids: vec!["0x0000000000000000000000000000000000000000000000000000000000000000".to_string()],
    fee_option: Some(FeeOption {
        fee_option: Some(fee_option::FeeOption::Medium(Default::default())),
    }),
});

let response = client.estimate_redeem_closed_channel_fee(request).await?;
println!("Redemption fee: {}", response.into_inner().fee);

Redeem Closed Channel

Redeem assets from a force-closed channel after dispute period.

Method: RedeemClosedChannel

Parameters:

NameTypeRequiredDescription
networkNetworkYESNetwork
channel_idstringYESClosed channel
asset_idsstring[]YESAssets to redeem
fee_optionFeeOptionYESFee priority, or a custom rate

Response:

FieldTypeDescription
txidstringRedemption transaction ID

Example Request:

TypeScript
const request = new RedeemClosedChannelRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setChannelId('ch_abc123')
request.setAssetIdsList(['0x0000000000000000000000000000000000000000000000000000000000000000'])
request.setFeeOption({ medium: {} })

const response = await client.redeemClosedChannel(request, {})
console.log('Assets redeemed, TX:', response.getTxid())
Go
req := &pb.RedeemClosedChannelRequest{
    Network: &pb.Network{
        Protocol: pb.Protocol_PROTOCOL_BITCOIN,
        Id:       "0a03cf40",
    },
    ChannelId: "ch_abc123",
    AssetIds:  []string{"0x0000000000000000000000000000000000000000000000000000000000000000"},
    FeeOption: &pb.FeeOption{FeeOption: &pb.FeeOption_Medium_{Medium: &pb.FeeOption_Medium{}}},
}

resp, err := client.RedeemClosedChannel(context.Background(), req)
if err != nil {
    log.Fatal(err)
}

log.Println("Assets redeemed, TX:", resp.Txid)
Rust
let request = tonic::Request::new(RedeemClosedChannelRequest {
    network: Some(Network {
        protocol: Protocol::Bitcoin as i32,
        id: "0a03cf40".to_string(),
    }),
    channel_id: "ch_abc123".to_string(),
    asset_ids: vec!["0x0000000000000000000000000000000000000000000000000000000000000000".to_string()],
    fee_option: Some(FeeOption {
        fee_option: Some(fee_option::FeeOption::Medium(Default::default())),
    }),
});

let response = client.redeem_closed_channel(request).await?;
println!("Assets redeemed, TX: {}", response.into_inner().txid);

Batch Channel Operations

Executes a list of channel operations atomically in a single transaction — all of them land, or none do.

Method: BatchChannelOperations

BatchChannelOperation is a oneof over seven arms:

ArmPayloadEquivalent single call
openBatchOpenChannelOpenChannel
depositBatchDepositChannelDepositChannel
withdrawBatchWithdrawChannelWithdrawChannel
closeBatchCloseChannelCloseChannel
force_closeBatchForceCloseChannelForceCloseChannel
redeemBatchRedeemChannelRedeemClosedChannel
updateBatchUpdateChannel{ channel_id, asset_updates }(2026-09-11) UpdateChannel

Each Batch* payload is its single call's request minus network and fee_option, which the batch supplies once for all of them.

Parameters:

NameTypeRequiredDescription
networkNetworkYESTarget network
operationsBatchChannelOperation[]YESOperations to execute atomically
fee_optionFeeOptionYESFee priority, or a custom rate

Response:

FieldTypeDescription
txidstringSingle transaction ID for the entire batch
opened_channelsBatchOpenedChannel[]Channels created by Open operations (carries the input index of the originating operation)

Example Request:

import { BatchChannelOperationsRequest } from './proto/node_pb'

const request = new BatchChannelOperationsRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setOperationsList([/* BatchChannelOperation entries */])
request.setFeeOption({ medium: {} })

const response = await client.batchChannelOperations(request, {})
console.log('Batch TX:', response.getTxid())

Estimate Batch Channel Operations Fee

Estimates fees for a batch operation before executing it.

Method: EstimateBatchChannelOperationsFee

Parameters: Same as Batch Channel Operationsnetwork, operations, fee_option.

Response:

FieldTypeDescription
feeDecimalStringThe estimated total on-chain fee for the whole batch, in the network's native asset

The estimate returns only the fee — no txid, no opened_channels. Those exist on BatchChannelOperations, which actually broadcasts. An earlier revision of this page repeated that response table here.

Example:

const response = await client.estimateBatchChannelOperationsFee(request, {})
console.log('Batch fee:', response.getFee()?.getValue())

Estimate Simulated Channel Operation Fee

Estimates the fee for a single hypothetical operation against a simulated channel snapshot — useful for UI fee previews where the channel doesn't yet exist.

Method: EstimateSimulatedChannelOperationFee

Parameters:

NameTypeRequiredDescription
networkNetworkYESTarget network
channelSimulatedChannelYESHypothetical channel snapshot
operationSimulatedChannelOperation enumYESWhich operation to price
fee_optionFeeOptionYESFee priority, or a custom rate

Response:

FieldTypeDescription
feeDecimalStringEstimated fee, in the network's native currency

SimulatedChannel

{ assets: SimulatedAssetChannel[] } — one entry per asset the hypothetical channel would hold:

FieldTypeDescription
asset_idstringThe asset
onchain_self_balanceDecimalStringYour currently committed on-chain balance
onchain_counterparty_balanceDecimalStringThe counterparty's currently committed on-chain balance
latest_self_balanceDecimalStringYour latest off-chain balance
latest_counterparty_balanceDecimalStringThe counterparty's latest off-chain balance
num_htlcsuint64Pending HTLCs — they enlarge the commitment transaction, so they move the fee

SimulatedChannelOperation

An enum, not a message:

ValueConstant
0SIMULATED_CHANNEL_OPERATION_UNSPECIFIED — invalid default
1SIMULATED_CHANNEL_OPERATION_OPEN
2SIMULATED_CHANNEL_OPERATION_DEPOSIT
3SIMULATED_CHANNEL_OPERATION_WITHDRAW
4SIMULATED_CHANNEL_OPERATION_COOPERATIVE_CLOSE
5SIMULATED_CHANNEL_OPERATION_FORCE_CLOSE
6SIMULATED_CHANNEL_OPERATION_CONFIRM_SETTLEMENT

SimulatedChannelOp is a different type — the { channel, operation } pair taken by the plural call, which prices several snapshots at once. The singular call takes the channel and the enum as separate fields.

Example:

import { EstimateSimulatedChannelOperationFeeRequest } from './proto/node_pb'

const request = new EstimateSimulatedChannelOperationFeeRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setChannel({ assets: [{
  assetId: '0x0000000000000000000000000000000000000000000000000000000000000000',
  onchainSelfBalance: { value: '0.1' },
  onchainCounterpartyBalance: { value: '0' },
  latestSelfBalance: { value: '0.1' },
  latestCounterpartyBalance: { value: '0' },
  numHtlcs: 0,
}] })
request.setOperation(2)   // SIMULATED_CHANNEL_OPERATION_DEPOSIT
request.setFeeOption({ medium: {} })

const response = await client.estimateSimulatedChannelOperationFee(request, {})
console.log('Estimate:', response.getFee()?.getValue())

Estimate Simulated Channel Operations Fee

Estimates the total fee for a list of hypothetical operations across simulated channels — the multi-operation counterpart to the previous RPC.

Method: EstimateSimulatedChannelOperationsFee

Parameters:

NameTypeRequiredDescription
networkNetworkYESTarget network
operationsSimulatedChannelOp[]YESSimulated operations to estimate
fee_optionFeeOptionYESFee priority, or a custom rate

SimulatedChannelOp pairs a snapshot with the operation to price against it:

FieldTypeDescription
channelSimulatedChannelThe hypothetical channel
operationSimulatedChannelOperation enumWhat to price

Response:

FieldTypeDescription
feeDecimalStringEstimated total fee for all of them, in the network's native asset

This is not the sum of the singular estimates. Operations batched into one transaction share its overhead, which is the reason to ask for them together.

Example:

import { EstimateSimulatedChannelOperationsFeeRequest } from './proto/node_pb'

const request = new EstimateSimulatedChannelOperationsFeeRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setOperationsList([/* SimulatedChannelOp entries */])
request.setFeeOption({ medium: {} })

console.log('Total:', (await client.estimateSimulatedChannelOperationsFee(request, {})).getFee()?.getValue())

Send Channel Payment

Send a direct payment through a specific channel.

Method: SendChannelPayment

Use Cases:

  • Direct peer payments
  • Testing channel functionality
  • Hashlock payments (HTLC)

Parameters:

NameTypeRequiredDescription
networkNetworkYESNetwork
channel_idstringYESChannel to use
asset_amountsmap<string, Amount>YESAssets to send
hashlockHashlockNOOptional hashlock for HTLC
cltv_buffer_secsuint64NORenamed from expiry_timeout_secs (2026-05-10). Optional HTLC effective lifetime in seconds (BOLT-11 tag c semantic). KeySend has no separate invoice-validity knob — this buffer alone bounds the HTLC.

Hashlock — a oneof, not a struct with a hash field. Which arm you set decides who can settle the payment:

ArmPayloadMeaning
known{ preimage: string }You hold the preimage, hex-encoded. The recipient can claim as soon as the HTLC arrives
unknown{ hash: string }You hold only the hash, hex-encoded. The payment parks as pending_preimage until someone supplies the preimage — see ResolveHashlockPayment

There is no payment_hash field on Hashlock. An earlier revision of this page showed one; the proto has always carried the two-arm oneof. Sending { hash } where you meant { preimage } produces a payment nobody can settle.

Response:

FieldTypeDescription
payment_idstringPayment identifier

Example Request:

TypeScript
const request = new SendChannelPaymentRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setChannelId('ch_abc123')
request.setAssetAmountsMap({
  '0x0000000000000000000000000000000000000000000000000000000000000000': { exact: { amount: { value: '0.001' } } }
})
// Optional: set hashlock for HTLC
// request.setHashlock({ paymentHash: 'abc123...' })
request.setCltvBufferSecs(3600) // HTLC lifetime, seconds

const response = await client.sendChannelPayment(request, {})
console.log('Payment sent:', response.getPaymentId())
Go
req := &pb.SendChannelPaymentRequest{
    Network: &pb.Network{
        Protocol: pb.Protocol_PROTOCOL_BITCOIN,
        Id:       "0a03cf40",
    },
    ChannelId: "ch_abc123",
    AssetAmounts: map[string]*pb.Amount{
        "0x0000000000000000000000000000000000000000000000000000000000000000": &pb.Amount{Amount: &pb.Amount_Exact_{Exact: &pb.Amount_Exact{Amount: &pb.DecimalString{Value: "0.001"}}}},
    },
    // Optional: set hashlock for HTLC
    // Hashlock: &pb.Hashlock{PaymentHash: "abc123..."},
    CltvBufferSecs: 3600, // HTLC lifetime, seconds
}

resp, err := client.SendChannelPayment(context.Background(), req)
if err != nil {
    log.Fatal(err)
}

log.Println("Payment sent:", resp.PaymentId)
Rust
let mut asset_amounts = HashMap::new();
asset_amounts.insert("0x0000000000000000000000000000000000000000000000000000000000000000".to_string(), Amount {
    amount: Some(amount::Amount::Exact(amount::Exact {
        amount: Some(DecimalString { value: "0.001".to_string() }),
    })),
});

let request = tonic::Request::new(SendChannelPaymentRequest {
    network: Some(Network {
        protocol: Protocol::Bitcoin as i32,
        id: "0a03cf40".to_string(),
    }),
    channel_id: "ch_abc123".to_string(),
    asset_amounts,
    // Optional: set hashlock for HTLC
    // hashlock: Some(Hashlock {
    //     payment_hash: "abc123...".to_string(),
    // }),
    cltv_buffer_secs: Some(3600), // HTLC lifetime, seconds
});

let response = client.send_channel_payment(request).await?;
println!("Payment sent: {}", response.into_inner().payment_id);

Estimate Send Payment Fee

Estimate fee for a routed Lightning payment.

Method: EstimateSendPaymentFee

Parameters:

NameTypeRequiredDescription
networkNetworkYESNetwork
recipient_node_idstringYESRecipient's node ID
asset_amountsmap<string, Amount>YESAssets to send
hashlockHashlockNOOptional hashlock
cltv_buffer_secsuint64NORenamed from expiry_timeout_secs (2026-05-10). HTLC effective lifetime, seconds (BOLT-11 c).
max_total_cltv_secsuint64NOAdded 2026-05-10. Optional clamp on the route's max-total-CLTV (seconds). For atomic-swap-correct timing pass the invoice's cltv_buffer_secs; for permissive multi-hop routing omit.

Response:

FieldTypeDescription
feesmap<string, DecimalString>Fees per asset

Example Request:

TypeScript
const request = new EstimateSendPaymentFeeRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setRecipientNodeId('03def456...')
request.setAssetAmountsMap({
  '0x0000000000000000000000000000000000000000000000000000000000000000': { exact: { amount: { value: '0.005' } } }
})

const response = await client.estimateSendPaymentFee(request, {})
const fees = response.getFeesMap()
console.log('Routing fee:', fees.get('0x0000000000000000000000000000000000000000000000000000000000000000'), 'BTC')
Go
req := &pb.EstimateSendPaymentFeeRequest{
    Network: &pb.Network{
        Protocol: pb.Protocol_PROTOCOL_BITCOIN,
        Id:       "0a03cf40",
    },
    RecipientNodeId: "03def456...",
    AssetAmounts: map[string]*pb.Amount{
        "0x0000000000000000000000000000000000000000000000000000000000000000": &pb.Amount{Amount: &pb.Amount_Exact_{Exact: &pb.Amount_Exact{Amount: &pb.DecimalString{Value: "0.005"}}}},
    },
}

resp, err := client.EstimateSendPaymentFee(context.Background(), req)
if err != nil {
    log.Fatal(err)
}

fees := resp.Fees
log.Println("Routing fee:", fees["0x0000000000000000000000000000000000000000000000000000000000000000"], "BTC")
Rust
let mut asset_amounts = HashMap::new();
asset_amounts.insert("0x0000000000000000000000000000000000000000000000000000000000000000".to_string(), Amount {
    amount: Some(amount::Amount::Exact(amount::Exact {
        amount: Some(DecimalString { value: "0.005".to_string() }),
    })),
});

let request = tonic::Request::new(EstimateSendPaymentFeeRequest {
    network: Some(Network {
        protocol: Protocol::Bitcoin as i32,
        id: "0a03cf40".to_string(),
    }),
    recipient_node_id: "03def456...".to_string(),
    asset_amounts,
    hashlock: None,
    cltv_buffer_secs: None,
});

let response = client.estimate_send_payment_fee(request).await?;
let fees = response.into_inner().fees;
println!("Routing fee: {} BTC", fees.get("0x0000000000000000000000000000000000000000000000000000000000000000").unwrap_or(&"0".to_string()));

Send Payment

Send a routed Lightning payment through the network.

Method: SendPayment

Parameters:

NameTypeRequiredDescription
networkNetworkYESNetwork
recipient_node_idstringYESRecipient's node ID
asset_amountsmap<string, Amount>YESAssets to send
hashlockHashlockNOOptional hashlock
cltv_buffer_secsuint64NORenamed from expiry_timeout_secs (2026-05-10). HTLC effective lifetime, seconds (BOLT-11 c).
max_total_cltv_secsuint64NOAdded 2026-05-10. Optional clamp on the route's max-total-CLTV (seconds). For atomic-swap-correct timing pass the invoice's cltv_buffer_secs; for permissive multi-hop routing omit.

Response:

FieldTypeDescription
payment_idstringPayment identifier

Example Request:

TypeScript
const request = new SendPaymentRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setRecipientNodeId('03def456...')
request.setAssetAmountsMap({
  '0x0000000000000000000000000000000000000000000000000000000000000000': { exact: { amount: { value: '0.005' } } }
})
request.setCltvBufferSecs(3600)

const response = await client.sendPayment(request, {})
console.log('Payment routed:', response.getPaymentId())
Go
req := &pb.SendPaymentRequest{
    Network: &pb.Network{
        Protocol: pb.Protocol_PROTOCOL_BITCOIN,
        Id:       "0a03cf40",
    },
    RecipientNodeId:   "03def456...",
    AssetAmounts: map[string]*pb.Amount{
        "0x0000000000000000000000000000000000000000000000000000000000000000": &pb.Amount{Amount: &pb.Amount_Exact_{Exact: &pb.Amount_Exact{Amount: &pb.DecimalString{Value: "0.005"}}}},
    },
    CltvBufferSecs: 3600,
}

resp, err := client.SendPayment(context.Background(), req)
if err != nil {
    log.Fatal(err)
}

log.Println("Payment routed:", resp.PaymentId)
Rust
let mut asset_amounts = HashMap::new();
asset_amounts.insert("0x0000000000000000000000000000000000000000000000000000000000000000".to_string(), Amount {
    amount: Some(amount::Amount::Exact(amount::Exact {
        amount: Some(DecimalString { value: "0.005".to_string() }),
    })),
});

let request = tonic::Request::new(SendPaymentRequest {
    network: Some(Network {
        protocol: Protocol::Bitcoin as i32,
        id: "0a03cf40".to_string(),
    }),
    recipient_node_id: "03def456...".to_string(),
    asset_amounts,
    hashlock: None,
    cltv_buffer_secs: Some(3600),
});

let response = client.send_payment(request).await?;
println!("Payment routed: {}", response.into_inner().payment_id);

Create Invoice

Create a Lightning invoice for receiving payment.

Method: CreateInvoice

Parameters:

NameTypeRequiredDescription
networkNetworkYESNetwork
asset_idstringYESAsset to receive
amountDecimalStringNOAmount (omit for zero-amount invoice)
hashlockHashlockNOOptional hashlock
expiry_timeout_secsuint64NOInvoice validity window in seconds (BOLT-11 x). Unchanged — only the payment-send RPCs renamed this field.
cltv_buffer_secsuint64NOAdded 2026-05-10. Extra HTLC lifetime past invoice expiry the receiver requires for safe settlement (BOLT-11 c).

Response:

FieldTypeDescription
invoiceInvoiceInvoice details

Invoice Object:

FieldTypeDescription
networkNetworkThe network this invoice belongs to
payment_requeststringEncoded payment request string — the string you share with the payer
payment_hashstringHex-encoded payment hash
payment_secretstringPayment secret
amountU256String?Invoice amount, in base units (sats / wei) — not a DecimalString. Absent on an empty invoice
asset_idstringAsset identifier
recipientstringRecipient node id
expiry_timestampTimestamp?Invoice validity expiry. Absent when the invoice does not expire
min_final_cltv_expiry_secsuint64Added 2026-05-10. Minimum CLTV buffer (seconds) the receiver requires for the incoming HTLC. The HTLC's effective deadline is expiry_timestamp + min_final_cltv_expiry_secs.
signature / signable_hashbytesInvoice signature and the hash it covers

Invoice.amount is U256String, not DecimalString — one of the few amount fields in the API that really is in base units. See the note under Estimate Open Channel Fee.

Example Request:

TypeScript
const request = new CreateInvoiceRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setAssetId('0x0000000000000000000000000000000000000000000000000000000000000000')
request.setAmount({ value: '0.001' })   // DecimalString — 0.001 BTC, not satoshis
request.setExpiryTimeoutSecs(3600)

const response = await client.createInvoice(request, {})
const invoice = response.getInvoice()
console.log('Payment request:', invoice.getPaymentRequest())
console.log('Share this with the payer')
Go
req := &pb.CreateInvoiceRequest{
    Network: &pb.Network{
        Protocol: pb.Protocol_PROTOCOL_BITCOIN,
        Id:       "0a03cf40",
    },
    AssetId:           "0x0000000000000000000000000000000000000000000000000000000000000000",
    Amount:            "1000000",
    ExpiryTimeoutSecs: 3600,
}

resp, err := client.CreateInvoice(context.Background(), req)
if err != nil {
    log.Fatal(err)
}

invoice := resp.Invoice
log.Println("Payment request:", invoice.PaymentRequest)
log.Println("Share this with the payer")
Rust
let request = tonic::Request::new(CreateInvoiceRequest {
    network: Some(Network {
        protocol: Protocol::Bitcoin as i32,
        id: "0a03cf40".to_string(),
    }),
    asset_id: "0x0000000000000000000000000000000000000000000000000000000000000000".to_string(),
    amount: "1000000".to_string(),
    hashlock: None,
    expiry_timeout_secs: 3600,
});

let response = client.create_invoice(request).await?;
let invoice = response.into_inner().invoice.unwrap();
println!("Payment request: {}", invoice.payment_request);
println!("Share this with the payer");

Example (Zero-amount invoice):

TypeScript
const request = new CreateInvoiceRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setAssetId('0x0000000000000000000000000000000000000000000000000000000000000000')
// No amount set - payer decides

const response = await client.createInvoice(request, {})
const invoice = response.getInvoice()
console.log('Zero-amount invoice:', invoice.getPaymentRequest())
Go
req := &pb.CreateInvoiceRequest{
    Network: &pb.Network{
        Protocol: pb.Protocol_PROTOCOL_BITCOIN,
        Id:       "0a03cf40",
    },
    AssetId: "0x0000000000000000000000000000000000000000000000000000000000000000",
    // No amount set - payer decides
}

resp, err := client.CreateInvoice(context.Background(), req)
if err != nil {
    log.Fatal(err)
}

invoice := resp.Invoice
log.Println("Zero-amount invoice:", invoice.PaymentRequest)
Rust
let request = tonic::Request::new(CreateInvoiceRequest {
    network: Some(Network {
        protocol: Protocol::Bitcoin as i32,
        id: "0a03cf40".to_string(),
    }),
    asset_id: "0x0000000000000000000000000000000000000000000000000000000000000000".to_string(),
    amount: String::new(), // No amount set - payer decides
    hashlock: None,
    expiry_timeout_secs: 0,
});

let response = client.create_invoice(request).await?;
let invoice = response.into_inner().invoice.unwrap();
println!("Zero-amount invoice: {}", invoice.payment_request);

Decode Invoice

Decode a Lightning invoice to view its details.

Method: DecodeInvoice

Parameters:

NameTypeRequiredDescription
networkNetworkYESNetwork
payment_requeststringYESEncoded payment request

Response:

FieldTypeDescription
invoiceInvoiceDecoded invoice details

Example Request:

TypeScript
const request = new DecodeInvoiceRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setPaymentRequest('lnbc10m1...')

const response = await client.decodeInvoice(request, {})
const invoice = response.getInvoice()
console.log('Amount:', invoice.getAmount())
console.log('Asset:', invoice.getAssetId())
console.log('Expires:', invoice.getExpiry())
Go
req := &pb.DecodeInvoiceRequest{
    Network: &pb.Network{
        Protocol: pb.Protocol_PROTOCOL_BITCOIN,
        Id:       "0a03cf40",
    },
    PaymentRequest: "lnbc10m1...",
}

resp, err := client.DecodeInvoice(context.Background(), req)
if err != nil {
    log.Fatal(err)
}

invoice := resp.Invoice
log.Println("Amount:", invoice.Amount)
log.Println("Asset:", invoice.AssetId)
log.Println("Expires:", invoice.Expiry)
Rust
let request = tonic::Request::new(DecodeInvoiceRequest {
    network: Some(Network {
        protocol: Protocol::Bitcoin as i32,
        id: "0a03cf40".to_string(),
    }),
    payment_request: "lnbc10m1...".to_string(),
});

let response = client.decode_invoice(request).await?;
let invoice = response.into_inner().invoice.unwrap();
println!("Amount: {}", invoice.amount);
println!("Asset: {}", invoice.asset_id);
println!("Expires: {:?}", invoice.expiry);

Estimate Pay Invoice Fee

Estimate routing fee for paying an invoice.

Method: EstimatePayInvoiceFee

Parameters:

NameTypeRequiredDescription
networkNetworkYESNetwork
payment_requeststringYESInvoice to pay
max_total_cltv_secsuint64NOAdded 2026-05-10. Optional clamp on the route's max-total-CLTV (seconds). For atomic-swap-correct timing pass a value matching the invoice's cltv_buffer_secs; for permissive multi-hop routing omit.

Response:

FieldTypeDescription
feeDecimalStringEstimated routing fee

Example Request:

TypeScript
const request = new EstimatePayInvoiceFeeRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setPaymentRequest('lnbc10m1...')

const response = await client.estimatePayInvoiceFee(request, {})
console.log('Routing fee:', response.getFee(), 'BTC')
Go
req := &pb.EstimatePayInvoiceFeeRequest{
    Network: &pb.Network{
        Protocol: pb.Protocol_PROTOCOL_BITCOIN,
        Id:       "0a03cf40",
    },
    PaymentRequest: "lnbc10m1...",
}

resp, err := client.EstimatePayInvoiceFee(context.Background(), req)
if err != nil {
    log.Fatal(err)
}

log.Println("Routing fee:", resp.Fee, "BTC")
Rust
let request = tonic::Request::new(EstimatePayInvoiceFeeRequest {
    network: Some(Network {
        protocol: Protocol::Bitcoin as i32,
        id: "0a03cf40".to_string(),
    }),
    payment_request: "lnbc10m1...".to_string(),
});

let response = client.estimate_pay_invoice_fee(request).await?;
println!("Routing fee: {} BTC", response.into_inner().fee);

Pay Invoice

Pay a Lightning invoice.

Method: PayInvoice

Parameters:

NameTypeRequiredDescription
networkNetworkYESNetwork
payment_requeststringYESInvoice to pay
max_total_cltv_secsuint64NOAdded 2026-05-10. Optional clamp on the route's max-total-CLTV (seconds). For atomic-swap-correct timing pass a value matching the invoice's cltv_buffer_secs; for permissive multi-hop routing omit.

Response:

FieldTypeDescription
payment_idstringPayment identifier

Example Request:

TypeScript
const request = new PayInvoiceRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setPaymentRequest('lnbc10m1...')

const response = await client.payInvoice(request, {})
console.log('Payment sent:', response.getPaymentId())
Go
req := &pb.PayInvoiceRequest{
    Network: &pb.Network{
        Protocol: pb.Protocol_PROTOCOL_BITCOIN,
        Id:       "0a03cf40",
    },
    PaymentRequest: "lnbc10m1...",
}

resp, err := client.PayInvoice(context.Background(), req)
if err != nil {
    log.Fatal(err)
}

log.Println("Payment sent:", resp.PaymentId)
Rust
let request = tonic::Request::new(PayInvoiceRequest {
    network: Some(Network {
        protocol: Protocol::Bitcoin as i32,
        id: "0a03cf40".to_string(),
    }),
    payment_request: "lnbc10m1...".to_string(),
});

let response = client.pay_invoice(request).await?;
println!("Payment sent: {}", response.into_inner().payment_id);

Estimate Pay Empty Invoice Fee

Estimate fee for paying a zero-amount invoice with a specific amount.

Method: EstimatePayEmptyInvoiceFee

Parameters:

NameTypeRequiredDescription
networkNetworkYESNetwork
payment_requeststringYESZero-amount invoice
amountAmountYESAmount to pay
max_total_cltv_secsuint64NOAdded 2026-05-10. Optional clamp on the route's max-total-CLTV (seconds). For atomic-swap-correct timing pass a value matching the invoice's cltv_buffer_secs; for permissive multi-hop routing omit.

Response:

FieldTypeDescription
feeDecimalStringEstimated routing fee

Example Request:

TypeScript
const request = new EstimatePayEmptyInvoiceFeeRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setPaymentRequest('lnbc1...')
request.setAmount({ exact: { amount: { value: '0.02' } } })

const response = await client.estimatePayEmptyInvoiceFee(request, {})
console.log('Fee:', response.getFee())
Go
req := &pb.EstimatePayEmptyInvoiceFeeRequest{
    Network: &pb.Network{
        Protocol: pb.Protocol_PROTOCOL_BITCOIN,
        Id:       "0a03cf40",
    },
    PaymentRequest: "lnbc1...",
    Amount:         &pb.Amount{Amount: &pb.Amount_Exact_{Exact: &pb.Amount_Exact{Amount: &pb.DecimalString{Value: "0.02"}}}},
}

resp, err := client.EstimatePayEmptyInvoiceFee(context.Background(), req)
if err != nil {
    log.Fatal(err)
}

log.Println("Fee:", resp.Fee)
Rust
let request = tonic::Request::new(EstimatePayEmptyInvoiceFeeRequest {
    network: Some(Network {
        protocol: Protocol::Bitcoin as i32,
        id: "0a03cf40".to_string(),
    }),
    payment_request: "lnbc1...".to_string(),
    amount: Some(Amount { amount: Some(amount::Amount::Exact(amount::Exact { amount: Some(DecimalString { value: "0.02".to_string() }) })) }),
});

let response = client.estimate_pay_empty_invoice_fee(request).await?;
println!("Fee: {}", response.into_inner().fee);

Pay Empty Invoice

Pay a zero-amount invoice with a specific amount.

Method: PayEmptyInvoice

Parameters:

NameTypeRequiredDescription
networkNetworkYESNetwork
payment_requeststringYESZero-amount invoice
amountAmountYESAmount to pay
max_total_cltv_secsuint64NOAdded 2026-05-10. Optional clamp on the route's max-total-CLTV (seconds). For atomic-swap-correct timing pass a value matching the invoice's cltv_buffer_secs; for permissive multi-hop routing omit.

Response:

FieldTypeDescription
payment_idstringPayment identifier

Example Request:

TypeScript
const request = new PayEmptyInvoiceRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setPaymentRequest('lnbc1...')
request.setAmount({ exact: { amount: { value: '0.02' } } })

const response = await client.payEmptyInvoice(request, {})
console.log('Payment sent:', response.getPaymentId())
Go
req := &pb.PayEmptyInvoiceRequest{
    Network: &pb.Network{
        Protocol: pb.Protocol_PROTOCOL_BITCOIN,
        Id:       "0a03cf40",
    },
    PaymentRequest: "lnbc1...",
    Amount:         &pb.Amount{Amount: &pb.Amount_Exact_{Exact: &pb.Amount_Exact{Amount: &pb.DecimalString{Value: "0.02"}}}},
}

resp, err := client.PayEmptyInvoice(context.Background(), req)
if err != nil {
    log.Fatal(err)
}

log.Println("Payment sent:", resp.PaymentId)
Rust
let request = tonic::Request::new(PayEmptyInvoiceRequest {
    network: Some(Network {
        protocol: Protocol::Bitcoin as i32,
        id: "0a03cf40".to_string(),
    }),
    payment_request: "lnbc1...".to_string(),
    amount: Some(Amount { amount: Some(amount::Amount::Exact(amount::Exact { amount: Some(DecimalString { value: "0.02".to_string() }) })) }),
});

let response = client.pay_empty_invoice(request).await?;
println!("Payment sent: {}", response.into_inner().payment_id);

Resolve Hashlock Payment

Claim a hashlock payment by revealing the preimage.

Method: ResolveHashlockPayment

Parameters:

NameTypeRequiredDescription
networkNetworkYESNetwork
payment_idstringYESPayment to resolve
payment_preimagestringYESPreimage (32 bytes hex)

Response: Empty (success confirmation)

Example Request:

TypeScript
const request = new ResolveHashlockPaymentRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setPaymentId('payment_abc123')
request.setPaymentPreimage('0123456789abcdef...')

await client.resolveHashlockPayment(request, {})
console.log('Payment claimed')
Go
req := &pb.ResolveHashlockPaymentRequest{
    Network: &pb.Network{
        Protocol: pb.Protocol_PROTOCOL_BITCOIN,
        Id:       "0a03cf40",
    },
    PaymentId:       "payment_abc123",
    PaymentPreimage: "0123456789abcdef...",
}

_, err := client.ResolveHashlockPayment(context.Background(), req)
if err != nil {
    log.Fatal(err)
}

log.Println("Payment claimed")
Rust
let request = tonic::Request::new(ResolveHashlockPaymentRequest {
    network: Some(Network {
        protocol: Protocol::Bitcoin as i32,
        id: "0a03cf40".to_string(),
    }),
    payment_id: "payment_abc123".to_string(),
    payment_preimage: "0123456789abcdef...".to_string(),
});

client.resolve_hashlock_payment(request).await?;
println!("Payment claimed");

Reject Payment

Reject an incoming payment.

Method: RejectPayment

Parameters:

NameTypeRequiredDescription
networkNetworkYESNetwork
payment_idstringYESPayment to reject

Response: Empty (success confirmation)

Example Request:

TypeScript
const request = new RejectPaymentRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setPaymentId('payment_abc123')

await client.rejectPayment(request, {})
console.log('Payment rejected')
Go
req := &pb.RejectPaymentRequest{
    Network: &pb.Network{
        Protocol: pb.Protocol_PROTOCOL_BITCOIN,
        Id:       "0a03cf40",
    },
    PaymentId: "payment_abc123",
}

_, err := client.RejectPayment(context.Background(), req)
if err != nil {
    log.Fatal(err)
}

log.Println("Payment rejected")
Rust
let request = tonic::Request::new(RejectPaymentRequest {
    network: Some(Network {
        protocol: Protocol::Bitcoin as i32,
        id: "0a03cf40".to_string(),
    }),
    payment_id: "payment_abc123".to_string(),
});

client.reject_payment(request).await?;
println!("Payment rejected");

Register Preimage

Moved (2026-06-11). RegisterPreimage was removed from NodeService. Registering a revealed preimage now lives on the new PreimageService.SettlePreimage (JSON-RPC namespace preimage), which does everything RegisterPreimage did — persist the preimage, claim held channel hashlock payments, arm the force-close — and additionally claims matching on-chain HTLCs on HTLC-capable networks. The request fields are identical (network, hex payment_preimage).

See the HTLC & Preimage API → Settle Preimage.


Set Funding Allowance

Sets the per-asset funding allowance for a peer — how much they may fund, and how much of your balance they may be credited. Replaces any existing allowance.

Funding allowances let you accept incoming channel deposits, hashlock-funded payments, and service-broadcast operations from a known peer without approving each one by hand.

Method: SetFundingAllowance

Parameters:

NameTypeRequiredDescription
networkNetworkYESTarget network
node_idstringYESPeer public key
asset_allowancesmap<string, FundingAllowance>YESPer-asset allowances

FundingAllowance:

FieldTypeDescription
allowed_depositDecimalStringMaximum deposit the peer may initiate
allowed_paymentDecimalStringMaximum balance the peer may be credited out of this side, whatever moved it

allowed_payment caps more than payments

The name is narrower than the rule. It bounds every way your balance can cross to that peer without a payment of your own — a claim out of a deposit, and equally the BalanceCredit in a co-signed channel update, which is how a service-broadcast operation takes its fee.

So read it as the ceiling on what that peer can end up holding at your expense, and size it against the largest such operation you intend to allow — not just against deposits.

Response: Empty.

Example Request:

import { SetFundingAllowanceRequest } from './proto/node_pb'

const request = new SetFundingAllowanceRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setNodeId('02abc123...')
request.getAssetAllowancesMap().set('0x0000000000000000000000000000000000000000000000000000000000000000', {
  allowedDeposit: { value: '1.0' },
  allowedPayment: { value: '0.5' }
})

await client.setFundingAllowance(request, {})

Get Funding Allowance

Returns the current remaining funding allowances for a peer.

Method: GetFundingAllowance

Parameters:

NameTypeRequiredDescription
networkNetworkYESTarget network
node_idstringYESPeer public key

Response:

FieldTypeDescription
asset_allowancesmap<string, FundingAllowance>Remaining allowance per asset

Example Request:

import { GetFundingAllowanceRequest } from './proto/node_pb'

const request = new GetFundingAllowanceRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setNodeId('02abc123...')

const response = await client.getFundingAllowance(request, {})
const map = response.getAssetAllowancesMap()
map.forEach((allowance, assetId) => {
  console.log(assetId, allowance.toObject())
})

Increase Funding Allowance

Increases an existing funding allowance for a peer by a delta. Each entry in asset_allowances is added to the current allowance for that asset.

Method: IncreaseFundingAllowance

Parameters:

NameTypeRequiredDescription
networkNetworkYESTarget network
node_idstringYESPeer public key
asset_allowancesmap<string, FundingAllowance>YESDeltas to add

Response: Empty.

Example Request:

import { IncreaseFundingAllowanceRequest } from './proto/node_pb'

const request = new IncreaseFundingAllowanceRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setNodeId('02abc123...')
request.getAssetAllowancesMap().set('0x0000000000000000000000000000000000000000000000000000000000000000', {
  allowedDeposit: { value: '0.5' },
  allowedPayment: { value: '0.25' }
})

await client.increaseFundingAllowance(request, {})

Decrease Funding Allowance

Decreases an existing funding allowance for a peer by a delta. Subtracts each entry from the current allowance.

Method: DecreaseFundingAllowance

Parameters:

NameTypeRequiredDescription
networkNetworkYESTarget network
node_idstringYESPeer public key
asset_allowancesmap<string, FundingAllowance>YESDeltas to subtract

Response: Empty.

Example Request:

import { DecreaseFundingAllowanceRequest } from './proto/node_pb'

const request = new DecreaseFundingAllowanceRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setNodeId('02abc123...')
request.getAssetAllowancesMap().set('0x0000000000000000000000000000000000000000000000000000000000000000', {
  allowedDeposit: { value: '0.1' },
  allowedPayment: { value: '0.05' }
})

await client.decreaseFundingAllowance(request, {})

Revoke Funding Allowance

Revokes all funding allowances for a peer. Equivalent to setting every asset's allowance to zero.

Method: RevokeFundingAllowance

Parameters:

NameTypeRequiredDescription
networkNetworkYESTarget network
node_idstringYESPeer public key

Response: Empty.

Example Request:

import { RevokeFundingAllowanceRequest } from './proto/node_pb'

const request = new RevokeFundingAllowanceRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setNodeId('02abc123...')

await client.revokeFundingAllowance(request, {})

Common Workflows

Open channel and wait for activation

There is no WaitForActiveAssetChannel RPC — that pattern was replaced by event subscriptions. Subscribe to event.SubscribeNodeEvents before calling OpenChannel, then watch for the channel-active update for your new channel_id.

import {
  OpenChannelRequest,
  EstimateOpenChannelFeeRequest,
} from './proto/node_pb'
import { SubscribeNodeEventsRequest } from './proto/event_pb'

async function openAndWaitForChannel(
  node: NodeServiceClient,
  events: EventServiceClient,
  network: { protocol: number; id: string },
  peerNodeId: string,
  assetId: string,
  amountDecimal: string,
) {
  // 1. Subscribe BEFORE opening so we don't miss the activation event.
  const sub = new SubscribeNodeEventsRequest()
  sub.setNetwork(network)
  const stream = events.subscribeNodeEvents(sub, {})

  // 2. Open the channel.
  const open = new OpenChannelRequest()
  open.setNetwork(network)
  open.setNodeId(peerNodeId)
  open.getAssetAmountsMap().set(assetId, {
    exact: { amount: { value: amountDecimal } }   // DepositAmount oneof
  })
  open.setFeeOption({ medium: {} })

  const opened = await node.openChannel(open, {})
  const channelId = opened.getChannelId()
  console.log('OpenChannel broadcast, channel:', channelId, 'tx:', opened.getTxid())

  // 3. Wait for the channel to go active for this asset.
  await new Promise<void>((resolve, reject) => {
    stream.on('data', (evt: any) => {
      const upd = evt.getChannelUpdate?.()
      if (!upd) return
      if (upd.getChannelId?.() !== channelId) return
      // Asset-channel status `active` means it's ready to send & receive.
      const assetChannel = upd.getAssetChannelsMap?.().get(assetId)
      if (assetChannel?.getStatus?.()?.getActive?.()) {
        stream.cancel()
        resolve()
      }
    })
    stream.on('error', reject)
  })
  console.log('Channel is active for', assetId)
  return channelId
}

The exact field paths above (channel_update, asset_channels, Active) match the current event.proto and channel.proto. If you regenerate code from a newer proto and the names change, follow the proto rather than this snippet.

Create and pay invoice

TypeScript
async function createAndPayInvoice(
  client: NodeServiceClient,
  network: Network,
  assetId: string,
  amount: string
) {
  // Create invoice
  const createReq = new CreateInvoiceRequest()
  createReq.setNetwork(network)
  createReq.setAssetId(assetId)
  createReq.setAmount(amount)
  createReq.setExpiryTimeoutSecs(3600)

  const createResp = await client.createInvoice(createReq, {})
  const invoice = createResp.getInvoice()
  const paymentRequest = invoice.getPaymentRequest()

  console.log('Invoice created:', paymentRequest)

  // Decode to verify
  const decodeReq = new DecodeInvoiceRequest()
  decodeReq.setNetwork(network)
  decodeReq.setPaymentRequest(paymentRequest)

  const decoded = await client.decodeInvoice(decodeReq, {})
  console.log('Amount:', decoded.getInvoice()?.getAmount())

  // Pay invoice
  const payReq = new PayInvoiceRequest()
  payReq.setNetwork(network)
  payReq.setPaymentRequest(paymentRequest)

  const payResp = await client.payInvoice(payReq, {})
  console.log('Payment sent:', payResp.getPaymentId())
}
Go
func createAndPayInvoice(
    client pb.NodeServiceClient,
    network *pb.Network,
    assetId string,
    amount string,
) error {
    // Create invoice
    createReq := &pb.CreateInvoiceRequest{
        Network:           network,
        AssetId:           assetId,
        Amount:            amount,
        ExpiryTimeoutSecs: 3600,
    }

    createResp, err := client.CreateInvoice(context.Background(), createReq)
    if err != nil {
        return err
    }

    invoice := createResp.Invoice
    paymentRequest := invoice.PaymentRequest
    log.Println("Invoice created:", paymentRequest)

    // Decode to verify
    decodeReq := &pb.DecodeInvoiceRequest{
        Network:        network,
        PaymentRequest: paymentRequest,
    }

    decoded, err := client.DecodeInvoice(context.Background(), decodeReq)
    if err != nil {
        return err
    }

    log.Println("Amount:", decoded.Invoice.Amount)

    // Pay invoice
    payReq := &pb.PayInvoiceRequest{
        Network:        network,
        PaymentRequest: paymentRequest,
    }

    payResp, err := client.PayInvoice(context.Background(), payReq)
    if err != nil {
        return err
    }

    log.Println("Payment sent:", payResp.PaymentId)
    return nil
}
Rust
async fn create_and_pay_invoice(
    client: &mut NodeServiceClient<Channel>,
    network: Network,
    asset_id: String,
    amount: String,
) -> Result<(), Box<dyn std::error::Error>> {
    // Create invoice
    let create_req = tonic::Request::new(CreateInvoiceRequest {
        network: Some(network.clone()),
        asset_id,
        amount,
        hashlock: None,
        expiry_timeout_secs: 3600,
    });

    let create_resp = client.create_invoice(create_req).await?;
    let invoice = create_resp.into_inner().invoice.unwrap();
    let payment_request = invoice.payment_request.clone();

    println!("Invoice created: {}", payment_request);

    // Decode to verify
    let decode_req = tonic::Request::new(DecodeInvoiceRequest {
        network: Some(network.clone()),
        payment_request: payment_request.clone(),
    });

    let decoded = client.decode_invoice(decode_req).await?;
    println!("Amount: {}", decoded.into_inner().invoice.unwrap().amount);

    // Pay invoice
    let pay_req = tonic::Request::new(PayInvoiceRequest {
        network: Some(network),
        payment_request,
    });

    let pay_resp = client.pay_invoice(pay_req).await?;
    println!("Payment sent: {}", pay_resp.into_inner().payment_id);

    Ok(())
}

Safe channel closure

TypeScript
async function safeCloseChannel(
  client: NodeServiceClient,
  network: Network,
  channelId: string,
  assetIds: string[]
) {
  // Try cooperative close first
  try {
    const closeReq = new CloseChannelRequest()
    closeReq.setNetwork(network)
    closeReq.setChannelId(channelId)
    closeReq.setAssetIdsList(assetIds)
    closeReq.setFeeOption({ medium: {} })

    const response = await client.closeChannel(closeReq, {})
    console.log('Channel closed cooperatively:', response.getTxid())
    return
  } catch (error) {
    console.log('Cooperative close failed, trying force close...')
  }

  // Force close if cooperative fails
  const forceReq = new ForceCloseChannelRequest()
  forceReq.setNetwork(network)
  forceReq.setChannelId(channelId)
  forceReq.setAssetIdsList(assetIds)
  forceReq.setFeeOption({ high: {} })

  const forceResp = await client.forceCloseChannel(forceReq, {})
  console.log('Force close initiated:', forceResp.getTxid())
  console.log('Wait for dispute period before redeeming')
}
Go
func safeCloseChannel(
    client pb.NodeServiceClient,
    network *pb.Network,
    channelId string,
    assetIds []string,
) error {
    // Try cooperative close first
    closeReq := &pb.CloseChannelRequest{
        Network:   network,
        ChannelId: channelId,
        AssetIds:  assetIds,
        FeeOption: &pb.FeeOption{FeeOption: &pb.FeeOption_Medium_{Medium: &pb.FeeOption_Medium{}}},
    }

    closeResp, err := client.CloseChannel(context.Background(), closeReq)
    if err == nil {
        log.Println("Channel closed cooperatively:", closeResp.Txid)
        return nil
    }

    log.Println("Cooperative close failed, trying force close...")

    // Force close if cooperative fails
    forceReq := &pb.ForceCloseChannelRequest{
        Network:   network,
        ChannelId: channelId,
        AssetIds:  assetIds,
        FeeOption: &pb.FeeOption{FeeOption: &pb.FeeOption_Medium_{Medium: &pb.FeeOption_Medium{}}},
    }

    forceResp, err := client.ForceCloseChannel(context.Background(), forceReq)
    if err != nil {
        return err
    }

    log.Println("Force close initiated:", forceResp.Txid)
    log.Println("Wait for dispute period before redeeming")
    return nil
}
Rust
async fn safe_close_channel(
    client: &mut NodeServiceClient<Channel>,
    network: Network,
    channel_id: String,
    asset_ids: Vec<String>,
) -> Result<(), Box<dyn std::error::Error>> {
    // Try cooperative close first
    let close_req = tonic::Request::new(CloseChannelRequest {
        network: Some(network.clone()),
        channel_id: channel_id.clone(),
        asset_ids: asset_ids.clone(),
        fee_option: Some(FeeOption {
        fee_option: Some(fee_option::FeeOption::Medium(Default::default())),
    }),
    });

    match client.close_channel(close_req).await {
        Ok(response) => {
            println!("Channel closed cooperatively: {}", response.into_inner().txid);
            return Ok(());
        }
        Err(_) => {
            println!("Cooperative close failed, trying force close...");
        }
    }

    // Force close if cooperative fails
    let force_req = tonic::Request::new(ForceCloseChannelRequest {
        network: Some(network),
        channel_id,
        asset_ids,
        fee_option: Some(FeeOption {
        fee_option: Some(fee_option::FeeOption::Medium(Default::default())),
    }),
    });

    let force_resp = client.force_close_channel(force_req).await?;
    println!("Force close initiated: {}", force_resp.into_inner().txid);
    println!("Wait for dispute period before redeeming");

    Ok(())
}

Best Practices

  1. Subscribe to event.SubscribeNodeEvents before opening or paying — channel-active and payment-completed signals come through the stream, not as a return value of the opening RPC.
  2. Estimate fees first — every channel-mutating RPC has a paired Estimate* variant that takes the same request and returns just the fee.
  3. Try CloseChannel before ForceCloseChannel — cooperative close is cheaper and faster.
  4. Pick reasonable invoice expirations — 1–24 hours is the common range; very short expirations stress retry logic.
  5. Check capacity before sending — call EstimateSendPaymentFee or watchOnlyNode.GetChannel to confirm the asset side has the balance you need.
  6. Keep hashlock preimages secret until you're ready to release the funds — see preimage.SettlePreimage (which replaced RegisterPreimage) and ResolveHashlockPayment.
  7. Reuse channels via DepositChannel rather than opening a new one for the same peer / asset.

Channel Lifecycle

1. ConnectToPeer
   ↓
2. (optionally) AddPeerToZeroConfWhitelist
   ↓
3. SubscribeNodeEvents     ← keep this stream open for steps 4–9
   ↓
4. OpenChannel
   ↓
5. wait for ChannelUpdate { asset_channels[…].status = Active }
   ↓
6. SendPayment / CreateInvoice + PayInvoice / SendChannelPayment
   ↓
7. DepositChannel       (top up either side)
   ↓
8. WithdrawChannel      (pull funds out without closing)
   ↓
9. CloseChannel  →  RedeemClosedChannel  (if force closed)

Service-backed alternative for steps 4–5 and 7–9: use the Lease API (liquidity.RequestChannelLiquidity / RequestChannelRelease) instead of OpenChannel/DepositChannel/WithdrawChannel/CloseChannel. The lease flow handles the same lifecycle but the liquidity service signs and broadcasts the underlying transactions for you.


← Back to API Reference | Next: Lease API →


Copyright © 2025