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. Hydra App completes fee settlement and any required local confirmations internally before returning the final operation result.

JSON-RPC namespace: liquidity

Endpoints

Read-only discovery


Common types

ChannelLiquidityRequestOperation (one of)

Used by RequestChannelLiquidity and its 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 RequestChannelRelease and its 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.

Fee payment (one of)

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

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
RequestChannelLiquidityonchain · offchain · dual_fund · sponsored_deposit
RequestChannelReleaseonchain · offchain · sponsored_withdrawal
RequestChannelLeaseExtensiononchain · offchain

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. RequestChannelRelease only.

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_until_timestamp_secondsuint64 (optional)When the quote stops being honoured (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_timestamp_seconds }
                        │
      show `fee` to the user, they accept
                        │
                        ▼
Request*      ──▶  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.


Request Channel Liquidity

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_until_timestamp_secondsuint64 (optional)(2026-09-11) When the quote stops being honoured (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...",
  "validUntilTimestampSeconds": "1789012345"
}

Request Channel Release

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_until_timestamp_secondsuint64 (optional)(2026-09-11) When the quote stops being honoured (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

Extend the duration of an existing lease on a specific channel asset. 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. Usage keeps a channel's liquidity in place on its own, so an actively-traded channel's liquidity_expiry runs ahead of its lease — a market maker routing volume through a channel typically pays only the initial lease. 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_until_timestamp_secondsuint64 (optional)(2026-09-11) When the quote stops being honoured (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
from_timestamp_secondsuint64Where it counts from (Unix seconds): the lease's expiry (AssetChannel.lease_expiry), or the next slot boundary once it has passed
expiry_timestamp_secondsuint64The lease expiry after the extension (Unix seconds): from_timestamp_seconds + extension_seconds
rented_secondsuint64The lease time fee pays rent for, from from_timestamp_seconds to the new expiry: extension_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",
  "validUntilTimestampSeconds": "1790330220",
  "leaseExtension": {
    "extensionSeconds": "3600",
    "fromTimestampSeconds": "1790359200",
    "expiryTimestampSeconds": "1790362800",
    "rentedSeconds": "3600"
  }
}

Get Liquidity Service Info

Server-wide bounds, durations, per-asset fee configuration, and the provider's node public key on each network. Read-only, no side effects — call it once at start and cache it.

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

LiquidityAssetConfig:

FieldTypeDescription
protocolProtocolProtocol family of the network
network_idstringNetwork identifier
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

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.


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. 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_after_timestamp_secondsuint64noOnly payments committed at or after this instant. Absent reaches back to the first payment
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
refund_amountDecimalStringWhat was given back. Present only on a refunded payment
refund_tx_hashstringThe transaction that carried the refund, when there was one
created_at_timestamp_secondsuint64When the service committed to this payment
expires_at_timestamp_secondsuint64The deadline this wallet had to complete its half

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_amount says how much. FAILED is the one to reconcile by hand: the operations did not complete and no refund was made.

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

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
) {
  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())

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

  // 2. Execute
  const result = await client.requestChannelLiquidity(request, {})
  return {
    channelId: result.getChannelId(),
    txid: result.getTxid()
  }
}

Cooperatively close a channel through the service

async function cooperativeClose(
  client: LiquidityServiceClient,
  network: { protocol: number; id: string },
  channelId: string,
  paymentAssetId: string
) {
  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())

  const result = await client.requestChannelRelease(request, {})
  return result.getTxid()
}

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 RequestChannelLiquiditySwitch to onchain_fee_payment or offchain_fee_payment
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_EXHAUSTEDInsufficient liquidity available on the provider sideReduce server_amount or retry later
FAILED_PRECONDITIONChannel does not exist, asset not present in channel, or release/extension not permittedVerify channel_id and asset_id against watchOnlyNode.GetChannel
UNAVAILABLELiquidity service temporarily unavailableRetry with exponential backoff

Best Practices

  1. Always estimate first. Call the matching Estimate* RPC before the operation.
  2. Provide lease_duration_seconds only when needed. Required if any asset has server_amount > 0; otherwise omit.
  3. Pick the right fee payment. Offchain is cheapest; dual-fund and sponsored-deposit are only valid on RequestChannelLiquidity.
    • 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.
  4. Reuse channels when possible. deposit / deposit_any / open_or_deposit avoid the cost of opening a new channel.
  5. 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.
  6. 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, and liquidity_expiry shows until when. Read both on channel reads (or liquidity.GetLeaseExpiries) and extend only when liquidity_expiry is genuinely running down.
  7. 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