Api

Lease API

Service-backed channel liquidity, releases, and lease extensions

The Lease API exposes Hydra App's LiquidityService — a service-backed surface for provisioning channel liquidity, releasing channels (withdraw or cooperative close), and extending existing asset leases: what the service offers, the leases it holds for your node, and the requests your node makes of it.

(2026-10-14) A request is submitted, then followed. A Submit* call answers once your node has settled its part of the fee — or failed to — with the request as it then stands; Get Liquidity Request and Subscribe Liquidity Requests follow it until it is final. Name each request with a client_request_id and a call that was cut off can be read back or safely sent again. See Submitting and following a request.

JSON-RPC namespace: liquidity

Endpoints

Read-only discovery


Common types

ChannelLiquidityRequestOperation (one of)

Used by SubmitChannelLiquidityRequest, the deprecated RequestChannelLiquidity, and their estimator. Exactly one variant must be set.

VariantFieldsPurpose
opentarget_node_pubkey?, asset_liquidity, min_dispute_period_secsOpen a new channel and provision liquidity
depositchannel_id, asset_liquidityDeposit into an existing channel
deposit_anytarget_node_pubkey?, asset_liquidity, min_dispute_period_secsDeposit into any suitable channel; otherwise open a new one
open_or_deposittarget_node_pubkey?, asset_liquidity, min_dispute_period_secsSame as deposit_any but biases toward opening

asset_liquidity is map<string, AssetLiquidity> keyed by asset_id.

min_dispute_period_secs (2026-09-25) is the least dispute period the funded channel must carry — see dispute_period on Get Node Policy. An existing channel that falls short on any requested asset is not topped up, and a channel opened to serve the request is opened to carry it. Zero requires nothing; a period past the longest the hub's node accepts on a channel is refused at the quote (INVALID_ARGUMENT). A deposit names its channel, window included, so it carries no requirement.

On an EVM or Tron network your node raises min_dispute_period_secs of open, deposit_any and open_or_deposit to its own proposal (dispute_period.proposed on Get Node Policy) before sending the request, so the provider's liquidity only lands in a channel held to the terms your node opens on today. An older, shorter channel is passed over and a new one opened.

A channel a cooperative close drained is still topped up: on EVM and Tron it only withdrew everything, its asset channels read Closed with force_closed: false yet stay updatable, and a deposit reopens them in place. A channel with nothing left open — every asset channel sealed by a settlement, or closed for good — is never topped up, even for an asset it does not carry yet.

AssetLiquidity:

FieldTypeDescription
server_amountDecimalStringProvider-side contribution
client_amountDecimalStringLocal/client-side contribution (used for service-broadcasted outbound funding or dual-funded requests)

ChannelReleaseOperation (one of)

Used by SubmitChannelReleaseRequest, the deprecated RequestChannelRelease, and their estimator. Exactly one variant must be set.

VariantFieldsPurpose
withdrawchannel_id, asset_amounts{}Cooperatively withdraw funds from the channel. The service broadcasts one withdrawal paying each asset's requested side(s) out, and pays the gas.
cooperative_closechannel_id, asset_ids[]Cooperatively close asset channels. Empty asset_ids closes every asset channel; otherwise only the listed ones.

withdraw.asset_amounts — per-asset, per-side split

Changed 2026-07-23 — breaking: this replaced the old asset_ids[] list.

asset_amounts is a map of asset_id → AssetWithdrawAmounts:

FieldTypeDescription
server_amountAmount (optional)Releases the service's own balance to the service wallet
client_amountAmount (optional)Pays the client's balance out to the client's wallet

An absent side withdraws nothing; at least one side must be present. channel_id may be absent on fee estimates only (EstimateRequestChannelReleaseFee) — a swap quotes its exit before the channel exists — and every requested side must then be an exact amount.

⚠️ Absent means absent, not ""

(2026-09-13) channel_id is optional, and an empty string is not the sentinel. The service checks that you own the channel you named, and no empty string satisfies that check — so a quote built with "" comes back priced for somebody else's channel, and the operation silently falls to a rail you did not ask for.

Omit the field. optional is the same bytes on the wire as an unset string, so a client either side of this change reads the other correctly; what changed is that the generated types now carry the case, so it is harder to forget.

Migrating from asset_ids[]: a list entry "0xTOKEN" becomes a map entry "0xTOKEN": { client_amount: { exact: { amount: "..." } } } — you must now say how much and whose balance moves, not just which asset.

A channel the hub opened on demand stays open until keep_open_until

(2026-10-14) The hub sells a channel it opens on demand with a lifetime, and until the lifetime ends it neither takes anything out of one of its asset channels nor closes it. Your node shows that lifetime as AssetChannel.peer_keep_open_until, the hub's node as its own keep_open_until. Counted in blocks, it can only be estimated in time: your node reports it a margin before its last block is expected, so you never count on more than you bought, and the hub holds itself to a margin after, so it never closes early — the hub's time is the later. A withdraw or cooperative_close of such an asset channel before the hub's time is refused with FAILED_PRECONDITION — by the estimate and by the request alike, before any fee is paid — naming the asset, the channel and the time. Close the channel from your own node instead, whenever you choose, or wait until the time the refusal names has passed.

(2026-10-14) The lifetime is a lease. Once the payment the channel was opened for has settled in it, the asset channel carries a lease until the hub's keep_open_until, which its opening fee paid for: AssetChannel.lease_expiry shows it, on a boundary of the provider's lease clock, and moves later with the hub's time. The lease rents what the hub lent into the asset channel — its side of it once that payment had settled, the capital that gives you inbound liquidity. Keep the channel past the lifetime by extending the lease, or with a deposit that carries a lease_duration, as with any lease: the extension rents that capital for the time it adds past lease_expiry, never for time the lifetime already covers.

Fee payment (one of)

All RPCs that take a fee require exactly one fee_payment variant.

(2026-10-14) Which variants a provider accepts, and in which assets, is published in GetLiquidityServiceInfo's fee_payment_assets, with what each one charges. A fee in an asset or on a rail missing there is refused with FAILED_PRECONDITION before it is priced.

VariantFieldsWhen to use
onchain_fee_paymentOnchainFeePayment (UTXO or Account flow)Pay the service fee on chain
offchain_fee_paymentOffchainFeePayment (empty)Pay via invoice / offchain payment
dual_fund_fee_paymentDualFundFeePayment (empty)Settle the fee inside the dual-fund flow (provisioning RPCs only)
sponsored_deposit_fee_paymentSponsoredDepositFeePayment (empty)(2026-09-11) The wallet holds the token but no gas. The service funds the channel from your tokens on its own transaction and takes its fee out of the deposit (provisioning RPCs only)
sponsored_withdrawal_fee_paymentSponsoredWithdrawalFeePayment (empty)(2026-09-11) The wallet holds a channel balance but no gas. The service broadcasts the withdrawal and takes its fee out of what is released (release RPCs only)

OnchainFeePayment (one of):

VariantFields
utxorefund_address
accountsender_address, refund_address?

Fee methods are not interchangeable across the three operations:

OperationAccepts
SubmitChannelLiquidityRequestonchain · offchain · dual_fund · sponsored_deposit
SubmitChannelReleaseRequestonchain · offchain · sponsored_withdrawal
SubmitChannelLeaseExtensionRequestonchain · offchain

The deprecated Request* RPC of each operation, and its estimate, accept the same.

SponsoredDepositFeePayment — a deposit for a wallet with no gas

Added 2026-09-11.

The service funds the channel from this wallet's tokens, on its own transaction, and takes its fee out of the deposit. Hydra App signs the token permit that authorises the pull internally — you do not build or sign one yourself.

This exists for the wallet that holds a token and nothing else: no native asset, so no way to pay for a funding transaction of its own. Every other fee method assumes you can broadcast.

The fee is the quote the request references, or the service's price at execution when it references none — see Quotes. Because the fee comes out of the deposit, the channel ends up holding less than client_amount; estimate first so the user sees which figure is which.

The same rail inside a simple swap is DEPOSIT_RAIL_LIQUIDITY_SERVICE, and its post-swap counterpart is EXIT_RAIL_LIQUIDITY_SERVICE.

SponsoredWithdrawalFeePayment — an exit for a wallet with no gas

Added 2026-09-11. Release requests only (SubmitChannelReleaseRequest, the deprecated RequestChannelRelease).

The service broadcasts this wallet's withdrawal on its own transaction and takes its fee out of what is released — credited to the service in the state co-signed alongside it. Hydra App arms the funding allowance that authorises the credit internally; you do not call SetFundingAllowance yourself.

The gap this closes

Release used to offer two ways to pay, and a wallet with a channel balance and no gas could use neither:

  • On-chain needs a funded wallet — which is exactly what is missing.
  • Off-chain needs a channel balance to pay the invoice from — but withdraw all is about to take that balance away.

So the channel was stranded: funds in it, no way to get them out. This is the only fee method that works from that state, and it is why it exists.

What your node actually does: arm the allowance authorising the credit, then confirm. It never pays a fee of its own, so it takes the funding-allowance confirmation path a dual-funded open takes rather than the settlement path. A request you never confirm takes its allowance back rather than leaving standing consent for the service to draw on.

The fee is the quote the request references, or the service's price at execution when it references none — the same rule as every other method; see Quotes.

Because the fee comes out of the released amount, what lands in your wallet is less than the balance you withdrew. Estimate first so the user sees both figures.

The mechanism underneath is the generic UpdateChannel BalanceCredit — a withdrawal that hands part of itself to the peer. The swap-flow equivalent is EXIT_RAIL_LIQUIDITY_SERVICE; the entry-side counterpart is SponsoredDepositFeePayment.

Quotes: pinning an estimated price

Added 2026-09-11.

Every Estimate*Fee call returns a quote alongside the fee:

FieldTypeDescription
feeDecimalStringThe estimated fee, in the asset named by payment_asset_id
quote_idstring (optional)Names the quote this fee was read from
valid_untilTimestamp (optional)(2026-10-14) When the quote stops being honoured
valid_until_timestamp_secondsuint64 (optional)Deprecated (2026-10-14): read valid_until. The same instant, in Unix seconds

Pass quote_id back on the matching request and the service bills at that quote's price for as long as the quote is valid. Omit it and the service prices the request when it executes — which is a different number if the market moved in between.

A quote_id that has expired, or no longer fits the request, causes the request to be refused rather than silently re-priced. That is the point: the user agreed to a figure, and a request that can no longer honour it should come back for a fresh estimate, not quietly cost more.

Estimate*Fee  ──▶  { fee, quote_id, valid_until }
                        │
      show `fee` to the user, they accept
                        │
                        ▼
Submit*       ──▶  same request + quote_id   ──▶  billed at `fee`
                                              └─▶  refused if expired / changed

GetLiquidityServiceInfo.quote_validity_secs is how long every quote the service issues stays valid, so you can size the confirmation window before you ask for one.

A quote does not reserve liquidity. It fixes the price, not the capacity — a lease that no longer fits the provider's available liquidity is refused even inside its validity window.


Submitting and following a request

(2026-10-14) A request to the liquidity service runs in two phases, and the API splits along them.

  1. Submit. SubmitChannelLiquidityRequest, SubmitChannelReleaseRequest or SubmitChannelLeaseExtensionRequest has the service commit to the request, settles your node's part of the fee, and answers with the request as it then stands: fee_settled, or fee_settlement_failed with why. That normally takes seconds — on the on-chain rail, settled means your node broadcast the fee transfer, not that it confirmed.
  2. Follow. The service carries the request out once it holds the fee: on the on-chain rail once the transfer has the confirmations its network requires, which can take minutes on Ethereum; on every other rail at once. Read the request with GetLiquidityRequest, or watch it on SubscribeLiquidityRequests, until it is final: completed, failed, unresolved or expired.

An error from a Submit* call means the service made no request — the request was invalid, its quote expired or no longer fits, the provider refused it. There is nothing to follow: fix the request and submit it again.

  Submit*  ── error ──▶  no request was made: fix it and submit again
     │
     ▼
  awaiting_fee              the service committed; your node pays its part of the fee
     │
     ├──▶ fee_settled              ┐
     └──▶ fee_settlement_failed    ┘  Submit* answers with one of these
              │
              │  the service carries the request out once it holds the fee:
              │    on-chain rail     — once the transfer has its confirmations
              │    every other rail  — at once
              ▼
  completed · failed · unresolved     final
  expired                             final: fee_deadline passed before the service
                                      received the fee, and nothing was billed

Every request your node makes is recorded, and GetLiquidityRequest and SubscribeLiquidityRequests show it whichever way it was made: through a Submit* RPC, through one of the deprecated Request* RPCs, or by your node's own simple swaps when they provision liquidity.

fee_settled is not done. On the on-chain rail the Submit* call answers as soon as the fee transfer is broadcast, and the service acts only once that transfer has the confirmations its network requires. Wait for completed before you rely on the channel, and read what the service did from its result.

client_request_id — naming a request

client_request_id is your own name for a request: a string of 1 to 64 characters, unique per node and network. It is optional, the Submit* RPCs take it, and it survives restarts of your node.

Submitted under an id that…Answer
names no request yetThe request is submitted under it
names this same request — every field the sameThe request already made, as it stands now. Nothing is submitted again, and nothing is paid again
names a different requestALREADY_EXISTS (JSON-RPC -32005) with client_request_id_taken: payment_uuid=<uuid>, naming the request the id already names. Nothing is submitted
another call is still submitting underOne request: the second call waits for the first and answers with its request
  • A request refused before the service committed to it — an error from Submit* — leaves the id free, so the corrected request can go out under the same id.
  • Read a request back by its id with GetLiquidityRequest.
  • An id outside 1 to 64 characters is refused with INVALID_ARGUMENT: client_request_id must be 1 to 64 characters, got N.
  • The Estimate*Fee RPCs ignore it. The deprecated Request* RPCs refuse a request that carries it, with INVALID_ARGUMENT.

Give every request an id, and recover by it. A Submit* call that was cut off — DEADLINE_EXCEEDED, a dropped connection — may already have made the request: the submission runs on in your node whether or not its caller is still there. Submit again under the same id. That answers with the request the first call made, waiting for the first call if it is still running, and makes the request only if none was made. GetLiquidityRequest by the id reads it without submitting, but a NOT_FOUND right after a cut-off call does not prove that nothing was made: the first call may not have reached the service yet. Without an id, nothing names the request but the payment_uuid in the answer you lost.

Resend the request exactly as you first sent it — the same quote_id too. A retry that re-estimates first carries a new quote_id, which makes it a different request, and the id refuses it with client_request_id_taken.

LiquidityRequest

A request your node submitted to the liquidity service, and where it stands: what every Submit* call, GetLiquidityRequest and SubscribeLiquidityRequests return.

FieldTypeDescription
payment_uuidstringThe service's id for the request: the payment_uuid of its fee payment in GetFeePayments
client_request_idstring (optional)Your name for the request, when it was submitted under one
networkNetworkThe network of the channels the request operates on
operationFeePaymentOperationWhat the request asks of the service, as it was committed — the shape GetFeePayments reports
railFeePaymentRailHow the fee is paid: ONCHAIN / OFFCHAIN / DUAL_FUND / SPONSORED_DEPOSIT / SPONSORED_WITHDRAWAL
payment_networkNetworkThe network the fee is billed on — may differ from network
payment_asset_idstringThe asset the fee is billed in
feeDecimalStringWhat the service bills, in whole units of payment_asset_id
fee_deadlineTimestampUntil when the service waits for your node's part of the fee. A request whose fee it has not received by then expires, and nothing is billed. Absent on DUAL_FUND, whose commitment states none
fee_settlementLiquidityFeeTransfer (one of)How your node settled its part of the fee: onchain { txid }, the transfer it broadcast, or offchain { payment_id }, the payment's id as watchOnlyNode.GetPayments reports it. Absent until settled, and always absent on DUAL_FUND, SPONSORED_DEPOSIT and SPONSORED_WITHDRAWAL: the service broadcasts those rounds, and their fee never leaves a wallet
created_atTimestampWhen the service committed to the request, by your node's clock
updated_atTimestampWhen the request last changed
stateone ofWhere the request stands — see Request states

Request states

Exactly one state is set. completed, failed, unresolved and expired are final.

StateFinalFieldsMeaning
awaiting_feeno—The service committed to the request and waits for your node's part of the fee until fee_deadline. Normally lasts seconds, while your node pays
fee_settledno—Your node settled its part of the fee. The service carries the request out once it holds the fee — see what that takes on each rail
fee_settlement_failednoreasonYour node could not settle its part of the fee, and does not try again. The service carries the request out only if the fee reached it all the same; otherwise it lets the request expire at fee_deadline
completedyesresult (optional)The service carried the request out. result is what it reports it did — below
failedyesreason, refund (both optional)The service could not carry the request out. reason is in the service's words. refund, a LiquidityFeeRefund, appears once the service gives the fee back
unresolvedyesreason (optional)The service stopped at an operation whose outcome it does not know, and says where in its own words. The operation may still take effect on chain, and no later change of the request reports whether it did
expiredyes—fee_deadline passed before the service received the fee. Nothing was billed

completed.result is one of, in the shape of the request's operation:

VariantFieldsReports
channel_liquiditychannel_id, txidA provision: the channel the liquidity landed in — the one the request named, or the one the service opened or chose — and the transaction that put it there
channel_releasechannel_id, txidA release: the channel and its withdrawal or cooperative close transaction
channel_lease_extensionchannel_id, asset_id, lease_expiryA lease_extension: when the extended lease ends (AssetChannel.lease_expiry, a Timestamp)

LiquidityFeeRefund

What the service gave back of a fee, and how it reached your node. A refund goes back the way the fee came: a fee paid on chain by a transaction to the payment's refund address, a fee paid off chain by a payment back over your node's channel. Fees on DUAL_FUND, SPONSORED_DEPOSIT and SPONSORED_WITHDRAWAL never leave a wallet and are never refunded.

FieldTypeDescription
amountDecimalStringWhat the service gave back of the fee, in whole units of payment_asset_id. An on-chain refund's transaction carries this less what the service estimated sending it would cost; an off-chain refund carries all of it
transferLiquidityFeeTransfer (one of)How the refund reached your node

LiquidityFeeTransfer

How a fee, or its refund, moved between your node and the service. Exactly one variant is set:

VariantFieldsDescription
onchaintxidA transaction on chain
offchainpayment_idA payment over your node's channel: its id, as watchOnlyNode.GetPayments reports it

A final request changes only to take what it lacks: a completion's missing result, or the refund given after a failure.

completed without a result, failed or unresolved without a reason

These come from a reconnect. When your node reconnects to the liquidity service, it reads the service's fee-payment history for the requests still in progress and moves each to where the service has it. A request that completed while your node was not following the service takes the result its fee payment recorded (FeePayment.results), and is completed without one only when the service recorded none. A request that failed, or stopped at an operation whose outcome is unknown, meanwhile has no reason: the service's words never reached your node. Read the channel (watchOnlyNode.GetChannel) for what changed.

fee_settlement_failed is not final. The fee may still reach the service, and the request is then carried out as usual. Follow it to a final state before you submit the same request again — submitted twice, it can be carried out twice.

unresolved means unknown, not failed. Its operation may still land on chain, and the request will never say whether it did. Read the channel before you submit the same operation again: a second deposit or withdrawal on top of one that landed is a second operation.


Submit Channel Liquidity Request

(2026-10-14) Ask the liquidity service to provision channel liquidity on your node's behalf. Answers once your node has settled its part of the fee, or failed to; the service then carries the request out on its own, and you follow it.

Method: SubmitChannelLiquidityRequest

Parameters: the RequestChannelLiquidityRequest that Request Channel Liquidity documents — every field, rule and refusal there applies here, What this RPC cannot provision included — plus:

NameTypeRequiredDescription
client_request_idstringno(2026-10-14) Your own name for the request on this network, 1 to 64 characters — see client_request_id

Response: SubmitLiquidityRequestResponse

FieldTypeDescription
requestLiquidityRequestThe request as it stands when the call answers: fee_settled, or fee_settlement_failed with why. Under a client_request_id that already names this same request, that request in whatever state it has reached

An error means the service made no request: every refusal Request Channel Liquidity documents, a quote_id that expired or no longer fits, the provider's refusal. There is nothing to follow. Fix the request and submit it again — under the same client_request_id if you like, since a refused request leaves it free.

What fee settled means on each rail

fee_paymentfee_settled meansThen the service
onchain_fee_paymentYour node broadcast the fee transfer — seconds, not its confirmationsCarries the request out once the transfer has the confirmations its network requires: minutes on Ethereum
offchain_fee_paymentYour node sent the invoice paymentCarries the request out at once
dual_fund_fee_paymentYour node confirmed the funding allowanceBroadcasts the round at once. Its fee is carved out of the round: nothing leaves a wallet as a fee
sponsored_deposit_fee_paymentYour node handed over the token permitsThe same
sponsored_withdrawal_fee_payment (release only)Your node confirmed the funding allowanceThe same: the fee comes out of what is released

Deadline

A Submit* call runs under the 30 s request deadline — it is not exempt, as the deprecated Request* RPCs are — and normally answers in seconds. The submission runs on a task of its own in your node, so a caller that disconnects or is cut off does not stop it. A call cut off with DEADLINE_EXCEEDED (JSON-RPC -32002) may therefore have made the request: submit again under the same client_request_id, or read the request back by it with GetLiquidityRequest — see client_request_id. Without an id, look for the request on SubscribeLiquidityRequests or in GetFeePayments before you submit it again.

Example Request (open channel, offchain fee):

// `request` is the RequestChannelLiquidityRequest of the Request Channel Liquidity example.
request.setClientRequestId('lease-btc-0001')   // your name for it: 1 to 64 characters

// An error here means the service made no request.
const submitted = (await client.submitChannelLiquidityRequest(request, {})).getRequest()!
if (submitted.hasFeeSettlementFailed()) {
  // Not final: the fee may still reach the service. Follow the request.
  console.warn('Fee not settled:', submitted.getFeeSettlementFailed()!.getReason())
}
console.log('Request:', submitted.getPaymentUuid(), 'fee:', submitted.getFee()?.getValue())

To wait for the request to finish, open Subscribe Liquidity Requests before you submit; Common Workflows puts the whole flow together.

Example Response:

{
  "request": {
    "paymentUuid": "5b0d3c1e-8f7a-4c52-9e1b-2a6f0d4c7e91",
    "clientRequestId": "lease-btc-0001",
    "network": { "protocol": "PROTOCOL_BITCOIN", "id": "0a03cf40" },
    "operation": {
      "provision": {
        "operation": {
          "open": {
            "assetLiquidity": {
              "0x0000000000000000000000000000000000000000000000000000000000000000": {
                "serverAmount": { "value": "0.1" },
                "clientAmount": { "value": "0" }
              }
            }
          }
        },
        "leaseDurationSeconds": "2592000"
      }
    },
    "rail": "FEE_PAYMENT_RAIL_OFFCHAIN",
    "paymentNetwork": { "protocol": "PROTOCOL_BITCOIN", "id": "0a03cf40" },
    "paymentAssetId": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "fee": { "value": "0.0001" },
    "feeDeadline": "2026-10-04T10:20:00Z",
    "feeSettlement": { "offchain": { "paymentId": "9c1f0e7d..." } },
    "createdAt": "2026-10-04T10:05:00Z",
    "updatedAt": "2026-10-04T10:05:02Z",
    "feeSettled": {}
  }
}

Over JSON-RPC — submit under a clientRequestId, then read the request back by it. Keys are camelCase, uint64 values are strings, and each oneof is the key of its set arm, per the wire encoding:

curl -s -X POST http://127.0.0.1:5003 \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "liquidity_submitChannelLiquidityRequest",
    "params": [{
      "network": { "protocol": "PROTOCOL_BITCOIN", "id": "0a03cf40" },
      "operation": {
        "open": {
          "assetLiquidity": {
            "0x0000000000000000000000000000000000000000000000000000000000000000": {
              "serverAmount": { "value": "0.1" },
              "clientAmount": { "value": "0" }
            }
          }
        }
      },
      "leaseDurationSeconds": "2592000",
      "paymentNetwork": { "protocol": "PROTOCOL_BITCOIN", "id": "0a03cf40" },
      "paymentAssetId": "0x0000000000000000000000000000000000000000000000000000000000000000",
      "offchainFeePayment": {},
      "clientRequestId": "lease-btc-0001"
    }]
  }'
# → {"jsonrpc":"2.0","id":1,"result":{"request":{"paymentUuid":"5b0d3c1e-...","clientRequestId":"lease-btc-0001",...,"feeSettled":{}}}}

curl -s -X POST http://127.0.0.1:5003 \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "liquidity_getLiquidityRequest",
    "params": [{
      "network": { "protocol": "PROTOCOL_BITCOIN", "id": "0a03cf40" },
      "clientRequestId": "lease-btc-0001"
    }]
  }'
# → {"jsonrpc":"2.0","id":2,"result":{"request":{...,"completed":{"channelLiquidity":{"channelId":"...","txid":"..."}}}}}

A call cut off by the deadline answers {"code":-32002,…} in an ordinary HTTP 200 response. Send the first call again unchanged: under the same clientRequestId it answers with the request already made.


Submit Channel Release Request

(2026-10-14) Ask the liquidity service to withdraw from a channel or cooperatively close it on your node's behalf. Answers as Submit Channel Liquidity Request does: the same response, the same client_request_id rules, the same deadline, and an error the same way means no request was made.

Method: SubmitChannelReleaseRequest

Parameters: the RequestChannelReleaseRequest that Request Channel Release documents, plus client_request_id (string, optional).

Response: SubmitLiquidityRequestResponse — request, a LiquidityRequest. Completed, its result is channel_release { channel_id, txid }. On sponsored_withdrawal_fee_payment, fee_settled means your node confirmed the funding allowance, and the fee comes out of what is released.

A release submitted and followed to its end is in Common Workflows.


Submit Channel Lease Extension Request

(2026-10-14) Extend the lease of one asset on a channel. Answers as Submit Channel Liquidity Request does.

Method: SubmitChannelLeaseExtensionRequest

Parameters: the RequestChannelLeaseExtensionRequest that Request Channel Lease Extension documents — read what an extension rents there — plus client_request_id (string, optional).

Response: SubmitLiquidityRequestResponse — request, a LiquidityRequest. Completed, its result is channel_lease_extension { channel_id, asset_id, lease_expiry }: when the extended lease ends.


Get Liquidity Request

(2026-10-14) One request your node submitted, as it stands now, by the service's id or by your client_request_id. Read-only and local: your node answers from its own record, with no round trip to the liquidity service.

Method: GetLiquidityRequest

Parameters:

NameTypeRequiredDescription
networkNetworkYESThe network the request was submitted on
idone ofYESpayment_uuid (string), the service's id for the request, or client_request_id (string), your name for it

Response:

FieldTypeDescription
requestLiquidityRequestThe request as it stands now

NOT_FOUND (JSON-RPC -32004, no such liquidity request) when your node holds no such request on that network. Right after a Submit* call was cut off, that can mean the submission has not reached the service yet — see client_request_id.

Example Request:

import { GetLiquidityRequestRequest } from './proto/liquidity_pb'

const get = new GetLiquidityRequestRequest()
get.setNetwork({ protocol: 1, id: '0a03cf40' })
get.setClientRequestId('lease-btc-0001')   // or setPaymentUuid(...)

const request = (await client.getLiquidityRequest(get, {})).getRequest()!
if (request.hasCompleted()) {
  const result = request.getCompleted()!.getChannelLiquidity()   // absent if the report was missed
  console.log('Channel:', result?.getChannelId(), 'TX:', result?.getTxid())
}

Example Response:

{
  "request": {
    "paymentUuid": "5b0d3c1e-8f7a-4c52-9e1b-2a6f0d4c7e91",
    "clientRequestId": "lease-btc-0001",
    "network": { "protocol": "PROTOCOL_BITCOIN", "id": "0a03cf40" },
    "operation": {
      "provision": {
        "operation": {
          "open": {
            "assetLiquidity": {
              "0x0000000000000000000000000000000000000000000000000000000000000000": {
                "serverAmount": { "value": "0.1" },
                "clientAmount": { "value": "0" }
              }
            }
          }
        },
        "leaseDurationSeconds": "2592000"
      }
    },
    "rail": "FEE_PAYMENT_RAIL_OFFCHAIN",
    "paymentNetwork": { "protocol": "PROTOCOL_BITCOIN", "id": "0a03cf40" },
    "paymentAssetId": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "fee": { "value": "0.0001" },
    "feeDeadline": "2026-10-04T10:20:00Z",
    "feeSettlement": { "offchain": { "paymentId": "9c1f0e7d..." } },
    "createdAt": "2026-10-04T10:05:00Z",
    "updatedAt": "2026-10-04T10:05:41Z",
    "completed": {
      "channelLiquidity": { "channelId": "ch_def456", "txid": "abc123..." }
    }
  }
}

Subscribe Liquidity Requests

(2026-10-14) The requests your node submits on a network. Server-streaming. The stream opens with every request still in progress — not final — as it stands now, then sends each request again whenever it changes, in the order the changes happened. Each item is the whole LiquidityRequest, not a delta.

Method: SubscribeLiquidityRequests — over JSON-RPC, the WebSocket subscription liquidity_subscribeLiquidityRequests (unsubscribe: liquidity_unsubscribeLiquidityRequests), whose items are LiquidityRequest.

Parameters:

NameTypeRequiredDescription
networkNetworkYESThe network whose requests to follow

Stream item: LiquidityRequest

Waiting for one request

The race-free way to wait for a request you submit:

  1. Open the subscription first, and wait until it is in place: the stream's response headers over gRPC, the subscription id over JSON-RPC.
  2. Submit the request.
  3. Wait on the stream for the submitted payment_uuid to reach a final state. Keep the events that arrive before the Submit* call answers: the request's awaiting_fee and fee_settled usually do.

When the Submit* answer is already final — it answered an earlier submission under the same client_request_id — there is nothing to wait for.

Subscribed after submitting? Read the request once more. The opening snapshot holds only the requests still in progress, so a request that became final before your subscription was in place is not in it, and no later change brings it. Call GetLiquidityRequest once after the subscription is in place: that read and the stream together miss nothing. The same holds when the stream drops — reopen it, then read each request you are still waiting for once.

Example (helpers the Common Workflows use):

import {
  LiquidityRequest,
  SubscribeLiquidityRequestsRequest,
} from './proto/liquidity_pb'

/** completed, failed, unresolved and expired are final. */
function isFinal(r: LiquidityRequest): boolean {
  return r.hasCompleted() || r.hasFailed() || r.hasUnresolved() || r.hasExpired()
}

/**
 * Opens SubscribeLiquidityRequests and resolves once the node has it in place
 * (the response headers). It keeps the latest state of every request it sees,
 * so the events that arrive before a Submit call answers are not lost.
 */
async function openRequests(
  client: LiquidityServiceClient,
  network: { protocol: number; id: string },
) {
  const subscribe = new SubscribeLiquidityRequestsRequest()
  subscribe.setNetwork(network)
  const stream = client.subscribeLiquidityRequests(subscribe, {})

  const latest = new Map<string, LiquidityRequest>()   // payment_uuid → latest state
  let failure: unknown
  let wake: () => void = () => {}
  stream.on('data', (r: LiquidityRequest) => { latest.set(r.getPaymentUuid(), r); wake() })
  stream.on('error', (e: unknown) => { failure = e; wake() })
  stream.on('end', () => { if (!failure) failure = new Error('stream ended'); wake() })
  await new Promise<void>(resolve => {
    wake = () => resolve()
    stream.on('metadata', () => resolve())
  })
  if (failure) throw failure

  return {
    /** Resolves with the submitted request once it is final. */
    async waitFinal(submitted: LiquidityRequest): Promise<LiquidityRequest> {
      if (isFinal(submitted)) return submitted       // answered an earlier submission
      for (;;) {
        const r = latest.get(submitted.getPaymentUuid())
        if (r && isFinal(r)) return r
        if (failure) throw failure                   // reopen, then GetLiquidityRequest once
        await new Promise<void>(resolve => { wake = () => resolve() })
      }
    },
    close: () => stream.cancel(),
  }
}

Keep one stream per network open for the life of your client rather than one per request, as with every other stream: the helpers above open one per call only to stay self-contained.


Request Channel Liquidity

(2026-10-14) Deprecated: use Submit Channel Liquidity Request. This RPC still works, unchanged: it submits the request and waits until the service has carried it out, which on the on-chain fee rail means waiting through the fee transfer's confirmations — minutes on Ethereum. That is why it is exempt from the 30 s request deadline, over gRPC and JSON-RPC alike. It refuses a request that carries a client_request_id (INVALID_ARGUMENT), so a call that is lost leaves nothing to look its request up by; the request is recorded all the same, and SubscribeLiquidityRequests shows it while it is in progress.

Ask the liquidity service to provision channel liquidity on the client's behalf. Hydra App completes fee settlement and the required local confirmations before returning.

Method: RequestChannelLiquidity

Parameters:

NameTypeRequiredDescription
networkNetworkYESChannel network
operationChannelLiquidityRequestOperationYESProvisioning action (open / deposit / deposit_any / open_or_deposit)
lease_duration_secondsuint64ConditionalRequired when any requested asset has server_amount > 0; otherwise omit. Billed as asked: the lease runs this long from when the service funds it and ends on the next lease_slot_secs boundary, the time up to that boundary unbilled
payment_networkNetworkYESNetwork used to pay the service fee
payment_asset_idstringYESAsset used to pay the service fee
fee_paymentone of OnchainFeePayment / OffchainFeePayment / DualFundFeePayment / SponsoredDepositFeePaymentYESExactly one
quote_idstringno(2026-09-11) The quote an earlier estimate of this same request returned — see Quotes. Absent prices the request at execution

Response:

FieldTypeDescription
txidstringFunding transaction ID
channel_idstringChannel ID of the (new or updated) channel
feeDecimalString(2026-09-15) What the service billed, in whole units of payment_asset_id. Settled, not quoted

fee is the figure to reconcile against. It is what the service took, not what an estimate predicted, so accounting needs no diff of channel balances before and after the request. On DualFundFeePayment and SponsoredDepositFeePayment it is carved out of the round and never leaves a wallet — the deposit that lands is smaller by exactly this much.

What this RPC cannot provision

Two rules, and together they close one case off entirely.

A round the client funds alone is served only by the rails the service broadcasts — DualFundFeePayment and SponsoredDepositFeePayment. On OnchainFeePayment and OffchainFeePayment the service funds its own side and broadcasts nothing of the client's, so a request whose every server_amount is zero is refused:

A round funded by the client alone is served by the DualFund and SponsoredDeposit rails only

The native asset cannot be deposited on the client's behalf at all. A native contribution can only ride the transaction's own value, and on both broadcasting rails that transaction is the service's — so a leg asking the client to fund native is refused there too:

the native asset … cannot be deposited on the client's behalf: a native deposit is
settled from the broadcaster's own transaction value, and the hub broadcasts a
sponsored round

A client-funded native deposit therefore has no rail here. Use NodeService.OpenChannel / DepositChannel / UpdateChannel instead: the client broadcasts its own funding transaction, so the value it attaches is its own deposit. That path needs the wallet to hold native gas — which is exactly the constraint SponsoredDepositFeePayment exists to remove for tokens.

On DualFundFeePayment, a successful estimate does not prove the round is accepted.OnchainFeePayment and SponsoredDepositFeePayment refuse a client-funded native leg at estimate time. DualFundFeePayment currently does not: EstimateRequestChannelLiquidityFee returns a real quote, and the same request pinned to that quote is then refused later, when the funding allowance is confirmed, with the message above.

Treat a quote as a price, never as an admission check. Neither call moves funds, so the failure costs only the round trip — but budget for it landing one call later than you would expect.

Example Request (open channel, offchain fee):

import { LiquidityServiceClient } from './proto/LiquidityServiceClientPb'
import {
  RequestChannelLiquidityRequest,
  ChannelLiquidityRequestOperation,
  AssetLiquidity,
  OffchainFeePayment
} from './proto/liquidity_pb'

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

const request = new RequestChannelLiquidityRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' }) // Bitcoin Signet

// Open new channel with 0.1 BTC on the server side
const open = new ChannelLiquidityRequestOperation.Open()
const liq = new AssetLiquidity()
liq.setServerAmount({ value: '0.1' })
liq.setClientAmount({ value: '0' })
open.getAssetLiquidityMap().set('0x0000000000000000000000000000000000000000000000000000000000000000', liq)

const op = new ChannelLiquidityRequestOperation()
op.setOpen(open)
request.setOperation(op)

request.setLeaseDurationSeconds(2592000) // 30 days
request.setPaymentNetwork({ protocol: 1, id: '0a03cf40' })
request.setPaymentAssetId('0x0000000000000000000000000000000000000000000000000000000000000000')
request.setOffchainFeePayment(new OffchainFeePayment())

const response = await client.requestChannelLiquidity(request, {})
console.log('Channel:', response.getChannelId(), 'TX:', response.getTxid())

Example Response:

{
  "txid": "abc123...",
  "channel_id": "ch_def456"
}

Estimate Request Channel Liquidity Fee

Estimate the service fee for a channel liquidity request before executing it. Takes the same RequestChannelLiquidityRequest and returns just the fee.

Topping up a leased channel renews what is already in it. A lease covers the provider's whole side of an asset channel, so a deposit into a channel the provider already leases also leases the capital already there for the new window, for every hour past where its current lease ends. The fee includes that rent, which is why the same window can cost more on one channel than on another. Ask for an open to lease only the new amount, in a channel of its own.

Method: EstimateRequestChannelLiquidityFee

Parameters: Same as Request Channel Liquidity.

Response:

FieldTypeDescription
feeDecimalStringEstimated service fee, denominated in the asset identified by payment_asset_id
quote_idstring (optional)(2026-09-11) Pass this back on the matching request to be billed at fee — see Quotes
valid_untilTimestamp (optional)(2026-10-14) When the quote stops being honoured
valid_until_timestamp_secondsuint64 (optional)(2026-09-11) Deprecated (2026-10-14): read valid_until. The same instant, in Unix seconds
lease_duration_secondsuint64 (optional)The lease window fee pays for, as billed. The lease runs that long from when the service funds it, to the slot boundary it lands in. Absent when the request carries no lease window

Example Request:

// Build the same RequestChannelLiquidityRequest as you would for the actual call
const response = await client.estimateRequestChannelLiquidityFee(request, {})
console.log('Estimated fee:', response.getFee()?.getValue())

Example Response:

{
  "fee": { "value": "0.0001" },
  "quoteId": "q_8f3c1a...",
  "validUntil": "2026-09-10T03:52:25+00:00"
}

Request Channel Release

(2026-10-14) Deprecated: use Submit Channel Release Request. This RPC still works, unchanged: it submits the request and waits until the service has carried it out — on the on-chain fee rail, through the fee transfer's confirmations. That is why it is exempt from the 30 s request deadline, over gRPC and JSON-RPC alike. It refuses a request that carries a client_request_id (INVALID_ARGUMENT).

Ask the liquidity service to withdraw assets from a channel or cooperatively close it. The service charges a fee and Hydra App settles it before observing the release update.

Method: RequestChannelRelease

Parameters:

NameTypeRequiredDescription
networkNetworkYESChannel network
operationChannelReleaseOperationYESwithdraw or cooperative_close
payment_networkNetworkYESNetwork used to pay the service fee
payment_asset_idstringYESAsset used to pay the service fee
fee_paymentone of OnchainFeePayment / OffchainFeePayment / SponsoredWithdrawalFeePaymentYESExactly one. SponsoredWithdrawalFeePayment is release-only — see above
quote_idstringno(2026-09-11) The quote an earlier estimate of this same request returned — see Quotes

Response:

FieldTypeDescription
txidstringRelease transaction ID
feeDecimalString(2026-09-15) What the service billed, in whole units of payment_asset_id. SponsoredWithdrawalFeePayment carves it out of what is released

Example Request (cooperative close, offchain fee):

import {
  RequestChannelReleaseRequest,
  ChannelReleaseOperation,
  OffchainFeePayment
} from './proto/liquidity_pb'

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

const close = new ChannelReleaseOperation.CooperativeClose()
close.setChannelId('ch_def456')
const op = new ChannelReleaseOperation()
op.setCooperativeClose(close)
request.setOperation(op)

request.setPaymentNetwork({ protocol: 1, id: '0a03cf40' })
request.setPaymentAssetId('0x0000000000000000000000000000000000000000000000000000000000000000')
request.setOffchainFeePayment(new OffchainFeePayment())

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

Example Response:

{ "txid": "rel_abc789..." }

Estimate Request Channel Release Fee

Estimate the service fee for a channel release before executing it.

Method: EstimateRequestChannelReleaseFee

Parameters: Same as Request Channel Release.

Response:

FieldTypeDescription
feeDecimalStringEstimated service fee
quote_idstring (optional)(2026-09-11) Pass this back on the matching request to be billed at fee — see Quotes
valid_untilTimestamp (optional)(2026-10-14) When the quote stops being honoured
valid_until_timestamp_secondsuint64 (optional)(2026-09-11) Deprecated (2026-10-14): read valid_until. The same instant, in Unix seconds

Example:

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

Example Response:

{ "fee": "0.00005" }

Request Channel Lease Extension

(2026-10-14) Deprecated: use Submit Channel Lease Extension Request. This RPC still works, unchanged: it submits the request and waits until the service has carried it out — on the on-chain fee rail, through the fee transfer's confirmations. That is why it is exempt from the 30 s request deadline, over gRPC and JSON-RPC alike. It refuses a request that carries a client_request_id (INVALID_ARGUMENT).

Extend the duration of an existing lease on a specific channel asset — including one the hub opened on demand, whose lifetime is its lease. lease_extension_seconds is an extension delta, not a replacement absolute expiry. It is added to the expiry of the lease you paid for — AssetChannel.lease_expiry — and sold in whole slots of the provider's lease clock (lease_slot_secs in Get Liquidity Service Info, one hour): extending by an hour moves lease_expiry by exactly an hour, and half an hour is sold, priced and delivered as a whole one. A lease that has already ended is extended from the next slot boundary. The estimate returns the rounded extension and the expiry it delivers.

Check both expiries before paying to extend. AssetChannel.lease_expiry is the lease you paid for. AssetChannel.liquidity_expiry is until when the provider's liquidity actually stays in the channel: the later of that lease and what the channel's usage, or how little of the provider's capital it holds, buys it. Every payment through the channel buys it time, in proportion to the payment's size against the provider's capital in the channel; that time adds up, but never reaches more than the provider's usage window (ten days on mainnet) past the latest payment. So an actively-traded channel's liquidity_expiry keeps running ahead of its lease, while one that no payment has crossed for that window keeps the provider's liquidity only until its lease ends — then the provider withdraws it, leaving the channel open with no inbound liquidity from the provider until you lease again. Provider liquidity in a channel that carries no lease follows the same rule. Both ride along on every watchOnlyNode.GetChannels / GetChannel read, and liquidity.GetLeaseExpiries returns them for all channels at once. Extend when liquidity_expiry is actually running down, or when you need the time guaranteed: only the lease is.

An extension rents exactly the time it adds. It counts from lease_expiry, or from the next slot boundary once the lease has ended, and rents that time — the estimate's lease_extension.rented_seconds. Time usage holds the liquidity in place past the lease is not paid time and is not counted: a lease paid to 15:00 on a channel whose liquidity_expiry is 18:00 extends by one hour to 16:00, for one hour of rent, and liquidity_expiry then shows the later of 16:00 and what usage still buys.

Method: RequestChannelLeaseExtension

Parameters:

NameTypeRequiredDescription
networkNetworkYESChannel network
channel_idstringYESChannel whose lease to extend
asset_idstringYESSpecific asset whose lease is being extended
lease_extension_secondsuint64YESTime to add to the lease's expiry (AssetChannel.lease_expiry), rounded up to whole slots (lease_slot_secs) — see above
payment_networkNetworkYESNetwork used to pay the service fee
payment_asset_idstringYESAsset used to pay the service fee
fee_paymentone of OnchainFeePayment / OffchainFeePaymentYESExactly one. The dual-fund and sponsored methods are not valid here
quote_idstringno(2026-09-11) The quote an earlier estimate of this same request returned — see Quotes

Response:

FieldTypeDescription
channel_idstringChannel that was extended
asset_idstringAsset whose lease was extended
expiry_timestamp_secondsint64The lease expiry after the extension (Unix seconds). With the estimate's quote_id, the expiry that estimate showed — reached for less rent when another paid operation carried the lease part of the way there meanwhile — later when the lease ended while the fee was being paid and so counted from a later slot boundary, earlier only when lease time the estimate counted from was taken back because its own fee was never collected
feeDecimalString(2026-09-15) What the service billed, in whole units of payment_asset_id

Example Request:

import {
  RequestChannelLeaseExtensionRequest,
  OffchainFeePayment
} from './proto/liquidity_pb'

const request = new RequestChannelLeaseExtensionRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setChannelId('ch_def456')
request.setAssetId('0x0000000000000000000000000000000000000000000000000000000000000000')
request.setLeaseExtensionSeconds(2592000) // +30 days
request.setPaymentNetwork({ protocol: 1, id: '0a03cf40' })
request.setPaymentAssetId('0x0000000000000000000000000000000000000000000000000000000000000000')
request.setOffchainFeePayment(new OffchainFeePayment())

const response = await client.requestChannelLeaseExtension(request, {})
console.log('New expiry:', response.getExpiryTimestampSeconds())

Example Response:

{
  "channelId": "ch_def456",
  "assetId": "0x0000000000000000000000000000000000000000000000000000000000000000",
  "expiryTimestampSeconds": "1735689600"
}

Estimate Request Channel Lease Extension Fee

Estimate the service fee for a lease extension before executing it.

The fee is the whole fee the extension is billed: the setup fee, plus the rental of the capital the provider lent into the asset channel — never funds you put in yourself — for lease_extension.rented_seconds, the time the extension adds. Pass its quote_id back on the request and you are billed no more than that figure, even if the channel grows in between — less when another paid operation carries the lease part of the way meanwhile — and the lease runs to the expiry the estimate showed: later when the lease ends while the fee is being paid, since it then counts from a later slot boundary, earlier only when lease time the estimate counted from is taken back because its own fee was never collected. See Quotes.

The estimate refuses an extension the request would refuse: an asset channel that is closed, has an on-chain update in flight, holds no capacity, or holds none of the provider's funds.

Method: EstimateRequestChannelLeaseExtensionFee

Parameters: Same as Request Channel Lease Extension.

Response:

FieldTypeDescription
feeDecimalStringEstimated service fee
quote_idstring (optional)(2026-09-11) Pass this back on the matching request to be billed at fee — see Quotes
valid_untilTimestamp (optional)(2026-10-14) When the quote stops being honoured
valid_until_timestamp_secondsuint64 (optional)(2026-09-11) Deprecated (2026-10-14): read valid_until. The same instant, in Unix seconds
lease_extensionLeaseExtensionTerms (optional)What the extension buys — below

LeaseExtensionTerms — every instant is a slot boundary, and every duration a whole number of slots:

FieldTypeDescription
extension_secondsuint64The extension asked for, rounded up to whole slots
starts_atTimestamp(2026-10-14) Where it counts from: the lease's expiry (AssetChannel.lease_expiry), or the next slot boundary once it has passed
expiryTimestamp(2026-10-14) The lease expiry after the extension: extension_seconds past starts_at
rented_secondsuint64The lease time fee pays rent for, from starts_at to expiry: extension_seconds
from_timestamp_secondsuint64Deprecated (2026-10-14): read starts_at. The same instant, in Unix seconds
expiry_timestamp_secondsuint64Deprecated (2026-10-14): read expiry. The same instant, in Unix seconds

Example:

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

Example Response:

{
  "fee": "0.00008",
  "quoteId": "3f0f6f52-5a3b-4b1e-9b1f-6a0e2c1d9e44",
  "validUntil": "2026-09-25T09:57:00+00:00",
  "leaseExtension": {
    "extensionSeconds": "3600",
    "startsAt": "2026-09-25T18:00:00+00:00",
    "expiry": "2026-09-25T19:00:00+00:00",
    "rentedSeconds": "3600"
  }
}

Get Liquidity Service Info

Server-wide bounds, durations, per-asset fee configuration, the assets a fee may be paid in with what each rail charges, and the provider's node public key on each network. Read-only, no side effects. (2026-10-14) Your node keeps the answer until the provider announces a change of its terms, and then reads it again: call it whenever you show fee options, rather than once at start.

Method: GetLiquidityServiceInfo — Parameters: None.

Response:

FieldTypeDescription
min_duration_secs / max_duration_secsuint64The lease-duration window the service accepts
lease_slot_secsuint64The unit of the lease clock: every lease ends on a slot boundary, which is when the service acts on the channel — a lease runs its window from when the service funds it to the next boundary, the time up to it unbilled — and an extension is rounded up to whole slots. One hour on this service; a provider that runs its clock to the second reports 1, and one that does not say reports 0
min_capacity_usd / max_capacity_usdDecimalStringCapacity bounds, in USD
asset_configsLiquidityAssetConfig[]Per-asset fee configuration — see below
node_pubkeysLiquidityNetworkNode[]{ network, node_pubkey } — the provider's node id per network
quote_validity_secsuint64How long every quote the service issues stays valid — see Quotes
fee_payment_assetsLiquidityFeePaymentAsset[](2026-10-14) The assets a fee may be paid in, each with the rails the service accepts it on and what each of them charges — see below. A fee in an asset or on a rail missing here is refused with FAILED_PRECONDITION

LiquidityAssetConfig:

FieldTypeDescription
networkNetwork(2026-10-14) The network the asset is leased on
asset_idstringThe asset
fee_ratio_per_hourDecimalStringHourly fee ratio — "0.001" is 0.1% per hour
pricing_tickerstring (optional)External pricing ticker used for the USD conversion
protocolProtocolDeprecated (2026-10-14): read network
network_idstringDeprecated (2026-10-14): read network

node_pubkeys is where a bot gets the peer id to connect to before requesting liquidity — it is the same value the peer tables publish, read from the live service.

LiquidityFeePaymentAsset (2026-10-14):

FieldTypeDescription
networkNetworkThe network the fee is paid on
asset_idstringThe asset the fee is paid in, as the service spells it
railsLiquidityFeeRailsThe rails a fee in it is accepted on

LiquidityFeeRails — one field per fee_payment variant, each a LiquidityFeeRailTerms, absent when the service refuses that rail for the asset: onchain, offchain, dual_fund, sponsored_deposit, sponsored_withdrawal. Bitcoin carries only onchain and offchain, and a sponsored deposit is never offered in a network's native asset.

LiquidityFeeRailTerms — what a fee paid on that rail is made of, beyond the hourly rent of the leased assets (fee_ratio_per_hour above):

FieldTypeDescription
on_chain_cost_multipleDecimalStringEvery on-chain cost the service pays to carry the request out — funding, permits, a withdrawal or a close — is billed at this multiple of its estimate. On a sponsored deposit, the largest multiple among the assets it pulls applies
force_close_cost_multipleDecimalStringThe force-close cost each funded channel is billed for, at this multiple of its estimate
min_fee_usdDecimalStringThe least the setup fee of a request comes to, in USD

The terms say how a fee is built, not what it comes to. Estimate the request (Estimate*Fee) for the figure: on-chain costs move with the chain's fee rate, and the rent with the amount and duration.


Get Leaseable Asset Info

Liquidity bounds and fee ratio for one (network, asset) pair on the provider. This is the call that answers "can I lease this, and how much".

Method: GetLeaseableAssetInfo

Parameters:

NameTypeRequiredDescription
networkNetworkYESChannel network
asset_idstringYESAsset to lease

Response: info (LeaseableAssetInfo):

FieldTypeDescription
available_liquidityDecimalStringWhat the provider can lease of this asset right now
min_capacity / max_capacityDecimalStringPer-lease size bounds, in the asset's own units
min_duration_secs / max_duration_secsuint64Duration bounds for this asset
fee_ratioDecimalStringThe fee ratio applied to this asset

Sizing a lease against available_liquidity is what avoids the lease_too_big estimate variant — that variant reports the same three numbers after the fact.


Get Leases

The caller's active leases on a network.

Method: GetLeases

Parameters:

NameTypeRequiredDescription
networkNetworkYESNetwork to list leases for

Response: leases (AssetChannelLease[]):

FieldTypeDescription
networkNetworkThe network the lease is on
channel_idstringThe leased channel
asset_idstringThe leased asset
expiryTimestamp (optional)When the lease you paid for ends, on a slot boundary. Absent when the asset channel carries no lease
liquidity_expiryTimestamp (optional)Until when the provider's liquidity stays in the asset channel: the later of the lease and what the channel's usage, or how little of the provider's capital it holds, buys it — each payment buying time, up to a bounded window past the latest one. Absent while the provider has no plan to take it back, not "never"

Get Lease Expiries

Every lease expiry the node holds for a network, in one call, keyed by channel then asset.

Method: GetLeaseExpiries

Parameters:

NameTypeRequiredDescription
networkNetworkYESNetwork to read expiries for

Response:

FieldTypeDescription
channelsmap<string, ChannelAssetExpiries>channel_id → { assets, liquidity_expiries }, each a map<asset_id, Timestamp>: when the lease you paid for ends (assets, as AssetChannel.lease_expiry), and until when the provider's liquidity stays (liquidity_expiries, as AssetChannel.liquidity_expiry)

This one reads the local cache — no round-trip to the provider

That makes it cheap enough to poll on a UI timer, and it is the right call for "which of my leases are running down". GetLeases asks the provider and is the authority when the two disagree.

Usage keeps the provider's liquidity in place past the lease on its own, so an actively-traded channel's liquidity expiry moves forward without you paying anything. Watch it rather than counting down from the duration you bought.


Get Fee Payments

(2026-09-15) What this node has been billed on a network, newest first — one record per payment the service committed to, whatever became of it.

Method: GetFeePayments

Parameters:

NameTypeRequiredDescription
networkNetworkYESThe network the payments were committed on — the channel network, not the one their fee was billed on
statusesrepeated FeePaymentStatusnoRestrict to these lifecycle states. Empty returns every state
created_afterTimestampno(2026-10-14) Only payments committed at or after this instant. Absent reaches back to the first payment
created_after_timestamp_secondsuint64noDeprecated (2026-10-14): set created_after. The same bound, in Unix seconds. A request that sets both is refused with INVALID_ARGUMENT
beforeFeePaymentCursornoReturn only payments strictly older than this cursor. Absent starts at the newest
limituint32noPage size, clamped to 200. 0 asks for the service's default (50)

Response:

FieldTypeDescription
paymentsrepeated FeePaymentNewest first
nextFeePaymentCursorPass back as before for the next page. Absent when the history ends

FeePaymentCursor

FieldTypeDescription
created_at_timestamp_microsint64The last returned payment's creation instant, in microseconds since the Unix epoch
payment_uuidstringThe last returned payment's id

Treat it as opaque — pass back the next you were given, unchanged. The two fields are documented so you can persist and reconstruct one across a restart, not so you can synthesise a position.

The instant is microseconds rather than seconds on purpose: payments committed inside the same second still have to page deterministically, and the id is the tie-breaker when even that collides.

FeePayment

FieldTypeDescription
payment_uuidstringNames this payment — the same id the service's updates carry
networkNetworkThe network whose channels the operations touched
operationsrepeated FeePaymentOperationWhat the payment covered — see below
statusFeePaymentStatusPENDING / PROCESSING / COMPLETED / EXPIRED / FAILED / REFUNDED
railFeePaymentRailONCHAIN / OFFCHAIN / DUAL_FUND / SPONSORED_DEPOSIT / SPONSORED_WITHDRAWAL
payment_networkNetworkThe network the fee was billed on — may differ from network
payment_asset_idstringThe asset the fee was billed in
feeDecimalStringWhat was billed, in whole units of payment_asset_id
refundLiquidityFeeRefund(2026-10-14) What the service gave back of the fee, and how it reached this wallet. Present only on a refunded payment
created_atTimestamp(2026-10-14) When the service committed to this payment
fee_deadlineTimestamp(2026-10-14) Until when the service waited for this wallet's part of the fee
outcome_unknownbool(2026-10-14) Whether the operations stopped at one whose outcome is unknown, which may have taken effect: it may still land, or landed after the round ended, as the channel then shows. Only a FAILED payment's can be — the payment of a request that reads unresolved. Cleared once the service sees the operation can no longer land
resultsrepeated LiquidityOperationResult(2026-10-14) What the operations the service carried out did, in the order it carried them out — one of channel_liquidity { channel_id, txid }, channel_release { channel_id, txid }, channel_lease_extension { channel_id, asset_id, lease_expiry } each, the shapes completed.result takes. An operation that did not take effect has none; one whose outcome was unknown and that landed later has its own
refund_amountDecimalStringDeprecated (2026-10-14): read refund. What was given back
refund_tx_hashstringDeprecated (2026-10-14): read refund. The refund's transaction or off-chain payment, without saying which
created_at_timestamp_secondsuint64Deprecated (2026-10-14): read created_at. The same instant, in Unix seconds
expires_at_timestamp_secondsuint64Deprecated (2026-10-14): read fee_deadline. The same instant, in Unix seconds

FeePaymentOperation

The same operation shapes a request carries, read back. Exactly one variant is set:

VariantTypeDescription
provision{ operation: ChannelLiquidityRequestOperation, lease_duration_seconds }An open, deposit, deposit_any or open_or_deposit, with the lease window it bought
releaseChannelReleaseOperationA withdraw or a cooperative_close. Buys no lease time
lease_extensionChannelLeaseExtensionOperation{ channel_id, asset_id, lease_extension_seconds } — an extension delta, not a replacement expiry

The duration sits inside provision rather than beside the variants because only a provisioning operation has one: a release buys no lease time, and an extension states its own delta.

A record states the amounts it was priced on. The operation comes back in the vocabulary it was requested in — the same AssetLiquidity and AssetWithdrawAmounts the request carried — so a fee can be reconciled against what it bought, not merely against which assets it touched.

Only COMPLETED means you got what you paid for. EXPIRED billed nothing — the deadline elapsed before this wallet completed its half. REFUNDED billed and gave it back; refund says how much, and how it reached this wallet. FAILED is the one to reconcile by hand: no refund was made, and the operations did not complete — unless outcome_unknown is set, when one of them may have taken effect: read the channel, and results, which holds it once the service has seen it land.

Page by cursor, never by offset

Pass the response's next back as before, and stop when it comes back absent. New payments arrive at the newest end of this ordering, which is exactly where an offset would shift a page under you — the same row returned twice, or skipped.

The cursor is a position, not an index: a creation instant in microseconds plus the payment id that breaks ties with anything committed in the same instant. Treat it as opaque and hand it back unchanged.


Duration Reference

lease_duration_seconds and lease_extension_seconds are expressed in seconds. Every lease ends on a boundary of the service's lease clock (lease_slot_secs): a lease window runs from when the service funds it to the next boundary, and an extension is rounded up to whole slots — its estimate says what it became.

DurationSecondsCalculation
1 hour3,60060 × 60
1 day86,40024 × 60 × 60
1 week604,8007 × 24 × 60 × 60
30 days2,592,00030 × 24 × 60 × 60
90 days7,776,00090 × 24 × 60 × 60
const days = 30
const leaseSeconds = days * 24 * 60 * 60 // 2,592,000

Choosing a fee payment method

VariantAvailable onWhen to use
offchain_fee_paymentLiquidity / Release / Lease ExtensionYou have offchain (channel) balance to pay the fee — fastest, lowest overhead. The estimate and the request both refuse a fee above what the node can send off-chain on the payment network (max_sendable of the payment asset), before anything is committed: pay it on-chain instead
onchain_fee_payment (UTXO)Liquidity / Release / Lease Extension on Bitcoin-style protocolsYou're paying the fee from on-chain funds and need a refund address for change
onchain_fee_payment (Account)Liquidity / Release / Lease Extension on EVM-style protocolsYou're paying the fee from an EVM account; refund_address is optional
dual_fund_fee_paymentLiquidity onlyYou're contributing client-side funds (client_amount > 0) and want the fee settled inside the dual-fund flow
sponsored_deposit_fee_paymentLiquidity only(2026-09-11) The wallet holds the token but no native asset — the service broadcasts the funding and takes its fee out of the deposit. See SponsoredDepositFeePayment
sponsored_withdrawal_fee_paymentRelease only(2026-09-11) The wallet holds a channel balance but no gas — the only method that can exit that channel at all. See SponsoredWithdrawalFeePayment

Common Workflows

(2026-10-14) Both workflows submit under a client_request_id and follow the request to its end with the helpers from Subscribe Liquidity Requests: the stream is open before the request is submitted, so no change of it is missed.

Open a channel with server-provided liquidity (offchain fee)

async function openChannelWithLiquidity(
  client: LiquidityServiceClient,
  network: { protocol: number; id: string },
  assetId: string,
  serverAmount: string,
  leaseDays: number,
  clientRequestId: string   // your name for this request: persist it, and reuse it on a retry
) {
  const request = new RequestChannelLiquidityRequest()
  request.setNetwork(network)

  const open = new ChannelLiquidityRequestOperation.Open()
  const liq = new AssetLiquidity()
  liq.setServerAmount({ value: serverAmount })
  liq.setClientAmount({ value: '0' })
  open.getAssetLiquidityMap().set(assetId, liq)

  const op = new ChannelLiquidityRequestOperation()
  op.setOpen(open)
  request.setOperation(op)

  request.setLeaseDurationSeconds(leaseDays * 24 * 60 * 60)
  request.setPaymentNetwork(network)
  request.setPaymentAssetId(assetId)
  request.setOffchainFeePayment(new OffchainFeePayment())
  request.setClientRequestId(clientRequestId)

  // 1. Estimate first
  const estimate = await client.estimateRequestChannelLiquidityFee(request, {})
  console.log('Estimated fee:', estimate.getFee()?.getValue())

  // 2. Subscribe, then submit, then follow the request until it is final
  const requests = await openRequests(client, network)
  try {
    const submitted = (await client.submitChannelLiquidityRequest(request, {})).getRequest()!
    const final = await requests.waitFinal(submitted)
    if (!final.hasCompleted()) {
      throw new Error(`request ${final.getPaymentUuid()} ended without completing`)
    }
    // Absent when the node missed the service's report: read the channel then.
    const result = final.getCompleted()!.getChannelLiquidity()
    return {
      channelId: result?.getChannelId(),
      txid: result?.getTxid()
    }
  } finally {
    requests.close()
  }
}

Cooperatively close a channel through the service

async function cooperativeClose(
  client: LiquidityServiceClient,
  network: { protocol: number; id: string },
  channelId: string,
  paymentAssetId: string,
  clientRequestId: string   // your name for this request: persist it, and reuse it on a retry
) {
  const request = new RequestChannelReleaseRequest()
  request.setNetwork(network)

  const close = new ChannelReleaseOperation.CooperativeClose()
  close.setChannelId(channelId)
  const op = new ChannelReleaseOperation()
  op.setCooperativeClose(close)
  request.setOperation(op)

  request.setPaymentNetwork(network)
  request.setPaymentAssetId(paymentAssetId)
  request.setOffchainFeePayment(new OffchainFeePayment())
  request.setClientRequestId(clientRequestId)

  // Subscribe, then submit, then follow the request until it is final
  const requests = await openRequests(client, network)
  try {
    const submitted = (await client.submitChannelReleaseRequest(request, {})).getRequest()!
    const final = await requests.waitFinal(submitted)
    if (!final.hasCompleted()) {
      throw new Error(`release ${final.getPaymentUuid()} ended without completing`)
    }
    // Absent when the node missed the service's report: read the channel then.
    return final.getCompleted()!.getChannelRelease()?.getTxid()
  } finally {
    requests.close()
  }
}

Error Handling

Error CodeDescriptionSolution
INVALID_ARGUMENTMissing lease_duration_seconds while server_amount > 0, or no fee_payment variant setProvide a duration when leasing; pick exactly one fee payment variant
INVALID_ARGUMENTdual_fund_fee_payment or sponsored_deposit_fee_payment used outside of a channel liquidity request (SubmitChannelLiquidityRequest, the deprecated RequestChannelLiquidity)Switch to onchain_fee_payment or offchain_fee_payment
INVALID_ARGUMENT(2026-10-14) client_request_id must be 1 to 64 characters, got NSend an id of 1 to 64 characters, or none
INVALID_ARGUMENT(2026-10-14) client_request_id is answered by SubmitChannelLiquidityRequest, not by this deprecated RPC — a deprecated Request* RPC was sent a client_request_id; the message names the matching Submit* RPCCall the Submit* RPC it names
ALREADY_EXISTS(2026-10-14) client_request_id_taken: payment_uuid=… — the id already names a different request, the one payment_uuid namesOn a retry, resend the first request exactly, quote_id included; otherwise submit this one under a new id. See client_request_id
NOT_FOUND(2026-10-14) no such liquidity request — GetLiquidityRequest for a request your node does not hold on that networkCheck the network and the id. Right after a Submit* call was cut off, submit again under the same id rather than read
DEADLINE_EXCEEDED(2026-10-14) A Submit* call outlived the 30 s request deadline. The request may already have been madeSubmit again under the same client_request_id, or read it back with GetLiquidityRequest — see Deadline
FAILED_PRECONDITIONThe quote_id has expired, or no longer fits the requestRe-run the matching Estimate*Fee, show the user the new figure, and send the fresh quote_id — see Quotes
RESOURCE_EXHAUSTEDThe request would take this client's leased capital past the per-client limit (Hub capital committed to this client would exceed the per-client limit…), or a sponsored rail has reached its budget for nowLease less, or let a lease lapse first; for a sponsored rail, try again later or pay the fee another way
FAILED_PRECONDITIONChannel does not exist, asset not present in channel, or release/extension not permittedVerify channel_id and asset_id against watchOnlyNode.GetChannel
FAILED_PRECONDITION(2026-10-14) A fee is not accepted in … on network … on the … rail — the asset or the rail is not among those the service acceptsPay in an asset, and on a rail, listed in GetLiquidityServiceInfo's fee_payment_assets
FAILED_PRECONDITION(2026-10-14) Operation #N: asset … of channel … has an on-chain operation awaiting confirmation — a deposit, withdrawal or close of an asset channel whose last on-chain operation has not confirmedWait until your node shows the asset channel is_updatable again — it does once the operation has confirmed — then ask again. Do not retry on a timer
FAILED_PRECONDITION(2026-10-14) Operation #N: asset … of channel … is not updatable: … — a deposit, withdrawal or close of an asset channel the hub's node cannot update now: an update of it is under way, or your node is not connected to the hubWait until your node shows the asset channel is_updatable again — reconnect to the hub if your node is not connected — then ask again. Do not retry on a timer
FAILED_PRECONDITION(2026-10-14) the hub keeps asset … of channel … open until … — a release of an asset channel the hub opened on demand, before the hub's keep_open_until, the time the refusal namesClose the channel from your own node, or release it once that time has passed — see above
UNAVAILABLELiquidity service temporarily unavailableRetry with exponential backoff

Best Practices

  1. Submit with a client_request_id, and follow the request. (2026-10-14) Use the Submit* RPCs, not the deprecated Request* ones. Name every request, open SubscribeLiquidityRequests before you submit, and act on the request's final state — not on the Submit* answer, which on the on-chain rail comes before the fee transfer has confirmed. After a call that was cut off, submit again under the same id.
  2. Always estimate first. Call the matching Estimate* RPC before the operation.
  3. Provide lease_duration_seconds only when needed. Required if any asset has server_amount > 0; otherwise omit.
  4. Pick the right fee payment. Offchain is cheapest; dual-fund and sponsored-deposit are only valid on a channel liquidity request.
    • Pass the quote_id from your estimate. Without it the service prices the request at execution, and the user is billed a figure they never saw.
  5. Reuse channels when possible. deposit / deposit_any / open_or_deposit avoid the cost of opening a new channel.
  6. Treat lease_extension_seconds as a delta, not a target expiry. It counts from lease_expiry and is sold in whole slots; offer durations in multiples of lease_slot_secs.
  7. Watch AssetChannel.liquidity_expiry to know when the provider takes its liquidity back. lease_expiry is the lease you paid for and moves only when a lease is paid for; usage keeps the liquidity in place past it for as long as payments keep buying time, and liquidity_expiry shows until when. Read both on channel reads (or liquidity.GetLeaseExpiries) and extend only when liquidity_expiry is genuinely running down. Once it has passed, the provider takes its liquidity back as soon as your node is online — it does not wait for you to be online at a particular time.
  8. On withdraw, name the side. asset_amounts needs server_amount and/or client_amount per asset — at least one — since 2026-07-23.

← Back to API Reference | Next: Watch-Only Node API →


Copyright © 2025