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_liquidityOpen a new channel and provision liquidity
depositchannel_id, asset_liquidityDeposit into an existing channel
deposit_anytarget_node_pubkey?, asset_liquidityDeposit into any suitable channel; otherwise open a new one
open_or_deposittarget_node_pubkey?, asset_liquiditySame as deposit_any but biases toward opening

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

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_idAssetWithdrawAmounts:

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 empty on fee estimates only (EstimateRequestChannelReleaseFee), and every requested side must then be an exact amount.

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
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

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.

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)

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

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.

Check the expiry before paying to extend. Leases extend automatically as a channel is used, so an actively-traded channel's expiry moves forward on its own — a market maker routing volume through a channel typically pays only the initial lease. Read the current expiry from AssetChannel.lease_expiry (on every watchOnlyNode.GetChannels / GetChannel read, added 2026-07-12), and extend manually only when it is actually running down. liquidity.GetLeaseExpiries returns the same information for all channels at once.

Method: RequestChannelLeaseExtension

Parameters:

NameTypeRequiredDescription
networkNetworkYESChannel network
channel_idstringYESChannel whose lease to extend
asset_idstringYESSpecific asset whose lease is being extended
lease_extension_secondsuint64YESDelta to add to the current expiry
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_secondsint64New absolute expiry (Unix seconds)

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.

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)

Example:

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

Example Response:

{ "fee": "0.00002" }

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: GetLiquidityServiceInfoParameters: None.

Response:

FieldTypeDescription
min_duration_secs / max_duration_secsuint64The lease-duration window the service accepts
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 expires. Absent means not yet calculated, not "never expires"

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: map<asset_id, Timestamp> }

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 extends a lease on its own, so an actively-traded channel's expiry moves forward without you paying anything. Watch this rather than counting down from the duration you bought.


Duration Reference

lease_duration_seconds and lease_extension_seconds are expressed in seconds.

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
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.
  6. Watch AssetChannel.lease_expiry instead of assuming the duration you paid for. Usage extends a lease automatically, so the expiry moves; poll it (or liquidity.GetLeaseExpiries) and extend only when it 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