Lease API
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
- Submit Channel Liquidity Request
- Submit Channel Release Request
- Submit Channel Lease Extension Request
- Get Liquidity Request
- Subscribe Liquidity Requests
- Request Channel Liquidity (deprecated)
- Estimate Request Channel Liquidity Fee
- Request Channel Release (deprecated)
- Estimate Request Channel Release Fee
- Request Channel Lease Extension (deprecated)
- Estimate Request Channel Lease Extension Fee
Read-only discovery
Common types
ChannelLiquidityRequestOperation (one of)
Used by SubmitChannelLiquidityRequest, the deprecated RequestChannelLiquidity, and their estimator. Exactly one variant must be set.
| Variant | Fields | Purpose |
|---|---|---|
open | target_node_pubkey?, asset_liquidity, min_dispute_period_secs | Open a new channel and provision liquidity |
deposit | channel_id, asset_liquidity | Deposit into an existing channel |
deposit_any | target_node_pubkey?, asset_liquidity, min_dispute_period_secs | Deposit into any suitable channel; otherwise open a new one |
open_or_deposit | target_node_pubkey?, asset_liquidity, min_dispute_period_secs | Same 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:
| Field | Type | Description |
|---|---|---|
server_amount | DecimalString | Provider-side contribution |
client_amount | DecimalString | Local/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.
| Variant | Fields | Purpose |
|---|---|---|
withdraw | channel_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_close | channel_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:
| Field | Type | Description |
|---|---|---|
server_amount | Amount (optional) | Releases the service's own balance to the service wallet |
client_amount | Amount (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_idisoptional, 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.
optionalis the same bytes on the wire as an unsetstring, 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.
| Variant | Fields | When to use |
|---|---|---|
onchain_fee_payment | OnchainFeePayment (UTXO or Account flow) | Pay the service fee on chain |
offchain_fee_payment | OffchainFeePayment (empty) | Pay via invoice / offchain payment |
dual_fund_fee_payment | DualFundFeePayment (empty) | Settle the fee inside the dual-fund flow (provisioning RPCs only) |
sponsored_deposit_fee_payment | SponsoredDepositFeePayment (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_payment | SponsoredWithdrawalFeePayment (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):
| Variant | Fields |
|---|---|
utxo | refund_address |
account | sender_address, refund_address? |
Fee methods are not interchangeable across the three operations:
Operation Accepts SubmitChannelLiquidityRequestonchain·offchain·dual_fund·sponsored_depositSubmitChannelReleaseRequestonchain·offchain·sponsored_withdrawalSubmitChannelLeaseExtensionRequestonchain·offchainThe 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 isEXIT_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 closesRelease 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 allis 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
UpdateChannelBalanceCredit— a withdrawal that hands part of itself to the peer. The swap-flow equivalent isEXIT_RAIL_LIQUIDITY_SERVICE; the entry-side counterpart isSponsoredDepositFeePayment.
Quotes: pinning an estimated price
Added 2026-09-11.
Every Estimate*Fee call returns a quote alongside the fee:
| Field | Type | Description |
|---|---|---|
fee | DecimalString | The estimated fee, in the asset named by payment_asset_id |
quote_id | string (optional) | Names the quote this fee was read from |
valid_until | Timestamp (optional) | (2026-10-14) When the quote stops being honoured |
valid_until_timestamp_seconds | uint64 (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_secsis 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.
- Submit.
SubmitChannelLiquidityRequest,SubmitChannelReleaseRequestorSubmitChannelLeaseExtensionRequesthas 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, orfee_settlement_failedwith why. That normally takes seconds — on the on-chain rail, settled means your node broadcast the fee transfer, not that it confirmed. - 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 onSubscribeLiquidityRequests, until it is final:completed,failed,unresolvedorexpired.
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_settledis not done. On the on-chain rail theSubmit*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 forcompletedbefore you rely on the channel, and read what the service did from itsresult.
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 yet | The request is submitted under it |
| names this same request — every field the same | The request already made, as it stands now. Nothing is submitted again, and nothing is paid again |
| names a different request | ALREADY_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 under | One 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*FeeRPCs ignore it. The deprecatedRequest*RPCs refuse a request that carries it, withINVALID_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.GetLiquidityRequestby the id reads it without submitting, but aNOT_FOUNDright 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 thepayment_uuidin the answer you lost.Resend the request exactly as you first sent it — the same
quote_idtoo. A retry that re-estimates first carries a newquote_id, which makes it a different request, and the id refuses it withclient_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.
| Field | Type | Description |
|---|---|---|
payment_uuid | string | The service's id for the request: the payment_uuid of its fee payment in GetFeePayments |
client_request_id | string (optional) | Your name for the request, when it was submitted under one |
network | Network | The network of the channels the request operates on |
operation | FeePaymentOperation | What the request asks of the service, as it was committed — the shape GetFeePayments reports |
rail | FeePaymentRail | How the fee is paid: ONCHAIN / OFFCHAIN / DUAL_FUND / SPONSORED_DEPOSIT / SPONSORED_WITHDRAWAL |
payment_network | Network | The network the fee is billed on — may differ from network |
payment_asset_id | string | The asset the fee is billed in |
fee | DecimalString | What the service bills, in whole units of payment_asset_id |
fee_deadline | Timestamp | Until 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_settlement | LiquidityFeeTransfer (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_at | Timestamp | When the service committed to the request, by your node's clock |
updated_at | Timestamp | When the request last changed |
state | one of | Where the request stands — see Request states |
Request states
Exactly one state is set. completed, failed, unresolved and expired are final.
| State | Final | Fields | Meaning |
|---|---|---|---|
awaiting_fee | no | — | 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_settled | no | — | 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_failed | no | reason | Your 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 |
completed | yes | result (optional) | The service carried the request out. result is what it reports it did — below |
failed | yes | reason, 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 |
unresolved | yes | reason (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 |
expired | yes | — | 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:
| Variant | Fields | Reports |
|---|---|---|
channel_liquidity | channel_id, txid | A 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_release | channel_id, txid | A release: the channel and its withdrawal or cooperative close transaction |
channel_lease_extension | channel_id, asset_id, lease_expiry | A 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.
| Field | Type | Description |
|---|---|---|
amount | DecimalString | What 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 |
transfer | LiquidityFeeTransfer (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:
| Variant | Fields | Description |
|---|---|---|
onchain | txid | A transaction on chain |
offchain | payment_id | A 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.
completedwithout aresult,failedorunresolvedwithout areasonThese 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
resultits fee payment recorded (FeePayment.results), and iscompletedwithout one only when the service recorded none. A request that failed, or stopped at an operation whose outcome is unknown, meanwhile has noreason: the service's words never reached your node. Read the channel (watchOnlyNode.GetChannel) for what changed.
fee_settlement_failedis 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.
unresolvedmeans 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:
| Name | Type | Required | Description |
|---|---|---|---|
client_request_id | string | no | (2026-10-14) Your own name for the request on this network, 1 to 64 characters — see client_request_id |
Response: SubmitLiquidityRequestResponse
| Field | Type | Description |
|---|---|---|
request | LiquidityRequest | The 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_payment | fee_settled means | Then the service |
|---|---|---|
onchain_fee_payment | Your node broadcast the fee transfer — seconds, not its confirmations | Carries the request out once the transfer has the confirmations its network requires: minutes on Ethereum |
offchain_fee_payment | Your node sent the invoice payment | Carries the request out at once |
dual_fund_fee_payment | Your node confirmed the funding allowance | Broadcasts the round at once. Its fee is carved out of the round: nothing leaves a wallet as a fee |
sponsored_deposit_fee_payment | Your node handed over the token permits | The same |
sponsored_withdrawal_fee_payment (release only) | Your node confirmed the funding allowance | The 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 sameclientRequestIdit 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:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | The network the request was submitted on |
id | one of | YES | payment_uuid (string), the service's id for the request, or client_request_id (string), your name for it |
Response:
| Field | Type | Description |
|---|---|---|
request | LiquidityRequest | The 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:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | The network whose requests to follow |
Stream item: LiquidityRequest
Waiting for one request
The race-free way to wait for a request you submit:
- Open the subscription first, and wait until it is in place: the stream's response headers over gRPC, the subscription id over JSON-RPC.
- Submit the request.
- Wait on the stream for the submitted
payment_uuidto reach a final state. Keep the events that arrive before theSubmit*call answers: the request'sawaiting_feeandfee_settledusually 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
GetLiquidityRequestonce 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:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Channel network |
operation | ChannelLiquidityRequestOperation | YES | Provisioning action (open / deposit / deposit_any / open_or_deposit) |
lease_duration_seconds | uint64 | Conditional | Required 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_network | Network | YES | Network used to pay the service fee |
payment_asset_id | string | YES | Asset used to pay the service fee |
fee_payment | one of OnchainFeePayment / OffchainFeePayment / DualFundFeePayment / SponsoredDepositFeePayment | YES | Exactly one |
quote_id | string | no | (2026-09-11) The quote an earlier estimate of this same request returned — see Quotes. Absent prices the request at execution |
Response:
| Field | Type | Description |
|---|---|---|
txid | string | Funding transaction ID |
channel_id | string | Channel ID of the (new or updated) channel |
fee | DecimalString | (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
opento lease only the new amount, in a channel of its own.
Method: EstimateRequestChannelLiquidityFee
Parameters: Same as Request Channel Liquidity.
Response:
| Field | Type | Description |
|---|---|---|
fee | DecimalString | Estimated service fee, denominated in the asset identified by payment_asset_id |
quote_id | string (optional) | (2026-09-11) Pass this back on the matching request to be billed at fee — see Quotes |
valid_until | Timestamp (optional) | (2026-10-14) When the quote stops being honoured |
valid_until_timestamp_seconds | uint64 (optional) | (2026-09-11) Deprecated (2026-10-14): read valid_until. The same instant, in Unix seconds |
lease_duration_seconds | uint64 (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:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Channel network |
operation | ChannelReleaseOperation | YES | withdraw or cooperative_close |
payment_network | Network | YES | Network used to pay the service fee |
payment_asset_id | string | YES | Asset used to pay the service fee |
fee_payment | one of OnchainFeePayment / OffchainFeePayment / SponsoredWithdrawalFeePayment | YES | Exactly one. SponsoredWithdrawalFeePayment is release-only — see above |
quote_id | string | no | (2026-09-11) The quote an earlier estimate of this same request returned — see Quotes |
Response:
| Field | Type | Description |
|---|---|---|
txid | string | Release transaction ID |
fee | DecimalString | (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:
| Field | Type | Description |
|---|---|---|
fee | DecimalString | Estimated service fee |
quote_id | string (optional) | (2026-09-11) Pass this back on the matching request to be billed at fee — see Quotes |
valid_until | Timestamp (optional) | (2026-10-14) When the quote stops being honoured |
valid_until_timestamp_seconds | uint64 (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_expiryis the lease you paid for.AssetChannel.liquidity_expiryis 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'sliquidity_expirykeeps 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 everywatchOnlyNode.GetChannels/GetChannelread, andliquidity.GetLeaseExpiriesreturns them for all channels at once. Extend whenliquidity_expiryis 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'slease_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 whoseliquidity_expiryis 18:00 extends by one hour to 16:00, for one hour of rent, andliquidity_expirythen shows the later of 16:00 and what usage still buys.
Method: RequestChannelLeaseExtension
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Channel network |
channel_id | string | YES | Channel whose lease to extend |
asset_id | string | YES | Specific asset whose lease is being extended |
lease_extension_seconds | uint64 | YES | Time to add to the lease's expiry (AssetChannel.lease_expiry), rounded up to whole slots (lease_slot_secs) — see above |
payment_network | Network | YES | Network used to pay the service fee |
payment_asset_id | string | YES | Asset used to pay the service fee |
fee_payment | one of OnchainFeePayment / OffchainFeePayment | YES | Exactly one. The dual-fund and sponsored methods are not valid here |
quote_id | string | no | (2026-09-11) The quote an earlier estimate of this same request returned — see Quotes |
Response:
| Field | Type | Description |
|---|---|---|
channel_id | string | Channel that was extended |
asset_id | string | Asset whose lease was extended |
expiry_timestamp_seconds | int64 | The 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 |
fee | DecimalString | (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:
| Field | Type | Description |
|---|---|---|
fee | DecimalString | Estimated service fee |
quote_id | string (optional) | (2026-09-11) Pass this back on the matching request to be billed at fee — see Quotes |
valid_until | Timestamp (optional) | (2026-10-14) When the quote stops being honoured |
valid_until_timestamp_seconds | uint64 (optional) | (2026-09-11) Deprecated (2026-10-14): read valid_until. The same instant, in Unix seconds |
lease_extension | LeaseExtensionTerms (optional) | What the extension buys — below |
LeaseExtensionTerms — every instant is a slot boundary, and every duration a whole number of slots:
| Field | Type | Description |
|---|---|---|
extension_seconds | uint64 | The extension asked for, rounded up to whole slots |
starts_at | Timestamp | (2026-10-14) Where it counts from: the lease's expiry (AssetChannel.lease_expiry), or the next slot boundary once it has passed |
expiry | Timestamp | (2026-10-14) The lease expiry after the extension: extension_seconds past starts_at |
rented_seconds | uint64 | The lease time fee pays rent for, from starts_at to expiry: extension_seconds |
from_timestamp_seconds | uint64 | Deprecated (2026-10-14): read starts_at. The same instant, in Unix seconds |
expiry_timestamp_seconds | uint64 | Deprecated (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:
| Field | Type | Description |
|---|---|---|
min_duration_secs / max_duration_secs | uint64 | The lease-duration window the service accepts |
lease_slot_secs | uint64 | The 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_usd | DecimalString | Capacity bounds, in USD |
asset_configs | LiquidityAssetConfig[] | Per-asset fee configuration — see below |
node_pubkeys | LiquidityNetworkNode[] | { network, node_pubkey } — the provider's node id per network |
quote_validity_secs | uint64 | How long every quote the service issues stays valid — see Quotes |
fee_payment_assets | LiquidityFeePaymentAsset[] | (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:
| Field | Type | Description |
|---|---|---|
network | Network | (2026-10-14) The network the asset is leased on |
asset_id | string | The asset |
fee_ratio_per_hour | DecimalString | Hourly fee ratio — "0.001" is 0.1% per hour |
pricing_ticker | string (optional) | External pricing ticker used for the USD conversion |
protocol | Protocol | Deprecated (2026-10-14): read network |
network_id | string | Deprecated (2026-10-14): read network |
node_pubkeysis 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):
| Field | Type | Description |
|---|---|---|
network | Network | The network the fee is paid on |
asset_id | string | The asset the fee is paid in, as the service spells it |
rails | LiquidityFeeRails | The 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):
| Field | Type | Description |
|---|---|---|
on_chain_cost_multiple | DecimalString | Every 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_multiple | DecimalString | The force-close cost each funded channel is billed for, at this multiple of its estimate |
min_fee_usd | DecimalString | The 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:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Channel network |
asset_id | string | YES | Asset to lease |
Response: info (LeaseableAssetInfo):
| Field | Type | Description |
|---|---|---|
available_liquidity | DecimalString | What the provider can lease of this asset right now |
min_capacity / max_capacity | DecimalString | Per-lease size bounds, in the asset's own units |
min_duration_secs / max_duration_secs | uint64 | Duration bounds for this asset |
fee_ratio | DecimalString | The fee ratio applied to this asset |
Sizing a lease against
available_liquidityis what avoids thelease_too_bigestimate variant — that variant reports the same three numbers after the fact.
Get Leases
The caller's active leases on a network.
Method: GetLeases
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Network to list leases for |
Response: leases (AssetChannelLease[]):
| Field | Type | Description |
|---|---|---|
network | Network | The network the lease is on |
channel_id | string | The leased channel |
asset_id | string | The leased asset |
expiry | Timestamp (optional) | When the lease you paid for ends, on a slot boundary. Absent when the asset channel carries no lease |
liquidity_expiry | Timestamp (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:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Network to read expiries for |
Response:
| Field | Type | Description |
|---|---|---|
channels | map<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 providerThat makes it cheap enough to poll on a UI timer, and it is the right call for "which of my leases are running down".
GetLeasesasks 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:
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | The network the payments were committed on — the channel network, not the one their fee was billed on |
statuses | repeated FeePaymentStatus | no | Restrict to these lifecycle states. Empty returns every state |
created_after | Timestamp | no | (2026-10-14) Only payments committed at or after this instant. Absent reaches back to the first payment |
created_after_timestamp_seconds | uint64 | no | Deprecated (2026-10-14): set created_after. The same bound, in Unix seconds. A request that sets both is refused with INVALID_ARGUMENT |
before | FeePaymentCursor | no | Return only payments strictly older than this cursor. Absent starts at the newest |
limit | uint32 | no | Page size, clamped to 200. 0 asks for the service's default (50) |
Response:
| Field | Type | Description |
|---|---|---|
payments | repeated FeePayment | Newest first |
next | FeePaymentCursor | Pass back as before for the next page. Absent when the history ends |
FeePaymentCursor
| Field | Type | Description |
|---|---|---|
created_at_timestamp_micros | int64 | The last returned payment's creation instant, in microseconds since the Unix epoch |
payment_uuid | string | The last returned payment's id |
Treat it as opaque — pass back the
nextyou 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
| Field | Type | Description |
|---|---|---|
payment_uuid | string | Names this payment — the same id the service's updates carry |
network | Network | The network whose channels the operations touched |
operations | repeated FeePaymentOperation | What the payment covered — see below |
status | FeePaymentStatus | PENDING / PROCESSING / COMPLETED / EXPIRED / FAILED / REFUNDED |
rail | FeePaymentRail | ONCHAIN / OFFCHAIN / DUAL_FUND / SPONSORED_DEPOSIT / SPONSORED_WITHDRAWAL |
payment_network | Network | The network the fee was billed on — may differ from network |
payment_asset_id | string | The asset the fee was billed in |
fee | DecimalString | What was billed, in whole units of payment_asset_id |
refund | LiquidityFeeRefund | (2026-10-14) What the service gave back of the fee, and how it reached this wallet. Present only on a refunded payment |
created_at | Timestamp | (2026-10-14) When the service committed to this payment |
fee_deadline | Timestamp | (2026-10-14) Until when the service waited for this wallet's part of the fee |
outcome_unknown | bool | (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 |
results | repeated 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_amount | DecimalString | Deprecated (2026-10-14): read refund. What was given back |
refund_tx_hash | string | Deprecated (2026-10-14): read refund. The refund's transaction or off-chain payment, without saying which |
created_at_timestamp_seconds | uint64 | Deprecated (2026-10-14): read created_at. The same instant, in Unix seconds |
expires_at_timestamp_seconds | uint64 | Deprecated (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:
| Variant | Type | Description |
|---|---|---|
provision | { operation: ChannelLiquidityRequestOperation, lease_duration_seconds } | An open, deposit, deposit_any or open_or_deposit, with the lease window it bought |
release | ChannelReleaseOperation | A withdraw or a cooperative_close. Buys no lease time |
lease_extension | ChannelLeaseExtensionOperation | { 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 offsetPass the response's
nextback asbefore, 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.
| Duration | Seconds | Calculation |
|---|---|---|
| 1 hour | 3,600 | 60 × 60 |
| 1 day | 86,400 | 24 × 60 × 60 |
| 1 week | 604,800 | 7 × 24 × 60 × 60 |
| 30 days | 2,592,000 | 30 × 24 × 60 × 60 |
| 90 days | 7,776,000 | 90 × 24 × 60 × 60 |
const days = 30
const leaseSeconds = days * 24 * 60 * 60 // 2,592,000
Choosing a fee payment method
| Variant | Available on | When to use |
|---|---|---|
offchain_fee_payment | Liquidity / Release / Lease Extension | You 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 protocols | You'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 protocols | You're paying the fee from an EVM account; refund_address is optional |
dual_fund_fee_payment | Liquidity only | You're contributing client-side funds (client_amount > 0) and want the fee settled inside the dual-fund flow |
sponsored_deposit_fee_payment | Liquidity 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_payment | Release 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 Code | Description | Solution |
|---|---|---|
INVALID_ARGUMENT | Missing lease_duration_seconds while server_amount > 0, or no fee_payment variant set | Provide a duration when leasing; pick exactly one fee payment variant |
INVALID_ARGUMENT | dual_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 N | Send 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* RPC | Call 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 names | On 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 network | Check 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 made | Submit again under the same client_request_id, or read it back with GetLiquidityRequest — see Deadline |
FAILED_PRECONDITION | The quote_id has expired, or no longer fits the request | Re-run the matching Estimate*Fee, show the user the new figure, and send the fresh quote_id — see Quotes |
RESOURCE_EXHAUSTED | The 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 now | Lease less, or let a lease lapse first; for a sponsored rail, try again later or pay the fee another way |
FAILED_PRECONDITION | Channel does not exist, asset not present in channel, or release/extension not permitted | Verify 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 accepts | Pay 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 confirmed | Wait 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 hub | Wait 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 names | Close the channel from your own node, or release it once that time has passed — see above |
UNAVAILABLE | Liquidity service temporarily unavailable | Retry with exponential backoff |
Best Practices
- Submit with a
client_request_id, and follow the request. (2026-10-14) Use theSubmit*RPCs, not the deprecatedRequest*ones. Name every request, openSubscribeLiquidityRequestsbefore you submit, and act on the request's final state — not on theSubmit*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. - Always estimate first. Call the matching
Estimate*RPC before the operation. - Provide
lease_duration_secondsonly when needed. Required if any asset hasserver_amount > 0; otherwise omit. - Pick the right fee payment. Offchain is cheapest; dual-fund and sponsored-deposit are only valid on a channel liquidity request.
- Pass the
quote_idfrom your estimate. Without it the service prices the request at execution, and the user is billed a figure they never saw.
- Pass the
- Reuse channels when possible.
deposit/deposit_any/open_or_depositavoid the cost of opening a new channel. - Treat
lease_extension_secondsas a delta, not a target expiry. It counts fromlease_expiryand is sold in whole slots; offer durations in multiples oflease_slot_secs. - Watch
AssetChannel.liquidity_expiryto know when the provider takes its liquidity back.lease_expiryis 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, andliquidity_expiryshows until when. Read both on channel reads (orliquidity.GetLeaseExpiries) and extend only whenliquidity_expiryis 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. - On
withdraw, name the side.asset_amountsneedsserver_amountand/orclient_amountper asset — at least one — since 2026-07-23.