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. Hydra App completes fee settlement and any required local confirmations internally before returning the final operation result.
JSON-RPC namespace: liquidity
Endpoints
- Request Channel Liquidity
- Estimate Request Channel Liquidity Fee
- Request Channel Release
- Estimate Request Channel Release Fee
- Request Channel Lease Extension
- Estimate Request Channel Lease Extension Fee
Read-only discovery
Common types
ChannelLiquidityRequestOperation (one of)
Used by RequestChannelLiquidity and its 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 RequestChannelRelease and its 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.
Fee payment (one of)
All RPCs that take a fee require exactly one fee_payment variant.
| 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 RequestChannelLiquidityonchain·offchain·dual_fund·sponsored_depositRequestChannelReleaseonchain·offchain·sponsored_withdrawalRequestChannelLeaseExtensiononchain·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 isEXIT_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 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_seconds | uint64 (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_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.
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:
| 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_seconds | uint64 (optional) | (2026-09-11) When the quote stops being honoured (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...",
"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:
| 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_seconds | uint64 (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_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. Usage keeps a channel's liquidity in place on its own, so an actively-traded channel'sliquidity_expiryruns ahead of its lease — a market maker routing volume through a channel typically pays only the initial lease. 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_seconds | uint64 (optional) | (2026-09-11) When the quote stops being honoured (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 |
from_timestamp_seconds | uint64 | Where it counts from (Unix seconds): the lease's expiry (AssetChannel.lease_expiry), or the next slot boundary once it has passed |
expiry_timestamp_seconds | uint64 | The lease expiry after the extension (Unix seconds): from_timestamp_seconds + extension_seconds |
rented_seconds | uint64 | The 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:
| 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 |
LiquidityAssetConfig:
| Field | Type | Description |
|---|---|---|
protocol | Protocol | Protocol family of the network |
network_id | string | Network identifier |
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 |
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.
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. 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_seconds | uint64 | no | Only payments committed at or after this instant. Absent reaches back to the first payment |
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_amount | DecimalString | What was given back. Present only on a refunded payment |
refund_tx_hash | string | The transaction that carried the refund, when there was one |
created_at_timestamp_seconds | uint64 | When the service committed to this payment |
expires_at_timestamp_seconds | uint64 | The deadline this wallet had to complete its half |
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_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 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
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 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 RequestChannelLiquidity | Switch to onchain_fee_payment or offchain_fee_payment |
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 | Insufficient liquidity available on the provider side | Reduce server_amount or retry later |
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 |
UNAVAILABLE | Liquidity service temporarily unavailable | Retry with exponential backoff |
Best Practices
- 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
RequestChannelLiquidity.- 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, andliquidity_expiryshows until when. Read both on channel reads (orliquidity.GetLeaseExpiries) and extend only whenliquidity_expiryis genuinely running down. - On
withdraw, name the side.asset_amountsneedsserver_amountand/orclient_amountper asset — at least one — since 2026-07-23.