Changelog
Changes to the .proto definitions and to the config.yaml schema, newest first. Dates are the dates the change was published here, which is what a client integrating against these docs can act on — several changes made at different times upstream land under one date when they are documented together.
The proto files served from
/protoand the per-service docs on this site track these changes. When in doubt, the.protofiles are the schema of record, and the liverpc.discovermethod on a running Hydra App is authoritative for what that build actually serves.
2026-09-11 — A generic channel update
Node — two new RPCs, UpdateChannel(network, channel_id, asset_updates, fee_option) → { txid } and EstimateUpdateChannelFee → { fee }, taking a ChannelUpdateAmount per asset. DepositChannel and WithdrawChannel are unchanged — they are the two shapes of this common enough to deserve their own call; UpdateChannel reaches every other combination, including an exit that carves its own fee out of what it withdraws.
New in balance.proto:
ChannelContribution—oneof { deposit: Amount | withdraw: Amount }, what one side's wallet contributes. Unset means that side's wallet does not move, which costs less gas than naming a zero.BalanceCredit—oneof { to_peer: DecimalString | to_self: DecimalString }, balance crossing between the sides beyond what either wallet moved. It is settled in the co-signed state that follows the transaction: it touches no wallet and reaches no chain.ChannelUpdateAmount—{ self_contribution, peer_contribution, credit }, all three independently optional.
Amount.all resolves per direction: against that side's channel balance for withdraw, against the local wallet for deposit — so deposit.all is rejected on peer_contribution, where that wallet is not visible. On a side that also pays a credit, withdraw.all resolves to its withdrawable balance minus the credit.
The two contributions must not cancel: the chain refuses an update that leaves the channel holding what it already held. Contributions that cancel are a payment, not an update.
Node — BatchChannelOperation gains a seventh arm, update (7), carrying BatchUpdateChannel { channel_id, asset_updates }, so the atomic path is not a step behind the single one.
Node — no shape change to FundingAllowance, but allowed_payment now bounds more than it did: every way your balance can cross to a peer without a payment of your own, including the BalanceCredit in a co-signed update. The field keeps its name and number, so nothing to migrate — but an allowance sized only against deposits is now sized against the wrong thing.
See Node → Update Channel and Set Funding Allowance.
2026-09-11 — Token permits: an allowance without gas
A token allowance can now be authorised by an off-chain signature and submitted by somebody else, who pays the fee. That is what lets a wallet holding only a token — no native asset at all — grant an allowance, fund a channel, and swap.
Allowance — new message TokenPermit: { token_id, owner, spender, value (U256String, smallest unit), deadline (unix seconds), terms (bytes), signature (bytes) }.
Blockchain — new RPC GetTokenPermitTerms(network, token_id, owner) → { terms?: bytes }. Returns what the token discloses before an owner signs — the bytes that go into TokenPermit.terms — or nothing when the token offers no signature-authorised allowance to that owner. Reading the terms is how a caller learns whether the permit path exists at all. The terms fold in the owner's current permit nonce, so read them immediately before signing.
Client — three new RPCs for the submitting side:
EstimateTokenPermitFee(network, asset_id, owner, spender, value, fee_option)→{ fee }. What submitting would cost this wallet, priced against the owner's live allowance and nonce. Takes no signature — the permit does not exist yet; the owner signs once the price is known.valuehere is a whole-unitDecimalString, unlikeTokenPermit.value.CreateTokenPermitTransaction(network, permit, fee_option)→{ transaction_request }— the unsigned submission, for an external signer.SubmitTokenPermit(network, permit, fee_option)→{ txid }— create, sign and broadcast in one call.
Swap — new enum DepositRail (UNSPECIFIED / LOCAL / LIQUIDITY_SERVICE), carried as SendingChannelDeposit.rail (7): who broadcasts the sending side's channel funding and pays its gas. The entry-side counterpart of ExitRail. New SimpleSwapUpdate variant funding_sending_channel_via_liquidity_service (35) — { fee, fee_payment_currency } — emitted when the deposit takes the service rail.
Liquidity — new fee-payment variant sponsored_deposit_fee_payment (9) on RequestChannelLiquidityRequest: the service funds the channel from this wallet's tokens on its own transaction, authorised by a permit Hydra App signs internally, and takes its fee out of the deposit. Provisioning only.
Wallet — new RPC RescanTransactions(network, from_block?) → { from_block, to_block, adopted, skipped }. Operator-driven recovery for a transaction the live subscriptions never delivered: the ordinary sync reaches back only a confirmation window, so anything older is never revisited on its own. Balances are read from the chain and are unaffected. skipped > 0 is what says the range was not fully adopted — it does not end the pass.
See Client → Token permits, Blockchain → Get Token Permit Terms, Swap → Deposit rails, Lease → SponsoredDepositFeePayment and Wallet → Rescan Transactions.
2026-09-11 — A release rail that needs no gas behind it
Liquidity — new fee-payment variant sponsored_withdrawal_fee_payment (8) on RequestChannelReleaseRequest. 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 internally.
This closes a gap where a channel could be stranded: release offered on-chain payment, which needs a funded wallet, and off-chain payment, which needs the channel balance that withdraw all is about to take away. A client holding a balance and no gas could use neither.
The wallet never pays a fee here, so the flow takes the funding-allowance confirmation path a dual-funded open takes rather than the settlement path — and a request that is never confirmed takes its allowance back rather than leaving consent standing.
Release-only.
RequestChannelLiquidityhas its ownsponsored_deposit_fee_payment;RequestChannelLeaseExtensionaccepts neither.
See Lease → SponsoredWithdrawalFeePayment.
2026-09-11 — An estimate that refuses for gas, not for liquidity
Swap — new SimpleSwapEstimate variant insufficient_native_balance (8) — { required, available }. The wallet holds the sending asset but cannot pay the sending network's native gas for the funding transaction the swap needs.
Gas and the sending asset are separate resources, so this is neither insufficient_sending_balance (the wallet is not short of what it is selling) nor no_liquidity (the market can serve the pair). Nothing about the swap has to change to clear it: fund the wallet with required and estimate again. required includes the broadcast envelope the chain's admission check reserves on top of the effective fee — a balance that merely equals the expected fee is still rejected — so topping up to exactly that figure is what clears it.
Only a funded deposit or channel open raises it: a swap served entirely from channel balances signs nothing on chain, and a native-asset sender short of gas is short of what it is sending, which is insufficient_sending_balance.
Add a branch for it. Without one it falls through your default case and you will report "no liquidity" for a wallet that just needs gas.
See Swap → InsufficientNativeBalance.
2026-09-11 — Lease quotes: pin the price you showed
Liquidity — EstimateRequestFeeResponse gained quote_id (2) and valid_until_timestamp_seconds (3). Pass quote_id back on the matching request — RequestChannelLiquidityRequest.quote_id (10), RequestChannelReleaseRequest.quote_id (7), RequestChannelLeaseExtensionRequest.quote_id (9) — and the service bills at that quote's price for as long as the quote is valid.
A quote that has expired, or that no longer fits the request, causes the request to be refused rather than silently re-priced: the user agreed to a figure, and a request that can no longer honour it should come back for a fresh estimate instead of quietly costing more. Omitting quote_id keeps the old behaviour — the service prices the request when it executes.
GetLiquidityServiceInfoResponse gained quote_validity_secs (7), so a client can size its confirmation window before asking for a quote.
A quote fixes the price, not the capacity. A lease that no longer fits the provider's available liquidity is refused even inside its validity window.
See Lease → Quotes.
2026-09-11 — Documentation corrections
No proto change. Three things this reference stated that the server does not do:
Config and endpoints, audited against the production deployment:
- Mainnet does use
alchemy_rpc/alchemy_ws— proxy-fronted, on both EVM networks. This page said "not used". They are now in the mainnet template, at the URLs the Hydranet node itself runs with. gossip_synchas a third variant,hybrid(RGS snapshot then live P2P), and no default — it is required and tagged.- Storage backends are now documented in full: fjall and redb tuning keys with their defaults,
durability: immediate | bufferedand whenbufferedis safe, the*_migrate_fromone-time migration fields, and the fact thatcompressiondefaults tononerather than zstd. server_portis optional, and omitting it starts no server at all. Both it andmetrics_portbind0.0.0.0.- Also filled in: Electrum
validate_tls, the Esplora variant's ownwaterfalls_url/proxy_auth,lithium.subgraph.ws_url/proxy_auth, the*_relay_urlwasm tunnelling fields, theapi_keyvscustom_urlforms of the Alchemy blocks, and thatbackup_configwithoutauthentication_configfails at boot.
The mainnet peer-port warning was right but explained wrong: the hub does still listen on 19736 / 29981 / 29983 at its origin for clients migrating off them — Cloudflare simply cannot proxy those ports, so the public hostname answers on 443 only.
Every mainnet service endpoint, Lithium contract address, deploy block, token contract and peer identity in the Setup Guide was checked against the production deployment and is correct.
- JSON-RPC field names are camelCase, not snake_case — responses always, requests accepting either.
DecimalString/U256Stringare{ "value": … }objects,uint64is a JSON string, and a field at its default is omitted rather than sent. See Wire encoding. - The by-name params envelope is
request, notreq.rpc.discoverhas always named it that way; thereqform never parsed. Positional params are unaffected. - Streaming works over JSON-RPC — as WebSocket subscriptions on the same URL, not only over gRPC. What fails is a one-shot HTTP POST, which cannot carry a stream. See Subscriptions over WebSocket.
Also corrected across the reference: Hashlock is a known/unknown oneof (not a payment_hash field); FeeOption is a oneof message (not a LOW/MEDIUM/HIGH enum) and Amount is a oneof (not { value, asset_id }); MatchedOrder carries swap_role, not is_taker; GetChannelsWithCounterparty takes counterparty_pubkey; GetTradeHistory returns trade_history; GetFeeEstimates returns a FeeEstimate of ChainFees; GetBlockHeader.block_number is required and its response is block_header with prev_hash. SignerService now has its own page, and SubscribeClientEvents / SubscribeNodeEvents have full event catalogues.
2026-09-04 — A discount per role (breaking)
Orderbook — the hub prices a client's two roles from two columns of its rung, so every discount figure that used to be one value is now two. A fee and a rebate move in opposite directions under the same discount, which is why one figure could not describe both.
MarketInfo.max_fee_discount(14) →max_taker_discount(14) +max_maker_discount(15).VolumeDiscountTier.discountandHoldingDiscountTier.discount→maker_discount(2) +taker_discount(3) on each.- The active-standing message was renamed
FeeDiscountStanding→ActiveFeeDiscount, and itscombined_discount(1) becametaker_discount(1) +maker_discount(2);volumeandholdingsrenumbered 2→3 and 3→4.VolumeDiscountStanding/HoldingDiscountStandingcarry the same pair per rung, plus anext_tierof the matching type. FeeDiscountStandingis now the name of the outer wrapper — theoneof { no_program | excluded | active }thatGetFeeDiscountResponse.standingand thefee_discount_updateevent both carry.GetFeeDiscountProgramsResponsegainedmax_taker_discount(3) /max_maker_discount(4) — the operator's default ceilings, which a market's own terms may replace.MarketFeeRatesgainedtaker_discount(5) /maker_discount(6), each already bounded by that market's own maximum.
The arithmetic is now stated on the wire: effective = listed − discount × |listed|. Applying it to a signed rate is what makes a fee shrink and a rebate deepen. The older listed × (1 − discount) form is wrong for a negative rate — it makes a rebate shallower.
The single-figure shapes (
max_fee_discount, a barediscountper rung,combined_discount) existed in the app source between 2026-08-24 and 2026-09-04 but never shipped in a published proto set — nothing served from/protoever carried them. If you generated stubs from a proto pulled straight out of the app source in that window, re-pull.
See Orderbook → Fee discounts.
2026-09-04 — Watchtower listing
Node — new read-only RPC GetConnectedWatchtowers(network) → node_ids[]: the watchtowers this node currently has a session with. The counterpart to ConnectToWatchtower, which until now you could call but never verify.
Config — each EVM / Tron network block gained a watchtowers list of <node_id>@<host>:<port> strings. The node dials them itself once its initial chain sync completes and keeps them attached, so coverage survives a restart without an operator re-issuing ConnectToWatchtower. A watchtower acts on evidence against a channel's counterparty, so it cannot defend a channel whose counterparty is the watchtower itself — coverage applies to channels with other peers.
See Node API → Get Connected Watchtowers and the Setup Guide.
2026-09-03 — Fee rates for this node
Orderbook — new RPC GetMarketFeeRates(first_currency, other_currency) → { fee_rates?: MarketFeeRates }: a market's four fee ratios with this node's own discounts already struck off, each role at its own figure, read from the same market copy the node's estimates price against. Absent when the pair is not a market.
MarketInfo keeps advertising list rates — the same for every client — which is what it is for. Prefer GetMarketFeeRates for anything that prices your own order: a fee shrinks with a discount while a rebate deepens, so deriving it from MarketInfo by hand gets the sign wrong.
Orderbook — DexEvent.update gained fee_discount_update (field 9), carrying the full FeeDiscountStanding. The hub opens the private stream with your standing and pushes the whole standing again whenever it moves, so silence means unchanged and the last message received is the one to hold. Nothing polls; a dropped stream heals by reconnecting into a fresh opening standing.
Swap / Orderbook (server-side) — estimates used to price every fill at list rates, so a node on any tier quoted itself a fee it would not be charged and a rebate it would not earn, with the error growing with the tier. Estimates now price this node's taker side at its own standing. Only the taker side: the makers an estimate walks belong to other clients, whose tiers are not knowable here. A market snapshot also carries the tier it was taken at, so a family of probes against one snapshot (a minimum, a maximum, a re-seeded range) stays one coherent answer.
No request/response shape change for estimates — but if you compared an estimate against a fee you computed from
MarketInfo, the two will now disagree whenever you hold a rung. The estimate is right.
2026-09-03 — Declared token symbols; oracle prices by deployment
Asset — AddTokenRequest gained optional symbol (3) and name (4): the ticker and name to show for a token in place of the ones its contract reports. Re-applied on every registration of the token; omit to keep the chain's own. A contract's symbol() is its deployer's choice — a bridged deployment often carries a variant of the asset's real ticker — while every consumer expects the asset's own identity. Decimals are never declared; they define the token's units and are only ever read from the chain.
Config — a tokens[] entry may now be either the bare id string it always was, or a map declaring the overrides:
tokens:
- "erc20:0xaf88d065e77c8cC2239327C5EDb3A432268e5831" # USDC — chain's own symbol
- id: "erc20:0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9"
symbol: USDT # contract reports "USD₮0"
name: "Tether USD"
A declared symbol must be a single word with no whitespace; a declared name must be non-blank. An id listed twice on one network fails at boot.
Pricing — no shape change. The oracle is now asked for the specific deployment of an asset, falling back to the symbol the node holds it under only when the oracle does not list that deployment. A price the oracle flags as stale (not refreshed within its maximum age) is reported as unavailable rather than served — so handle "no price" as a normal outcome, not an error.
2026-09-03 — Channel terms are configurable per network
Config — the terms a node proposes, accepts and advertises on its channels are now settable per network. Every key is optional and defaults to what the node used before, so an existing config keeps its behaviour.
Bitcoin networks take a lightning block (block units): channel_handshake (what we propose — our_to_self_delay_blocks, their_channel_reserve_millionths, our_max_accepted_htlcs, minimum_depth_blocks, announce_for_forwarding), channel_handshake_limits (what we accept — their_to_self_delay_blocks, max_self_reserve_millionths, min_funding_satoshis, max_minimum_depth_blocks) and channel_policy (ours alone — cltv_expiry_delta_blocks, forwarding_fee_base_msat, forwarding_fee_proportional_millionths).
EVM / Tron networks take the same three blocks inside lithium (whole seconds): channel_handshake.dispute_period_secs + safety, channel_handshake_limits.max_dispute_period_secs + reserve bounds, channel_policy.cltv_expiry_delta_secs + routing, plus a per_asset map of overrides keyed by canonical asset id.
A channel opens with the longer of the two peers'
dispute_period_secsproposals, within both peers' ceilings — so your proposal is the shortest window you can end up with, and your limit is the longest. A value outside the protocol floors fails at boot, naming the key.
See the Setup Guide → Channel terms.
2026-09-02 — Orderbook currency listing & fee-discount programs
Orderbook — three new read-only RPCs expose what the hub charges and what it lists. A client could not previously predict what a fill would cost.
GetCurrencies()→ListedCurrency[](ordered by network then ticker) — the operator's asset allowlist. Each carries theOrderbookCurrency, itsticker(the identity fee rules and holding ladders are keyed on — the same ticker on two networks is one asset bridged across chains, and it is distinct from the symbol the chain reports), aclass(STANDARD/STABLECOIN), astate(ENABLED/CANCEL_ONLY/FROZEN), and optionalmin_notional_usd/min_native_amountpolicy floors. A currency found in a persisted market but no longer listed has no ticker, is alwaysCANCEL_ONLY, and carries none of the other fields.GetFeeDiscountPrograms()→ the ladders as configured: an optionalvolumeprogram (rolling weighted settled USD volume overwindow_days) and any number ofholdingsprograms (holding an asset above a threshold), each naming exactly the currencies whose balances count.GetFeeDiscount()→ this node's own standing:no_program,excluded, oractivewith the rungs it has reached.
Both listing enums reserve their zero value (CURRENCY_CLASS_UNSPECIFIED, CURRENCY_STATE_UNSPECIFIED) and are never sent: JSON drops a default-valued field, so a real variant at zero would be indistinguishable from an absent one. Treat a zero there as a conversion error.
Orderbook — MarketInfo.min_place_base_amount (11), min_place_quote_amount (12) and order_min_notional_usd (13) became optional, matching what their documentation promised since 2026-08-15. A server that omits them is now distinguishable from one reporting "0".
A holding ladder counts the channel balance with the hub plus, on networks whose node ids are wallet addresses, the on-chain balance. A chain's own native currency cannot have its on-chain holding read —
CurrencyHolding.onchain_amountis absent on Bitcoin and whenever the last read failed with no earlier figure to carry.
See Orderbook → Fee discounts.
2026-08-29 — Amount grids, per-payment sizing & channel ceilings
Currency — SwapAmount now documents a hard precision rule. Whichever variant is set must carry no more decimal places than the orderbook market side it names allows — from measured on the first hop's sending side, to on the last hop's receiving side, against that market's base_precision or quote_precision. The orderbook rejects an over-precise amount rather than rounding it, since the direction to round is the caller's to choose. SwapAmount.To is additionally defined as the amount to receive net of the taker fee: the match is sized so you net at least that figure.
Balance — OffchainBalance gained max_sendable (13) and max_receivable (14): the largest amount a single payment can move in each direction right now. Equal to free_local / free_remote unless the channel protocol enforces a per-payment ceiling below the balance — on a Lightning channel whose value grew past the max_htlc_value_in_flight negotiated at open, free_local keeps growing with deposits while max_sendable stays near the original channel value.
Size single payments off
max_sendable, notfree_local. A balance check that passes while the payment still fails is almost always this.
Swap — SendingChannelDeposit.deposit_channel_id sharpened: absent now means a fresh channel is opened — either because none existed, or because every candidate's per-payment ceiling already binds and growing one cannot raise what a single payment carries.
2026-08-26 — Archive prune: self-paced passes + retention-lagging event
App — ArchivePruneEvent.kind gained retention_lagging (field 8), carrying ArchivePruneRetentionLagging { lag_secs }: the network is past its retention deadline, the archive is not draining fast enough for the configured policy, and the node is escalating how hard it prunes — at the cost of foreground latency — until the backlog clears. Repeated periodically while it persists. Add a branch for it only if you surface prune progress; it is informational, not a failure.
Config — breaking — settings.auto_prune.interval_secs was removed. The config is now the retention policy alone (max_age_secs / max_entries); everything about how pruning runs — how often each network is visited, how long a chunk may hold the archive write lock, what share of the node's time goes to pruning — is derived at runtime from what the node measures about itself, and escalates automatically when retention falls behind.
Action: delete
interval_secsfrom anyauto_pruneblock. At least one ofmax_age_secs/max_entriesis still required, andmax_age_secsstill has a 48-hour floor (172800) — pruning a settled payment deletes its stored preimage, which an in-flight swap may still need.
2026-08-19 — Invite listing & eligibility
App — two new RPCs alongside CreateInvite / RedeemInvite, so a UI can render quota usage without a doomed mint round-trip.
ListInvites(status_filter?)→Invite[], newest-minted first. Each carries the bearercode,created_epoch,created_at, an optionalexpires_at(unset = never expires) and aoneof status { pending | redeemed | expired }—InviteRedeemedstructurally carriesredeemed_by_public_keyandredeemed_at. Filter withInviteStatusFilter(UNSPECIFIED= unfiltered,PENDING,REDEEMED,EXPIRED). The application proves ownership of its identity key to the referral service before the list is returned: an unredeemed code is a bearer secret, shown only to its creator.GetInviteEligibility()→{ eligibility?: InviteEligibility, current_epoch }.InviteEligibilitycarriesjoined_epoch,is_seed(provisioned by the operator rather than onboarded through an invite),invite_quota,invites_mintedandcreated_at. An unseteligibilitymeans the identity cannot mint invites at all (it can still redeem one). Unauthenticated on the referral service side — quotas and epochs are not bearer secrets — so unlikeListInvitesthere is no signature round-trip.
Minting gates on current_epoch > joined_epoch and invites_minted < invite_quota. Quota is spent at mint, not at redeem — an expired or never-redeemed code still counts against it.
Also corrected in the shared OpenRPC schema: protobuf timestamps serialize as RFC 3339 strings, not {seconds, nanos} objects.
See General API → Invites & referral.
2026-08-15 — Advisory placement floor on MarketInfo
Orderbook — MarketInfo gained three fields and min_base_amount gained a sharper definition.
min_base_amountis now documented as the minimum fill amount and the minimum order size of any type — the market's structural grid minimum. A partially consumed maker whose remainder falls below it is evicted at match time, so any resting size ≥ this minimum is fully takeable.- New
min_place_base_amount(11) /min_place_quote_amount(12) — an advisory snapshot of the hub's USD placement floor converted to base/quote units at its last oracle read. Absent when the floor is disabled, a side is unpriceable, or the serving endpoint does not stamp it (notablyGetMarketsInfo, the market-list RPC). - New
order_min_notional_usd(13) — the pair's USD placement floor itself. Absent when disabled.
Do not size purely off
min_place_*. It moves with oracle prices; the authoritative value arrives on the rejection asmin_place=(see Placement rejections). A cached snapshot will intermittently under-size as prices move.Fields 11/12 briefly carried
min_order_base_amount/min_order_quote_amountbetween 2026-08-13 and 2026-08-15; those names never shipped in a published proto set and were replaced bymin_place_*.
See Orderbook → Two different minimums.
2026-08-13 — Structured placement rejections
Orderbook — the hub now encodes order-placement refusals in a stable, SDK-parseable form instead of opaque prose:
<prefix>: key=value key=value — human tail
Closed prefix set: below_minimum, asset_not_listed, market_state, below_floor_budget, below_floor_count, matcher_overloaded. Match on the prefix and key=value tokens only — never the human tail, and not the gRPC status code (the prefix is the contract). Any other shape, including everything a pre-2026-08-13 hub emits, is unclassified and behaves exactly as before.
below_minimum carries market, side, and exactly one of min_place / min_fill — whichever is the larger, binding minimum. It is also the only class that invalidates a cached MarketInfo: refetch GetMarketInfo on it. (market_state does not — MarketInfo carries no market state.)
See Errors → Placement rejections.
2026-08-05 — Config reference refreshed
Config — the Setup Guide template now mirrors the current staging config.yaml. Newly documented (the fields themselves are older; the docs simply never covered them):
pricing_config.price_oracle_url— the price oracle used for fiat conversion, plusfiat_currencies. Note: the upstreamconfig.yaml.samplestill showscmc_url/cmc_api_key; that sample is stale — the application readsprice_oracle_url.alchemy_rpc/alchemy_ws(optional, per EVM network) — enriched Alchemy HTTP / WebSocket endpoints alongside the plainweb3_provider. Each takescustom_url(orapi_key) plusproxy_auth.gossip_sync.rgs_server_urlis proxy-fronted on staging, so it takesproxy_auth: true— it is no longer the public Rapid Gossip Sync server.- Token entries use the lowercase
erc20:prefix.
2026-08-05 — Dual-fund eligibility excludes a native sending asset
Swap — server-side behavior; no request/response shape change. A simple swap whose sending asset is the network's native asset is no longer quoted as DualFundDeferred; it takes the ordinary deposit-and-lease path (Deferred) instead. The liquidity service broadcasts the dual-fund transaction and must pull the client's leg out of the client wallet — a native balance increase is settled from the broadcaster's own value, so it cannot be pulled, and quoting a dual fund there would promise a shape that fails at broadcast.
If you branch on
EstimateSimpleSwapreturningdual_fund_deferredfor native-asset sends (ETH → USDC on the same network, say), expectdeferrednow. No code change is required — just don't assume dual-fund availability.
2026-08-03 — Channel payability & funding state
Channel — AssetChannel gained four booleans that answer "can I use this channel right now" without inferring it from status:
can_send(9) — an outbound payment can be originated or routed over this asset channel right now.can_receive(10) — an inbound payment can be received right now.funding_credited(11) — the funds committed by the most recent on-chain operation are reflected in the balance both peers co-sign. Distinct fromis_updatable: a zero-conf open has its funding credited while still admitting no further on-chain update.onchain_operation_in_flight(12) — a transaction changing this channel's funding is awaiting confirmation.
Prefer can_send / can_receive over hand-rolled checks on status + balances before routing a payment. See Watch-Only Node API → Get Channels.
2026-08-02 — Watchtower attachment
Node — new RPC ConnectToWatchtower(network, watchtower_url). A watchtower holds the states and revocations this node signs, so a node that loses its local history can be handed back the coverage it gave away, and so a channel stays defended while this node is offline.
watchtower_url takes the same <node_id>@<host>:<port> form as ConnectToPeer — a watchtower is reached over the same transport. The watchtower session is independent of the ordinary peer session: the same remote node may be both a channel peer and a watchtower, and the two connections are tracked separately. Response is empty.
See Node API → Connect to Watchtower.
2026-08-01 — On-chain preimage settlement split out (breaking)
Preimage — the two settlement facets are now two RPCs, because they fail independently and only the on-chain one broadcasts transactions (and therefore costs a fee).
SettlePreimageis narrowed to CHANNEL legs only. It persists the preimage, claims held channel hashlock payments, and arms the on-chain settlement path. It no longer claims on-chain HTLCs. Costs no fee of its own.- New
SettleHtlcPreimage(network, payment_preimage, fee_option?)claims every on-chain HTLC the preimage unlocks — each one stillLocked, paying this node, undersha256(preimage). Returnstxids[], one per HTLC claimed (empty when the secret unlocks none here). Each claim is its own transaction; one that cannot be built (already swept, expired and refunded, unfunded for its fee) is skipped rather than withholding the others.FAILED_PRECONDITIONon a network with no on-chain HTLC facet. Optionalfee_option— absent = the network's default ("medium") HTLC fee rate. Idempotent.
Action required: if you relied on
SettlePreimagealso sweeping on-chain HTLCs (its 2026-06-11 behavior), call both. On a network with both facets, a caller drives both RPCs.
See HTLC & Preimage API → Settle Preimage and Settle HTLC Preimage.
2026-07-30 — Lithium reads from contract logs by default
Config — lithium.state_backend now defaults to logs (it was subgraph), and lithium.contract_deploy_block was added.
logs— contract event logs plus verified contract reads are authoritative; the subgraph is optional and only bootstraps the routing graph.subgraph— the indexer becomes the sole, unverified source. Needed only for a contract that predates the channel lifecycle events.
contract_deploy_block sets the first block to index from, so a first sync does not scan from genesis. Leaving it at 0 is valid but slow on a long-lived chain.
Action: if your config pins
state_backend: subgraph, drop it (or setlogs) unless you are on a pre-lifecycle-events contract —logsverifies state against the chain rather than trusting the indexer.
2026-07-23 — Liquidity-service exit rail (breaking)
Gasless channel exits: the liquidity-service hub can broadcast a post-swap withdrawal and pay the gas, with the fee folded into the swap totals — so a user with no native balance can still exit a channel.
Swap
- New enum
ExitRail:EXIT_RAIL_UNSPECIFIED(0, treated as local),EXIT_RAIL_LOCAL(1 — the local node broadcasts and pays gas from its on-chain native balance),EXIT_RAIL_LIQUIDITY_SERVICE(2 — the hub broadcasts and pays gas). WithdrawalFeegainedexit_rail(field 3) — which rail performs that side's exit. It appears on thesending_withdrawal_fee/receiving_withdrawal_feeof everySimpleSwapEstimatevariant that carries one.SimpleSwapUpdate.updategainedwithdrawing_funds_via_liquidity_service(field 34):{ is_sending_side, fee, fee_payment_currency }— one side's exit fee is being settled off-chain with the liquidity service.
Liquidity — breaking — ChannelReleaseOperation.Withdraw replaced repeated string asset_ids = 2 with map<string, AssetWithdrawAmounts> asset_amounts = 2, where AssetWithdrawAmounts { optional Amount server_amount, optional Amount client_amount } splits the withdrawal per side: server_amount releases the service's own balance to the service wallet, client_amount 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, and every requested side must then be an exact amount. CooperativeClose is unchanged (still asset_ids).
Action required: rewrite any
withdrawrelease operation from a list of asset IDs to a map of asset ID →{ server_amount?, client_amount? }. See Lease API → ChannelReleaseOperation.
2026-07-20 — Peer connection blacklist
Node — three new RPCs for a runtime peer-connection blacklist (also file-configurable):
GetBlacklist(network)→node_ids[](hex-encoded public keys of blacklisted peers).AddPeerToBlacklist(network, node_id)— empty response.RemovePeerFromBlacklist(network, node_id)— empty response.
A blacklisted peer is refused connections. This is the inverse of the zero-conf whitelist and is independent of it. See Node API → Peer Blacklist.
2026-07-12 — Lease expiry on the channel
Channel — AssetChannel gained lease_expiry (optional Timestamp, field 8): when the liquidity lease on that asset channel expires. Absent when the asset channel is not leased.
This is the cheapest way to monitor a lease: it rides along on every channel read (watchOnlyNode.GetChannels / GetChannel) and every channel update event, so you no longer need a separate liquidity.GetLeaseExpiries poll to know when a lease is running out. Leases extend automatically as a channel is used, so a busy channel's lease_expiry moves forward on its own — watch it rather than assuming the duration you originally paid for.
2026-07-11 — Tron protocol
Primitives — Protocol gained PROTOCOL_TRON (3) (Tron mainnet, Shasta, Nile — TVM). Anything that match/switches exhaustively on Protocol needs a Tron arm.
Config — a protocol: tron network block takes provider.url (Tron node HTTP API), optional indexer.url, rpc.url (eth-compatible JSON-RPC), a base58check htlc_factory_address (required for on-chain HTLC settlement on Tron — omit and that network settles through channels only), the usual lithium block with a base58check contract_address, and trc20: token entries.
Tron Shasta is live on staging — see the Setup Guide for the network block and Staging peers for the peer string.
2026-07-08 — Archive-prune redesign (breaking)
App — the synchronous PruneArchive RPC was removed and replaced by an asynchronous job model. PruneArchive / PruneArchiveRequest / PruneArchiveResponse (with total_pruned / per_table) no longer exist.
New RPCs:
StartArchivePrune— starts (or attaches to) the prune job for a network; returns anArchivePruneJobdescriptor +newly_started. At most one job runs per network. Filters (max_age_secs,max_items) moved intoArchivePruneParams; at least one must be set andmax_itemsmust be ≥ 1.GetArchivePruneStatus— the authoritative state: the currentlyrunningjob (with anArchivePruneProgresssnapshot) and thelastfinishedArchivePruneRecord.CancelArchivePrune— requests cancellation (asynchronous; stops at the next chunk boundary). Idempotent.SubscribeArchivePruneEvents— a stream ofArchivePruneEvent(started/progress/completed/failed/cancelled) across all networks. Best-effort delivery — reconcile withGetArchivePruneStatuson stream end.
Config also gained an optional settings.auto_prune block (periodic auto-prune). Pruning a settled payment still deletes its stored preimage, so GetPreimage stops serving pruned payments. See General API → Archive Prune.
Action required: replace any
PruneArchive/app_pruneArchivecall withStartArchivePrune+ a poll ofGetArchivePruneStatus(or aSubscribeArchivePruneEventssubscription).
App (invite / referral) — three new RPCs: CreateInvite (mint a bearer invite code), RedeemInvite (redeem a peer's code; returns the inviter's public key), and GetReferral (the currently configured referrer, if any). A new config.yaml referral_config.referral_service_url wires the referral service.
2026-07-06 — HTLC lock types
HTLC — the on-chain HTLC RPCs gained a lock_type field (canonical protocol-defined script-kind token, e.g. Bitcoin "taproot" / "p2wsh"; empty = the node's signer-derived default). It appears on CreateHtlcRequest, CreateHtlcLockTxRequest, DeriveHtlcAddressRequest, BatchCreateHtlcsRequest, and — as a required-match pin on the receiving side — HtlcExpectation.lock_type. A config.yaml htlc_factory_address (EVM) and htlc_script_type knob gate on-chain HTLC availability.
2026-07-02 — HTLC event handling refactor + transaction HTLC operations (breaking)
Event — the 2026-06-04 NodeEvent.HtlcUpdate / HtlcSnapshot design was replaced. NodeEvent no longer carries an HTLC variant. Instead EventService gained a dedicated stream SubscribeHtlcEvents(SubscribeHtlcEventsRequest{ network }) → stream Htlc: it emits the full Htlc on every lifecycle transition, and the HTLC's status (Locked / Claimed / Refunded) conveys what happened — Claimed reveals the preimage. Dedupe key: (htlc-id, status).
If you subscribed to
NodeEventforHtlcUpdate(added 2026-06-04), switch toSubscribeHtlcEvents. TheHtlcSnapshotmessage is gone; useHtlc.
Transaction — TransactionOperation gained repeated HtlcOperation htlc_operations (field 13): on-chain HTLC lock / claim / refund operations observed within a wallet transaction (HtlcLock / HtlcClaim — reveals the preimage — / HtlcRefund). TransactionRequest.raw_data was renamed to signable_data with a documented per-protocol encoding (EVM: UTF-8 JSON eth_sendTransaction params; Bitcoin: base64 PSBT); SignedTransactionRequest documents its broadcastable encoding likewise.
2026-06-24 — On-chain HTLC swap milestones
Swap — SimpleSwapUpdate.update (the SubscribeSimpleSwaps stream) gained seven on-chain-settlement milestone variants (fields 27–33): locking_onchain_htlc, onchain_htlc_locked, counterparty_lock_confirming ({ txid, current, required }), counterparty_lock_observed, claiming_onchain_htlc, onchain_htlc_claimed, refunding_onchain_htlc. They appear when a swap leg settles on-chain rather than through a channel (see the settlement model). Existing channel-path milestones are unchanged; add branches for the new variants only if you render on-chain progress.
2026-06-17 — Redeemable channel balances + unique HTLC keys
Balance — AssetChannelBalance gained redeemable_local and redeemable_remote (fields 11–12): the local/remote amounts redeemable on-chain when the channel is redeemable (zero otherwise). These are a view into unavailable_local / unavailable_remote, not additional balance categories — don't double-count.
HTLC — new RPC GetUniqueHtlcPubkey: reserves a fresh HTLC public key (UTXO protocols return a never-before-used key per call; account-model protocols return their single stable key). GetHtlcPubkey returns the node's canonical key.
2026-06-16 — Signer: sign-and-broadcast
Signer — new RPC SignAndBroadcastTx: signs (or authorizes) an unsigned transaction and ensures it reaches the chain, returning the txid. The universal path — an offline signer signs and the client broadcasts, or a self-broadcasting authority (e.g. MetaMask) signs and broadcasts in one step. SignTransactionResponse.signed_tx changed from a structured SignedTransactionRequest to serialized bytes (ready to broadcast); SignTransaction now fails for broadcast-only signers that can't produce standalone signed bytes — use SignAndBroadcastTx for those.
2026-06-15 — Orderbook on-chain settlement wiring
Orderbook — the swap-routing messages gained on-chain-settlement plumbing:
SwapHopgained optionalsending_onchain(OnchainSendSettlement) andreceiving_onchain(OnchainRecvSettlement) — unset means channel settlement (the default). NewTimelockSpec(absolute block-height / unix-seconds) mirrors the route's on-chain HTLC timelock.SwapPath(inMatchedOrder) gainedsettlement(OrderSettlement) per leg. On-chain is valid only for taker (market/swap) orders; resting maker orders are channel-only.ORDER_TYPE_LIQUIDITY(2) is a real enum value (previously an internal-reserved slot).
2026-06-11 — Preimage service, order settlement & HTLC service rework (breaking)
A large change introducing hybrid (channel + on-chain) swap settlement.
Preimage — a new top-level PreimageService (JSON-RPC namespace preimage) with one RPC SettlePreimage(network, payment_preimage): registers a revealed preimage and settles every leg it unlocks — held channel hashlock payments (claim + arm force-close) and, on HTLC-capable networks, matching on-chain HTLCs. NodeService.RegisterPreimage was removed — its behavior is now SettlePreimage (which additionally claims on-chain HTLCs).
Action required: replace
node.RegisterPreimagewithpreimage.SettlePreimage. Request fields are identical (network, hexpayment_preimage). See the new HTLC & Preimage API.
Currency — new LegSettlement enum (CHANNEL (0, default) / ONCHAIN (1) / CHANNEL_OR_ONCHAIN (2)) and OrderSettlement message (sending / receiving LegSettlement + taker-only route_filter bitmask). Absent = channel on both legs.
Swap — SwapRequest gained settlement (OrderSettlement) (field 4); the orderbook OrderVariant / SwapOrder creation paths accept per-leg settlement. Absent keeps the previous channel-only behavior.
Orderbook — breaking — add_liquidity was removed from CreateOrder's OrderVariant oneof. The creatable variants are now limit_order, market_order, swap_order only. Provide passive / maker liquidity by placing limit orders. The LiquidityOrder message and ORDER_TYPE_LIQUIDITY remain as the persisted / returned form (PairOrder.liquidity_order) for positions created that way; you can still read, hold, and cancel them.
Action required: delete any
CreateOrder { order_variant: { add_liquidity: … } }path. Replace range provision (min_buy_price/mid_price/max_sell_price/remove_on_fill) with one or more limit orders.
HTLC — HtlcService (namespace htlc) was reworked from a stub into a full on-chain HTLC surface: external-signer transaction builders (CreateHtlcLockTx / CreateHtlcClaimTx / CreateHtlcRefundTx / CreateHtlcSettlementTx / BroadcastHtlcSettlement), GetChainHtlc, VerifyHtlcByLockTxid, DeriveHtlcAddress, WatchHtlc / UnwatchHtlc, GetHtlcPubkey. The old HtlcState / HtlcStatus-enum / HtlcEvent shapes were replaced by a unified Htlc message (chain-native id, display-unit amount, Timelock + LedgerDepth with explicit kind, and a HtlcStatus oneof { Locked | Claimed | Refunded }). Amounts are now in the asset's display unit and timelocks are absolute (Unix-seconds / block-height), not block counts. See the HTLC & Preimage API.
2026-06-04 — HTLC update events (superseded 2026-07-02)
Superseded. This
NodeEvent.HtlcUpdate/HtlcSnapshotdesign was replaced on 2026-07-02 by the dedicatedEventService.SubscribeHtlcEventsstream, which emits the fullHtlc. Kept here for history; do not build againstHtlcUpdateorHtlcSnapshot.
Event — NodeEvent.update gained a new variant HtlcUpdate { HtlcSnapshot htlc } (field 15). The HtlcSnapshot carries protocol-agnostic on-chain HTLC state: identifier, asset, amount, payment hash, recipient / refund addresses, absolute expiry, lock txid + block height, and a oneof status { Locked | Claimed | Refunded }. Claimed status reveals the preimage — critical for atomic-swap takers waiting on the maker's claim. Dedupe key: (htlc.htlc_id, status).
2026-06-03 — EstimateSimpleSwappableAmounts response now non-optional
Swap — EstimateSimpleSwappableAmountsResponse.amounts changed from optional SimpleSwappableAmounts to always-present SimpleSwappableAmounts. When no feasible swap exists, all four fields collapse to "0" rather than the field being absent. Bots that branched on "amounts field unset → no liquidity" must switch to "max_sending == "0" → no swap currently feasible."
2026-05-26 — Archive retention (redesigned 2026-07-08)
Superseded. The synchronous
PruneArchiveintroduced here was replaced on 2026-07-08 by the asynchronousStartArchivePrunejob model. Kept for history.
App — new RPC PruneArchive: operator-driven retention. Prunes archive-side settled wallet transactions and settled payments for one network, filtered by max age and/or max count. Pending entries are never pruned. With both filters unset the call is a no-op.
See General API → Archive Prune for the current shape.
2026-05-21 — Node policy introspection
Node — new read-only RPC GetNodePolicy for one (network, asset_id). The response is an aggregate of policy sub-messages; the first category exposed is ReservePolicy (channel reserve parameters: counterparty proportional rate in millionths, max self proportional rate in millionths, absolute minimum, and a fixed channel-reserve fee). Liquidity providers and any party sizing on-chain deposits accurately should call this before negotiating a channel.
New sub-messages will be added in a backwards-compatible way as more NodeConfig categories are exposed — treat each as optional.
See Node API → Get Node Policy.
2026-05-19
Swap — counterparty invoice-window handling and convergence retry in the simple-swap flow (server-side behavior; no request/response shape change for clients).
2026-05-10 — Payment timing (CLTV) overhaul
Breaking field renames on the Node API payment RPCs. If you send keysend payments or create invoices, update your code.
Node
SendChannelPaymentRequest:expiry_timeout_secs→cltv_buffer_secs. (KeySend has no separate invoice-validity knob; the buffer alone bounds the HTLC.)EstimateSendPaymentFeeRequest:expiry_timeout_secs→cltv_buffer_secs; new optionalmax_total_cltv_secs(clamp the route's max-total-CLTV; for atomic-swap-correct timing pass the invoice'scltv_buffer_secs, otherwise omit).SendPaymentRequest: same change asEstimateSendPaymentFeeRequest.CreateInvoiceRequest:expiry_timeout_secskeeps its meaning (BOLT-11x— invoice validity window) and gains a new optionalcltv_buffer_secs(BOLT-11c— extra HTLC lifetime past invoice expiry the receiver requires).EstimatePayInvoiceFeeRequest,PayInvoiceRequest,EstimatePayEmptyInvoiceFeeRequest,PayEmptyInvoiceRequest: new optionalmax_total_cltv_secs.
Payment
Invoice: new fieldmin_final_cltv_expiry_secs— the minimum CLTV buffer (seconds) the receiver requires for the incoming HTLC. The HTLC's effective deadline isexpiry_timestamp + min_final_cltv_expiry_secs.
2026-05-06 — Lease API: read-only discovery
Liquidity — four new read-only RPCs (no funds move):
GetLiquidityServiceInfo— server-wide capacity bounds, durations, per-asset fee config, and the LP's per-network node pubkeys.GetLeaseableAssetInfo— per-asset liquidity bounds + fee ratio for one(network, asset).GetLeases— the caller's active leases on a network.GetLeaseExpiries— lease expiry per(channel_id, asset_id)from the local cache (no LP round-trip).
Also: ChannelReleaseOperation.CooperativeClose gained asset_ids — empty closes every asset channel; otherwise only the listed ones.
2026-05-05 — Lease API: tx_fee_rate removed (breaking)
Liquidity / Swap — the liquidity service now prices the underlying transaction work itself. The client no longer supplies a chain fee rate.
RequestChannelLiquidityRequest: removedtx_fee_rate(remaining fields renumbered).RequestChannelReleaseRequest: removedtx_fee_rate(remaining fields renumbered).ReceivingChannelLease(swap): removedtx_fee_rate(remaining fields renumbered).
Action required: delete
tx_fee_rate/txFeeRate/TxFeeRatefrom any Lease request you build. Sending it now returns-32602 unknown field 'txFeeRate'. The Lease API examples have been updated.
2026-05-03 — Channel close flag + client order IDs
Channel
AssetChannelStatus.Closedgainedforce_closed(bool).false= cooperative close (in lithium the channel slot can be reused for further deposits);true= unilateral / disputed close (slot is permanently dead — open a new channel).
Orderbook
CreateOrderRequestgained optionalclient_order_id(max 64 chars). When set and unique among your open orders, the orderbook stores it, returns the originalorder_idon retries with the same value (idempotent order creation), and exposes it on subsequent reads.Ordergained optionalclient_order_id.- New RPC
GetOrderByClientId— fetch an order by theclient_order_idyou supplied at creation. SwapRoleenum reordered:SWAP_ROLE_TAKERmoved from0to3.SWAP_ROLE_UNSPECIFIEDis0. If you persisted raw enum integers, re-map them.
2026-04-29 — Swappable-amount estimation
Swap — new RPC EstimateSimpleSwappableAmounts: given two currencies, returns the smallest and largest amounts that can currently be simple-swapped, accounting for wallet balances, orderbook liquidity, and the LP's leaseable capacity. Returns an empty amounts field when the pair has no orderbook liquidity; all-zero fields when the pair has liquidity but no swap is currently feasible.
2026-04-27 — New SimpleSwap estimate variant
Swap — SimpleSwapEstimate gained the InsufficientSendingBalance variant ({ available: DecimalString }). Distinct from NoLiquidity: the market is fine, but the wallet has nothing to send (no active channel and no usable on-chain funds after fees). If you match/switch on the estimate one-of, add a branch for it.
Earlier
The 2026-04 reconciliation aligned every doc with the then-current proto, including the rename of the old RentalService to LiquidityService (the Lease API). The rental_* JSON-RPC namespace no longer exists — it is liquidity_*. See the Lease API.
How this list is maintained
Each entry corresponds to a proto-touching commit in the Hydra App source. When the protos in /proto are refreshed, this page and the affected per-service docs are updated together. The single source of truth for a running server is its rpc.discover output — see JSON-RPC: Discovering methods.