Swap API
The Swap API exposes Hydra App's SwapService — cross-chain atomic swaps, either against balances you already hold off-chain (Swap) or with the channels set up for you first (SimpleSwap).
JSON-RPC namespace: swap
Endpoints
- Estimate Swap
- Swap
- Estimate Simple Swap
- Estimate Simple Swappable Amounts
- Simple Swap
- Cancel Simple Swap
- Subscribe Simple Swaps
Estimate Swap
Estimate a swap between two currencies using existing offchain balance.
Method: EstimateSwap
Requirements:
- Must have sufficient offchain balance for both sending and receiving currencies
- Use Simple Swap if you don't have channels set up
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
sending_currency | OrderbookCurrency | YES | Currency to send |
receiving_currency | OrderbookCurrency | YES | Currency to receive |
amount | SwapAmount | YES | Amount to swap (sending or receiving) |
SwapAmount Object (one of):
| Field | Type | Description |
|---|---|---|
from | { amount: DecimalString } | Amount to send, on the first hop's sending grid |
to | { amount: DecimalString } | Amount to receive, net of the taker fee, on the last hop's receiving grid |
⚠️ The amount must sit on the market's grid(2026-08-29) Whichever variant you set must carry no more decimal places than the orderbook market side it names allows —
fromis measured on the first hop's sending side,toon the last hop's receiving side, against that market'sbase_precisionorquote_precision(seeMarketInfo).An over-precise amount is rejected, not rounded. The direction to round in is yours to choose, so the orderbook refuses rather than guessing. Quantise before you send: truncate toward zero when you are sizing a send, and away from zero when you are sizing a receive, or you can end up asking for fractionally more than you meant.
tois defined net of the taker fee — the match is sized so you net at least that figure, so a filled order delivers no less than you asked for.
Response:
| Field | Type | Description |
|---|---|---|
order_match | OrderMatch | Estimated execution (optional - null if no liquidity) |
Example Request:
TypeScript
import { SwapServiceClient, EstimateSwapRequest } from './proto/SwapServiceClientPb'
const client = new SwapServiceClient('http://localhost:5003')
const request = new EstimateSwapRequest()
request.setSendingCurrency({
network: { protocol: 1, id: '0a03cf40' },
assetId: '0x0000000000000000000000000000000000000000000000000000000000000000'
})
request.setReceivingCurrency({
network: { protocol: 2, id: '11155111' },
assetId: '0x0000000000000000000000000000000000000000'
})
request.setAmount({
from: { amount: { value: '1000000' } } // 0.01 BTC
})
const response = await client.estimateSwap(request, {})
const match = response.getOrderMatch()
if (match) {
console.log('Sending:', match.getSendingAmount())
console.log('Receiving:', match.getReceivingAmount())
console.log('Price:', match.getPrice())
} else {
console.log('No liquidity available')
}
Go
import (
"context"
"log"
pb "your-package/proto"
)
client := pb.NewSwapServiceClient(conn)
req := &pb.EstimateSwapRequest{
SendingCurrency: &pb.OrderbookCurrency{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
AssetId: "0x0000000000000000000000000000000000000000000000000000000000000000",
},
ReceivingCurrency: &pb.OrderbookCurrency{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_EVM,
Id: "11155111",
},
AssetId: "0x0000000000000000000000000000000000000000",
},
Amount: &pb.SwapAmount{
Amount: &pb.SwapAmount_From_{
From: &pb.SwapAmount_From{Amount: &pb.DecimalString{Value: "0.01"}},
},
},
}
resp, err := client.EstimateSwap(context.Background(), req)
if err != nil {
log.Fatalf("Error: %v", err)
}
if match := resp.GetOrderMatch(); match != nil {
log.Printf("Sending: %s", match.GetSendingAmount())
log.Printf("Receiving: %s", match.GetReceivingAmount())
log.Printf("Price: %s", match.GetPrice())
} else {
log.Println("No liquidity available")
}
Rust
use tonic::Request;
use your_package::swap_service_client::SwapServiceClient;
use your_package::{EstimateSwapRequest, OrderbookCurrency, Network, SwapAmount, DecimalString};
let mut client = SwapServiceClient::connect("http://localhost:5003").await?;
let request = Request::new(EstimateSwapRequest {
sending_currency: Some(OrderbookCurrency {
network: Some(Network {
protocol: Protocol::Bitcoin as i32,
id: "0a03cf40".to_string(),
}),
asset_id: "0x0000000000000000000000000000000000000000000000000000000000000000".to_string(),
}),
receiving_currency: Some(OrderbookCurrency {
network: Some(Network {
protocol: Protocol::Evm as i32,
id: "11155111".to_string(),
}),
asset_id: "0x0000000000000000000000000000000000000000".to_string(),
}),
amount: Some(SwapAmount {
amount: Some(your_package::swap_amount::Amount::From(your_package::swap_amount::From {
amount: Some(DecimalString { value: "0.01".to_string() }),
})),
}),
});
let response = client.estimate_swap(request).await?.into_inner();
if let Some(match_) = response.order_match {
println!("Sending: {}", match_.sending_amount);
println!("Receiving: {}", match_.receiving_amount);
println!("Price: {}", match_.price);
} else {
println!("No liquidity available");
}
Example Response:
{
"order_match": {
"sending_amount": "1000000",
"receiving_amount": "28500000000000000000",
"price": "28.5"
}
}
Swap
Execute a swap between two currencies using existing offchain balance.
Method: Swap
Requirements:
- Must have sufficient offchain balance for both sending and receiving currencies
- Channels must be active for both currencies
- Use Simple Swap for automatic channel setup
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
sending_currency | OrderbookCurrency | YES | Currency to send |
receiving_currency | OrderbookCurrency | YES | Currency to receive |
amount | SwapAmount | YES | Amount to swap |
settlement | OrderSettlement | NO | Added 2026-06-11. Per-leg channel-vs-on-chain settlement, signed into the order. Absent = channel-only (the previous behavior). Opting a leg on-chain lets you swap a currency you hold on-chain without opening a channel for it. See the settlement model and the orderbook's On-chain settlement field reference. |
Response:
| Field | Type | Description |
|---|---|---|
order_id | string | Swap order identifier |
Example Request:
TypeScript
const request = new SwapRequest()
request.setSendingCurrency({
network: { protocol: 1, id: '0a03cf40' },
assetId: '0x0000000000000000000000000000000000000000000000000000000000000000'
})
request.setReceivingCurrency({
network: { protocol: 2, id: '11155111' },
assetId: '0x0000000000000000000000000000000000000000'
})
request.setAmount({
from: { amount: { value: '1000000' } }
})
const response = await client.swap(request, {})
const orderId = response.getOrderId()
console.log('Swap order created:', orderId)
Go
req := &pb.SwapRequest{
SendingCurrency: &pb.OrderbookCurrency{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
AssetId: "0x0000000000000000000000000000000000000000000000000000000000000000",
},
ReceivingCurrency: &pb.OrderbookCurrency{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_EVM,
Id: "11155111",
},
AssetId: "0x0000000000000000000000000000000000000000",
},
Amount: &pb.SwapAmount{
Amount: &pb.SwapAmount_From_{From: &pb.SwapAmount_From{Amount: &pb.DecimalString{Value: "1000000"}}},
},
}
resp, err := client.Swap(context.Background(), req)
if err != nil {
log.Fatalf("Error: %v", err)
}
orderId := resp.GetOrderId()
log.Printf("Swap order created: %s", orderId)
Rust
let request = Request::new(SwapRequest {
sending_currency: Some(OrderbookCurrency {
network: Some(Network {
protocol: Protocol::Bitcoin as i32,
id: "0a03cf40".to_string(),
}),
asset_id: "0x0000000000000000000000000000000000000000000000000000000000000000".to_string(),
}),
receiving_currency: Some(OrderbookCurrency {
network: Some(Network {
protocol: Protocol::Evm as i32,
id: "11155111".to_string(),
}),
asset_id: "0x0000000000000000000000000000000000000000".to_string(),
}),
amount: Some(SwapAmount {
amount: Some(your_package::swap_amount::Amount::From(swap_amount::From { amount: Some(DecimalString { value: "1000000".to_string() }) })),
}),
});
let response = client.swap(request).await?.into_inner();
let order_id = response.order_id;
println!("Swap order created: {}", order_id);
Estimate Simple Swap
Estimate a swap with automatic channel setup. This calculates all fees including channel opening, deposits, and rentals.
Method: EstimateSimpleSwap
Benefits:
- No need for existing channels
- Automatic balance management
- One-click cross-chain swaps
- Optional auto-withdrawal
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
sending_currency | OrderbookCurrency | YES | Currency to send |
receiving_currency | OrderbookCurrency | YES | Currency to receive |
amount | SwapAmount | YES | Amount to swap |
price_change_tolerance | DecimalString | YES | Max price slippage (e.g., "0.01" = 1%) |
withdraw_sending_funds | bool | YES | Auto-withdraw sent funds after swap |
withdraw_receiving_funds | bool | YES | Auto-withdraw received funds after swap |
Response:
| Field | Type | Description |
|---|---|---|
estimate | SimpleSwapEstimate | Estimate details (one of multiple variants) |
SimpleSwapEstimate Variants:
Instant
Ready to swap immediately - you have sufficient offchain balance.
| Field | Type | Description |
|---|---|---|
order_match | OrderMatch | Expected execution |
sending_withdrawal_fee | WithdrawalFee | Fee to withdraw sent funds (optional) |
receiving_withdrawal_fee | WithdrawalFee | Fee to withdraw received funds (optional) |
Deferred
Need to set up channels via deposits / leases first.
| Field | Type | Description |
|---|---|---|
order_match | OrderMatch | Expected execution |
sending_channel_deposit | SendingChannelDeposit | Required deposit (optional) |
receiving_channel_lease | ReceivingChannelLease | Required lease (optional) |
sending_withdrawal_fee | WithdrawalFee | Withdrawal fee (optional) |
receiving_withdrawal_fee | WithdrawalFee | Withdrawal fee (optional) |
token_approval_fee | DecimalString | Native-asset fee for the token approval tx (optional, EVM only) |
SendingChannelDeposit: { amount, unspendable_reserve, fee, fee_payment_currency, counterparty, deposit_channel_id?, rail }.
An absent
deposit_channel_idmeans a fresh channel is opened — either because no channel existed, or because (2026-08-29) every candidate's per-payment ceiling already binds and growing one cannot raise what a single payment carries. Don't read "no channel id" as "no channel exists"; read it as "this deposit will not reuse one". The ceiling in question is the same onemax_sendablereports.
DualFundDeferred
Need to dual-fund a channel.
| Field | Type | Description |
|---|---|---|
order_match | OrderMatch | Expected execution |
dual_fund_deposit | DualFundDeposit | Required dual-fund deposit |
sending_withdrawal_fee | WithdrawalFee | Withdrawal fee (optional) |
receiving_withdrawal_fee | WithdrawalFee | Withdrawal fee (optional) |
token_approval_fee | DecimalString | Native-asset fee for the token approval tx (optional, EVM only) |
FeesHigherThanAmount
The fees needed to set up channels would exceed the swap amount — the swap is uneconomical and won't be performed.
| Field | Type | Description |
|---|---|---|
sending_fee | DecimalString | Total fee in sending currency |
receiving_fee | DecimalString | Total fee in receiving currency |
LeaseTooBig
The receiving lease needed exceeds available liquidity or the provider's max capacity.
| Field | Type | Description |
|---|---|---|
needed_lease | DecimalString | Required lease amount |
available_lease_liquidity | DecimalString | Currently available |
max_lease_capacity | DecimalString | Provider's maximum |
NoLiquidity
No liquidity in the orderbook for this pair (empty payload). This means the market can't serve the pair.
InsufficientSendingBalance
The wallet has nothing to send — no active channel and no usable on-chain balance for the sending asset after fees. Distinct from NoLiquidity (where the market is the problem; here the market is fine).
| Field | Type | Description |
|---|---|---|
available | DecimalString | The spendable balance the wallet does have for the sending asset |
Added 2026-04-27. If you
match/switchon the estimate one-of, add a branch forInsufficientSendingBalance— otherwise it falls through your default case and you'll misreport "no liquidity" when the real problem is an empty wallet.
InsufficientNativeBalance
Added 2026-09-11.
The wallet holds the sending asset but cannot pay the sending network's native gas for the funding transaction the swap needs.
| Field | Type | Description |
|---|---|---|
required | DecimalString | Native-asset amount the funding transactions need, including the broadcast envelope the chain's admission check reserves on top of the effective fee |
available | DecimalString | Native-asset balance currently usable on the sending network |
Gas and the sending asset are separate resourcesThis is neither
InsufficientSendingBalance(the wallet has plenty of the asset) norNoLiquidity(the market is fine). It is the specific case of holding, say, USDC on Arbitrum with no ETH to move it. Fund the wallet withrequiredof the native asset and estimate again.
requiredis above the raw fee on purpose: an EVM node admits a transaction only when the sender can covergas × max_fee_per_gas, so a balance that merely equals the expected fee is still rejected. Topping up to exactlyrequiredis what clears it.
When it can and cannot fireOnly a funded deposit or channel open raises it. A swap served entirely from existing channel balances signs nothing on chain and needs no gas, so it never appears there. And a swap whose sending asset is itself the native asset cannot produce it either — a wallet short of gas is then short of what it is selling, which is
InsufficientSendingBalance. In practice you meet this variant on a token send that needs a channel funded.
The alternative to topping up is the liquidity-service deposit rail — see Deposit rails — which has the hub pay the gas out of the deposit instead.
Older versions of these docs documented a
NotEnoughBalanceandRentalTooBigvariant. Those names no longer exist — they were renamed toFeesHigherThanAmountandLeaseTooBigrespectively when the rental flow was rewritten as the Lease flow.
Deposit rails
Added 2026-09-11. Carried as rail on SendingChannelDeposit.
DepositRail says who broadcasts the sending side's channel funding and pays its gas — the entry-side counterpart of ExitRail.
| Value | Constant | Meaning |
|---|---|---|
0 | DEPOSIT_RAIL_UNSPECIFIED | Treated as DEPOSIT_RAIL_LOCAL |
1 | DEPOSIT_RAIL_LOCAL | Your node broadcasts the funding transaction and pays gas from its on-chain native balance |
2 | DEPOSIT_RAIL_LIQUIDITY_SERVICE | The liquidity-service hub broadcasts the funding transaction and pays gas. Your wallet authorises the token pull with a signed permit, or with an allowance it already granted — never with an approval transaction of its own — and pays the hub's quoted fee out of the deposit itself, folded into the swap totals |
The liquidity-service rail makes the entry gasless for you: it is what lets a wallet holding only a token — no native asset at all — still fund a sending channel. It is the entry-side answer to InsufficientNativeBalance.
When the sending side takes this rail, the swap stream emits funding_sending_channel_via_liquidity_service alongside the ordinary funding milestones. The same mechanism outside a swap is SponsoredDepositFeePayment on the Lease API.
The rail is chosen for you. There is no field to request one: it is reported on the estimate and on the swap output. Read it to know what the user is actually agreeing to — a fee taken out of the deposit is not the same trade as gas paid from their own balance.
On the liquidity-service rail,
SendingChannelDeposit.feeis charged in the sending asset, never in native gas — sofee_payment_currencyon such a deposit is neverFEE_PAYMENT_CURRENCY_NATIVE. The same holds forWithdrawalFeeon theEXIT_RAIL_LIQUIDITY_SERVICErail.
WithdrawalFee — the cost of exiting a channel
Carried by sending_withdrawal_fee / receiving_withdrawal_fee on the Instant, Deferred, and DualFundDeferred variants. Present only when that side's funds would be withdrawn from the channel after the swap.
| Field | Type | Description |
|---|---|---|
fee | DecimalString | The fee amount |
fee_payment_currency | FeePaymentCurrency | Which asset pays it: ..._SENDING (1), ..._RECEIVING (2), or ..._NATIVE (3) |
exit_rail | ExitRail | (added 2026-07-23) Which rail broadcasts this side's exit — see below |
ExitRail — who broadcasts and pays gas
Added 2026-07-23.
| Value | Constant | Meaning |
|---|---|---|
0 | EXIT_RAIL_UNSPECIFIED | Treated as EXIT_RAIL_LOCAL |
1 | EXIT_RAIL_LOCAL | Your node broadcasts the exit transaction and pays gas from its on-chain native balance |
2 | EXIT_RAIL_LIQUIDITY_SERVICE | The liquidity-service hub broadcasts and pays the gas; the fee is folded into the swap totals |
The liquidity-service rail makes the exit gasless for you — it is what lets a wallet with no native balance still withdraw from a channel after a swap. When a side takes it, the swap stream emits withdrawing_funds_via_liquidity_service instead of the ordinary local-withdrawal milestones.
Example Request:
import {
EstimateSimpleSwapRequest,
} from './proto/swap_pb'
const request = new EstimateSimpleSwapRequest()
request.setSendingCurrency({
protocol: 1, networkId: '0a03cf40', assetId: '0x0000000000000000000000000000000000000000000000000000000000000000'
})
request.setReceivingCurrency({
protocol: 2, networkId: '11155111',
assetId: '0x0000000000000000000000000000000000000000'
})
// Amounts are human-readable. "0.1" means 0.1 BTC, not 0.1 sats.
request.setAmount({ from: { amount: { value: '0.1' } } })
request.setPriceChangeTolerance({ value: '0.01' }) // 1 %
request.setWithdrawSendingFunds(false)
request.setWithdrawReceivingFunds(true)
const response = await swap.estimateSimpleSwap(request, {})
const estimate = response.getEstimate()
if (estimate.hasInstant()) {
const instant = estimate.getInstant()
console.log('Ready instantly. Will receive:',
instant.getOrderMatch()?.getReceivingAmount()?.getValue())
}
if (estimate.hasDeferred()) {
const deferred = estimate.getDeferred()
if (deferred.hasSendingChannelDeposit()) {
const dep = deferred.getSendingChannelDeposit()
console.log('Sending-channel deposit:', dep?.getAmount()?.getValue(),
'fee:', dep?.getFee()?.getValue())
}
if (deferred.hasReceivingChannelLease()) {
const lease = deferred.getReceivingChannelLease()
console.log('Receiving-channel lease:', lease?.getAmount()?.getValue(),
'lease_fee:', lease?.getLeaseFee()?.getValue())
}
}
if (estimate.hasDualFundDeferred()) {
const dfd = estimate.getDualFundDeferred()
console.log('Dual-fund deposit needed:',
dfd?.getDualFundDeposit()?.getSelfAmount()?.getValue())
}
if (estimate.hasFeesHigherThanAmount()) {
const fhta = estimate.getFeesHigherThanAmount()
console.error('Fees exceed swap amount:',
'sending fee', fhta?.getSendingFee()?.getValue(),
'receiving fee', fhta?.getReceivingFee()?.getValue())
}
if (estimate.hasLeaseTooBig()) {
const ltb = estimate.getLeaseTooBig()
console.error('Lease unavailable:',
'needed', ltb?.getNeededLease()?.getValue(),
'available', ltb?.getAvailableLeaseLiquidity()?.getValue())
}
if (estimate.hasNoLiquidity()) {
console.error('No market liquidity for this pair')
}
Example Response (Instant):
{
"estimate": {
"instant": {
"order_match": {
"sending_amount": "0.1",
"receiving_amount": "285",
"price": "2850"
},
"receiving_withdrawal_fee": {
"fee": "0.0005",
"fee_payment_currency": "FEE_PAYMENT_CURRENCY_RECEIVING"
}
}
}
}
Example Response (Deferred):
{
"estimate": {
"deferred": {
"order_match": {
"sending_amount": "0.1",
"receiving_amount": "285",
"price": "2850"
},
"sending_channel_deposit": {
"amount": "0.1",
"unspendable_reserve": "0.00000546",
"fee": "0.000025",
"fee_payment_currency": "FEE_PAYMENT_CURRENCY_SENDING",
"counterparty": "02abc123..."
},
"receiving_channel_lease": {
"amount": "285",
"unspendable_reserve": "0.001",
"lease_fee": "0.3",
"pay_with_sending": true
}
}
}
}
Simple Swap
Execute a swap with automatic channel setup.
Method: SimpleSwap
Features:
- Automatically opens/deposits channels if needed
- Rents inbound liquidity if required
- Performs the swap when channels are ready
- Optionally withdraws funds back onchain
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
sending_currency | OrderbookCurrency | YES | Currency to send |
receiving_currency | OrderbookCurrency | YES | Currency to receive |
amount | SwapAmount | YES | Amount to swap |
price_change_tolerance | DecimalString | YES | Max price slippage |
withdraw_sending_funds | bool | YES | Auto-withdraw sent funds |
withdraw_receiving_funds | bool | YES | Auto-withdraw received funds |
Response:
| Field | Type | Description |
|---|---|---|
output | SimpleSwapOutput | Execution details |
SimpleSwapOutput:
| Field | Type | Description |
|---|---|---|
simple_swap_id | string | Unique swap identifier |
instant or deferred or dual_fund_deferred | - | Execution type |
Example Request:
TypeScript
const request = new SimpleSwapRequest()
request.setSendingCurrency({
network: { protocol: 1, id: '0a03cf40' },
assetId: '0x0000000000000000000000000000000000000000000000000000000000000000'
})
request.setReceivingCurrency({
network: { protocol: 2, id: '11155111' },
assetId: '0x0000000000000000000000000000000000000000'
})
request.setAmount({
from: { amount: { value: '10000000' } }
})
request.setPriceChangeTolerance('0.01')
request.setWithdrawSendingFunds(false)
request.setWithdrawReceivingFunds(true)
const response = await client.simpleSwap(request, {})
const output = response.getOutput()
const swapId = output.getSimpleSwapId()
console.log('Simple swap started:', swapId)
if (output.hasInstant()) {
const instant = output.getInstant()
console.log('Swap order:', instant.getOrderId())
}
if (output.hasDeferred()) {
console.log('Setting up channels...')
console.log('Subscribe to updates with SubscribeSimpleSwaps')
}
Go
req := &pb.SimpleSwapRequest{
SendingCurrency: &pb.OrderbookCurrency{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_BITCOIN,
Id: "0a03cf40",
},
AssetId: "0x0000000000000000000000000000000000000000000000000000000000000000",
},
ReceivingCurrency: &pb.OrderbookCurrency{
Network: &pb.Network{
Protocol: pb.Protocol_PROTOCOL_EVM,
Id: "11155111",
},
AssetId: "0x0000000000000000000000000000000000000000",
},
Amount: &pb.SwapAmount{
Amount: &pb.SwapAmount_From_{From: &pb.SwapAmount_From{Amount: &pb.DecimalString{Value: "10000000"}}},
},
PriceChangeTolerance: "0.01",
WithdrawSendingFunds: false,
WithdrawReceivingFunds: true,
}
resp, err := client.SimpleSwap(context.Background(), req)
if err != nil {
log.Fatalf("Error: %v", err)
}
output := resp.GetOutput()
swapId := output.GetSimpleSwapId()
log.Printf("Simple swap started: %s", swapId)
if instant := output.GetInstant(); instant != nil {
log.Printf("Swap order: %s", instant.GetOrderId())
}
if output.GetDeferred() != nil {
log.Println("Setting up channels...")
log.Println("Subscribe to updates with SubscribeSimpleSwaps")
}
Rust
let request = Request::new(SimpleSwapRequest {
sending_currency: Some(OrderbookCurrency {
network: Some(Network {
protocol: Protocol::Bitcoin as i32,
id: "0a03cf40".to_string(),
}),
asset_id: "0x0000000000000000000000000000000000000000000000000000000000000000".to_string(),
}),
receiving_currency: Some(OrderbookCurrency {
network: Some(Network {
protocol: Protocol::Evm as i32,
id: "11155111".to_string(),
}),
asset_id: "0x0000000000000000000000000000000000000000".to_string(),
}),
amount: Some(SwapAmount {
amount: Some(your_package::swap_amount::Amount::From(swap_amount::From { amount: Some(DecimalString { value: "10000000".to_string() }) })),
}),
price_change_tolerance: "0.01".to_string(),
withdraw_sending_funds: false,
withdraw_receiving_funds: true,
});
let response = client.simple_swap(request).await?.into_inner();
let output = response.output.unwrap();
let swap_id = output.simple_swap_id;
println!("Simple swap started: {}", swap_id);
match output.output {
Some(Output::Instant(instant)) => {
println!("Swap order: {}", instant.order_id);
}
Some(Output::Deferred(_)) => {
println!("Setting up channels...");
println!("Subscribe to updates with SubscribeSimpleSwaps");
}
_ => {}
}
Estimate Simple Swappable Amounts
Added 2026-04-29.
Returns the smallest and largest amounts that can currently be simple-swapped between two currencies, given the user's wallet balances, current orderbook liquidity, and the LP's leaseable capacity.
Use this to populate slider/min-max bounds in a UI, or to sanity-check an amount before calling SimpleSwap.
Method: EstimateSimpleSwappableAmounts
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
sending_currency | OrderbookCurrency | YES | Currency to sell |
receiving_currency | OrderbookCurrency | YES | Currency to buy |
price_change_tolerance | DecimalString | YES | Same semantic as on EstimateSimpleSwapRequest |
withdraw_sending_funds | bool | YES | Whether the eventual swap will withdraw sent funds on-chain |
withdraw_receiving_funds | bool | YES | Whether the eventual swap will withdraw received funds on-chain |
Response: EstimateSimpleSwappableAmountsResponse
| Field | Type | Description |
|---|---|---|
amounts | SimpleSwappableAmounts | Changed 2026-06-03 — now always present. When no feasible swap exists (no orderbook liquidity, fees exceed balance, solver infeasible, etc.), all four fields collapse to "0". |
SimpleSwappableAmounts:
| Field | Type | Description |
|---|---|---|
min_sending | DecimalString | Smallest sending amount the user can swap |
min_receiving | DecimalString | Receiving amount when sending min_sending |
max_sending | DecimalString | Largest sending amount the user can swap |
max_receiving | DecimalString | Receiving amount when sending max_sending |
Example Request:
import { EstimateSimpleSwappableAmountsRequest } from './proto/swap_pb'
// OrderbookCurrency.assetId form (verified against a live orderbook):
// • native asset → zero-padded 32-byte hex: 0x0000…0000
// • ERC-20 token → "erc20:<lowercase-contract-address>"
// NOT the ticker ("BTC") and NOT mixed-case ("ERC20:0xAbC…").
const request = new EstimateSimpleSwappableAmountsRequest()
request.setSendingCurrency({
protocol: 1, networkId: '0a03cf40',
assetId: '0x0000000000000000000000000000000000000000000000000000000000000000'
})
request.setReceivingCurrency({
protocol: 2, networkId: '11155111',
assetId: 'erc20:0x8cd0da3d001b013336918b8bc4e56d9dda1347e0'
})
request.setPriceChangeTolerance({ value: '0.01' })
request.setWithdrawSendingFunds(false)
request.setWithdrawReceivingFunds(true)
const response = await swap.estimateSimpleSwappableAmounts(request, {})
// As of 2026-06-03 `amounts` is always present; check max_sending=="0"
// instead of branching on absence.
const a = response.getAmounts()!
if (a.getMaxSending()?.getValue() === '0') {
console.log('No feasible swap right now (no liquidity, fees exceed balance, or solver infeasible)')
} else {
console.log(`Swappable: ${a.getMinSending()?.getValue()} – ${a.getMaxSending()?.getValue()} BTC`)
}
Example Response:
{
"amounts": {
"min_sending": "0.0001",
"min_receiving": "0.285",
"max_sending": "1.64",
"max_receiving": "4674"
}
}
Cancel Simple Swap
Cancels an ongoing simple swap operation. Useful when channel setup is taking too long, the user changes their mind, or the orderbook moves outside the configured price tolerance.
Only valid while the simple swap is still in setup. Once the underlying order has been created, use the standard order-cancellation flow (
CancelOrderon the orderbook) instead.
Method: CancelSimpleSwap
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
simple_swap_id | string | YES | ID returned from SimpleSwap |
Response: Empty.
Example Request:
import { CancelSimpleSwapRequest } from './proto/swap_pb'
const request = new CancelSimpleSwapRequest()
request.setSimpleSwapId('ss_abc123')
await client.cancelSimpleSwap(request, {})
console.log('Simple swap cancelled')
Example Response:
{}
Subscribe Simple Swaps
Subscribe to real-time updates for all simple swap operations.
Method: SubscribeSimpleSwaps
Parameters: None
Response: Stream of SimpleSwapUpdate
SimpleSwapUpdate:
| Field | Type | Description |
|---|---|---|
timestamp | Timestamp | Update time |
simple_swap_id | string | Swap identifier |
update | - | Update type (one of many variants) |
Update Types
Listed in roughly the order a swap emits them. Every payload is shown; an update with no payload shown is an empty marker.
| Update | Description |
|---|---|
waiting_for_deposit | The on-chain balance is short for the channel deposit — the user must send funds. { deposit_address, current_balance, needed_amount }. The swap parks here until it is covered |
deposit_received | The on-chain deposit arrived and the balance is now sufficient |
waiting_for_native_asset_deposit | The native balance is short for the on-chain fees — the user must send the native asset. { deposit_address, current_balance, needed_amount } |
native_asset_deposit_received | The native-asset deposit arrived |
setting_token_allowance | A token approval was broadcast because the current allowance did not cover the upcoming deposit or dual-fund — { asset_id, amount, txid } |
token_allowance_set | The token approval confirmed |
funding_sending_channel | Sending channel being funded, by opening or depositing — { txid, channel_id, amount, is_opening } |
leasing_receiving_channel | Receiving channel being leased — { txid, channel_id, amount } |
dual_funding_channel | Channel being dual-funded — { txid, channel_id, self_amount, counterparty_amount, is_opening } |
sending_channel_ready | Sending channel active — { channel_id } |
receiving_channel_ready | Receiving channel active — { channel_id } |
dual_fund_channel_ready | Dual-funded channel active — { channel_id } |
waiting_for_balances | Waiting for balances to be usable — { needed_sending, needed_receiving } |
balances_ready | Balances sufficient |
order_created | Swap order placed — { order_id } |
order_completed | Swap executed — { order_id, sent_amount, received_amount } |
withdrawing_sending_funds | Withdrawing sent funds — { txids[], self_amount, counterparty_amount } |
withdrawing_receiving_funds | Withdrawing received funds — { txids[], self_amount, counterparty_amount } |
withdrawing_dual_funded_funds | Withdrawing dual-fund — { txids[], sending_self_amount, sending_counterparty_amount, receiving_self_amount, receiving_counterparty_amount } |
sending_funds_withdrawn | Send withdrawal complete |
receiving_funds_withdrawn | Receive withdrawal complete |
dual_funded_funds_withdrawn | Dual-fund withdrawal complete |
simple_swap_completed | Swap fully complete |
simple_swap_error | Error occurred — { error } |
locking_onchain_htlc | (2026-06-24) Sending leg's on-chain HTLC lock tx broadcast — { txid } |
onchain_htlc_locked | (2026-06-24) Sending leg's on-chain lock reported and watched |
counterparty_lock_confirming | (2026-06-24) Counterparty's inbound on-chain lock gaining confirmations — { txid, current, required } |
counterparty_lock_observed | (2026-06-24) Counterparty's inbound on-chain lock observed and confirmed |
claiming_onchain_htlc | (2026-06-24) Receiving leg's on-chain claim tx broadcast — { txid } |
onchain_htlc_claimed | (2026-06-24) Receiving leg's on-chain claim observed |
refunding_onchain_htlc | (2026-06-24) Sending leg's on-chain HTLC refunded after timeout — { txid } |
withdrawing_funds_via_liquidity_service | (2026-07-23) One side's exit fee is being settled off-chain with the liquidity service, which broadcasts the withdrawal and pays the gas — { is_sending_side, fee, fee_payment_currency }. Appears instead of the local withdrawing_*_funds milestone when that side's exit_rail is EXIT_RAIL_LIQUIDITY_SERVICE. |
funding_sending_channel_via_liquidity_service | (2026-09-11) The sending channel is being funded by the liquidity service, which submits the wallet's signed permit and the funding transaction and pays the gas — { fee, fee_payment_currency }. Emitted when the deposit's rail is DEPOSIT_RAIL_LIQUIDITY_SERVICE; the fee comes out of the deposit in the sending asset. |
The twowaiting_for_*updates are the ones that need a UIEvery other milestone is progress you can render passively. These two are the swap telling you it cannot continue until the user acts — it is parked, not working — and each carries the address to send to and how much is still missing. A client that ignores them shows a swap that appears to hang forever.
The
*_onchain_htlc/*_lock_*variants appear only when a swap leg settles on-chain (opted viaSwapRequest.settlement) rather than through a channel. Channel-only swaps never emit them. See the settlement model.
Example:
TypeScript
const request = new SubscribeSimpleSwapsRequest()
const stream = client.subscribeSimpleSwaps(request, {})
stream.on('data', (update) => {
const timestamp = update.getTimestamp()
const swapId = update.getSimpleSwapId()
if (update.hasFundingSendingChannel()) {
const funding = update.getFundingSendingChannel()
console.log(`[${swapId}] Funding channel ${funding.getChannelId()}`)
console.log(` TX: ${funding.getTxid()}`)
console.log(` Amount: ${funding.getAmount()}`)
}
if (update.hasLeasingReceivingChannel()) {
const renting = update.getLeasingReceivingChannel()
console.log(`[${swapId}] Renting channel ${renting.getChannelId()}`)
console.log(` TX: ${renting.getTxid()}`)
}
if (update.hasSendingChannelReady()) {
console.log(`[${swapId}] Sending channel ready`)
}
if (update.hasReceivingChannelReady()) {
console.log(`[${swapId}] Receiving channel ready`)
}
if (update.hasWaitingForBalances()) {
const waiting = update.getWaitingForBalances()
console.log(`[${swapId}] Waiting for balances...`)
console.log(` Need sending: ${waiting.getNeededSending()}`)
console.log(` Need receiving: ${waiting.getNeededReceiving()}`)
}
if (update.hasBalancesReady()) {
console.log(`[${swapId}] Balances ready!`)
}
if (update.hasOrderCreated()) {
const order = update.getOrderCreated()
console.log(`[${swapId}] Order created: ${order.getOrderId()}`)
}
if (update.hasOrderCompleted()) {
const completed = update.getOrderCompleted()
console.log(`[${swapId}] Order completed!`)
console.log(` Sent: ${completed.getSentAmount()}`)
console.log(` Received: ${completed.getReceivedAmount()}`)
}
if (update.hasWithdrawingReceivingFunds()) {
const withdrawing = update.getWithdrawingReceivingFunds()
console.log(`[${swapId}] Withdrawing funds...`)
console.log(` TXs: ${withdrawing.getTxidsList()}`)
}
if (update.hasSimpleSwapCompleted()) {
console.log(`[${swapId}] ✓ Swap completed successfully!`)
}
if (update.hasSimpleSwapError()) {
const error = update.getSimpleSwapError()
console.error(`[${swapId}] ✗ Error: ${error.getError()}`)
}
})
stream.on('error', (err) => console.error('Stream error:', err))
Go
req := &pb.SubscribeSimpleSwapsRequest{}
stream, err := client.SubscribeSimpleSwaps(context.Background(), req)
if err != nil {
log.Fatalf("Error: %v", err)
}
for {
update, err := stream.Recv()
if err != nil {
log.Printf("Stream error: %v", err)
break
}
swapId := update.GetSimpleSwapId()
if funding := update.GetFundingSendingChannel(); funding != nil {
log.Printf("[%s] Funding channel %s", swapId, funding.GetChannelId())
log.Printf(" TX: %s", funding.GetTxid())
log.Printf(" Amount: %s", funding.GetAmount())
}
if renting := update.GetLeasingReceivingChannel(); renting != nil {
log.Printf("[%s] Renting channel %s", swapId, renting.GetChannelId())
log.Printf(" TX: %s", renting.GetTxid())
}
if update.GetSendingChannelReady() != nil {
log.Printf("[%s] Sending channel ready", swapId)
}
if update.GetReceivingChannelReady() != nil {
log.Printf("[%s] Receiving channel ready", swapId)
}
if waiting := update.GetWaitingForBalances(); waiting != nil {
log.Printf("[%s] Waiting for balances...", swapId)
log.Printf(" Need sending: %s", waiting.GetNeededSending())
log.Printf(" Need receiving: %s", waiting.GetNeededReceiving())
}
if update.GetBalancesReady() != nil {
log.Printf("[%s] Balances ready!", swapId)
}
if order := update.GetOrderCreated(); order != nil {
log.Printf("[%s] Order created: %s", swapId, order.GetOrderId())
}
if completed := update.GetOrderCompleted(); completed != nil {
log.Printf("[%s] Order completed!", swapId)
log.Printf(" Sent: %s", completed.GetSentAmount())
log.Printf(" Received: %s", completed.GetReceivedAmount())
}
if withdrawing := update.GetWithdrawingReceivingFunds(); withdrawing != nil {
log.Printf("[%s] Withdrawing funds...", swapId)
log.Printf(" TXs: %v", withdrawing.GetTxids())
}
if update.GetSimpleSwapCompleted() != nil {
log.Printf("[%s] Swap completed successfully!", swapId)
}
if swapError := update.GetSimpleSwapError(); swapError != nil {
log.Printf("[%s] Error: %s", swapId, swapError.GetError())
}
}
Rust
let request = Request::new(SubscribeSimpleSwapsRequest {});
let mut stream = client.subscribe_simple_swaps(request).await?.into_inner();
while let Some(update) = stream.message().await? {
let swap_id = &update.simple_swap_id;
if let Some(funding) = &update.funding_sending_channel {
println!("[{}] Funding channel {}", swap_id, funding.channel_id);
println!(" TX: {}", funding.txid);
println!(" Amount: {}", funding.amount);
}
if let Some(renting) = &update.leasing_receiving_channel {
println!("[{}] Renting channel {}", swap_id, renting.channel_id);
println!(" TX: {}", renting.txid);
}
if update.sending_channel_ready.is_some() {
println!("[{}] Sending channel ready", swap_id);
}
if update.receiving_channel_ready.is_some() {
println!("[{}] Receiving channel ready", swap_id);
}
if let Some(waiting) = &update.waiting_for_balances {
println!("[{}] Waiting for balances...", swap_id);
println!(" Need sending: {}", waiting.needed_sending);
println!(" Need receiving: {}", waiting.needed_receiving);
}
if update.balances_ready.is_some() {
println!("[{}] Balances ready!", swap_id);
}
if let Some(order) = &update.order_created {
println!("[{}] Order created: {}", swap_id, order.order_id);
}
if let Some(completed) = &update.order_completed {
println!("[{}] Order completed!", swap_id);
println!(" Sent: {}", completed.sent_amount);
println!(" Received: {}", completed.received_amount);
}
if let Some(withdrawing) = &update.withdrawing_receiving_funds {
println!("[{}] Withdrawing funds...", swap_id);
println!(" TXs: {:?}", withdrawing.txids);
}
if update.simple_swap_completed.is_some() {
println!("[{}] Swap completed successfully!", swap_id);
}
if let Some(error) = &update.simple_swap_error {
eprintln!("[{}] Error: {}", swap_id, error.error);
}
}
Common Workflows
One-click cross-chain swap
TypeScript
async function oneClickSwap(
client: SwapServiceClient,
fromCurrency: OrderbookCurrency,
toCurrency: OrderbookCurrency,
amount: string,
withdrawToWallet: boolean = true
) {
// 1. Estimate
const estimateReq = new EstimateSimpleSwapRequest()
estimateReq.setSendingCurrency(fromCurrency)
estimateReq.setReceivingCurrency(toCurrency)
estimateReq.setAmount({ sending: { value: amount } })
estimateReq.setPriceChangeTolerance('0.02') // 2% max slippage
estimateReq.setWithdrawSendingFunds(false)
estimateReq.setWithdrawReceivingFunds(withdrawToWallet)
const estimate = await client.estimateSimpleSwap(estimateReq, {})
if (estimate.getEstimate().hasNoLiquidity()) {
throw new Error('No liquidity available')
}
if (estimate.getEstimate().hasFeesHigherThanAmount()) {
throw new Error('Setup fees would exceed the swap amount')
}
if (estimate.getEstimate().hasLeaseTooBig()) {
const ltb = estimate.getEstimate().getLeaseTooBig()
throw new Error(
`Lease unavailable — needed ${ltb?.getNeededLease()?.getValue()}, ` +
`have ${ltb?.getAvailableLeaseLiquidity()?.getValue()}`
)
}
// 2. Execute
const swapReq = new SimpleSwapRequest()
swapReq.setSendingCurrency(fromCurrency)
swapReq.setReceivingCurrency(toCurrency)
swapReq.setAmount({ sending: { value: amount } })
swapReq.setPriceChangeTolerance('0.02')
swapReq.setWithdrawSendingFunds(false)
swapReq.setWithdrawReceivingFunds(withdrawToWallet)
const response = await client.simpleSwap(swapReq, {})
return response.getOutput().getSimpleSwapId()
}
// Example usage
const swapId = await oneClickSwap(
client,
btcCurrency,
ethCurrency,
'10000000', // 0.1 BTC
true // Auto-withdraw ETH to wallet
)
console.log('Swap started:', swapId)
console.log('Monitor progress with SubscribeSimpleSwaps')
Go
func oneClickSwap(
client pb.SwapServiceClient,
fromCurrency *pb.OrderbookCurrency,
toCurrency *pb.OrderbookCurrency,
amount string,
withdrawToWallet bool,
) (string, error) {
// 1. Estimate
estimateReq := &pb.EstimateSimpleSwapRequest{
SendingCurrency: fromCurrency,
ReceivingCurrency: toCurrency,
Amount: &pb.SwapAmount{
Amount: &pb.SwapAmount_From_{From: &pb.SwapAmount_From{Amount: &pb.DecimalString{Value: amount}}},
},
PriceChangeTolerance: "0.02", // 2% max slippage
WithdrawSendingFunds: false,
WithdrawReceivingFunds: withdrawToWallet,
}
estimate, err := client.EstimateSimpleSwap(context.Background(), estimateReq)
if err != nil {
return "", err
}
if estimate.GetEstimate().GetNoLiquidity() != nil {
return "", fmt.Errorf("no liquidity available")
}
if estimate.GetEstimate().GetFeesHigherThanAmount() != nil {
return "", fmt.Errorf("setup fees would exceed the swap amount")
}
if ltb := estimate.GetEstimate().GetLeaseTooBig(); ltb != nil {
return "", fmt.Errorf("lease unavailable — needed %s, have %s",
ltb.GetNeededLease().GetValue(),
ltb.GetAvailableLeaseLiquidity().GetValue())
}
// 2. Execute
swapReq := &pb.SimpleSwapRequest{
SendingCurrency: fromCurrency,
ReceivingCurrency: toCurrency,
Amount: &pb.SwapAmount{
Amount: &pb.SwapAmount_From_{From: &pb.SwapAmount_From{Amount: &pb.DecimalString{Value: amount}}},
},
PriceChangeTolerance: "0.02",
WithdrawSendingFunds: false,
WithdrawReceivingFunds: withdrawToWallet,
}
response, err := client.SimpleSwap(context.Background(), swapReq)
if err != nil {
return "", err
}
return response.GetOutput().GetSimpleSwapId(), nil
}
// Example usage
swapId, err := oneClickSwap(
client,
btcCurrency,
ethCurrency,
"10000000", // 0.1 BTC
true, // Auto-withdraw ETH to wallet
)
if err != nil {
log.Fatalf("Error: %v", err)
}
log.Printf("Swap started: %s", swapId)
log.Println("Monitor progress with SubscribeSimpleSwaps")
Rust
async fn one_click_swap(
client: &mut SwapServiceClient<Channel>,
from_currency: OrderbookCurrency,
to_currency: OrderbookCurrency,
amount: String,
withdraw_to_wallet: bool,
) -> Result<String, Box<dyn std::error::Error>> {
// 1. Estimate
let estimate_req = Request::new(EstimateSimpleSwapRequest {
sending_currency: Some(from_currency.clone()),
receiving_currency: Some(to_currency.clone()),
amount: Some(SwapAmount {
amount: Some(your_package::swap_amount::Amount::From(swap_amount::From { amount: Some(DecimalString { value: amount.clone().to_string() }) })),
}),
price_change_tolerance: "0.02".to_string(), // 2% max slippage
withdraw_sending_funds: false,
withdraw_receiving_funds: withdraw_to_wallet,
});
let estimate = client.estimate_simple_swap(estimate_req).await?.into_inner();
match &estimate.estimate.as_ref().unwrap().estimate {
Some(Estimate::NoLiquidity(_)) => {
return Err("No liquidity available".into());
}
Some(Estimate::FeesHigherThanAmount(_)) => {
return Err("Setup fees would exceed the swap amount".into());
}
Some(Estimate::LeaseTooBig(ltb)) => {
return Err(format!(
"Lease unavailable — needed {}, have {}",
ltb.needed_lease.as_ref().unwrap().value,
ltb.available_lease_liquidity.as_ref().unwrap().value
).into());
}
_ => {}
}
// 2. Execute
let swap_req = Request::new(SimpleSwapRequest {
sending_currency: Some(from_currency),
receiving_currency: Some(to_currency),
amount: Some(SwapAmount {
amount: Some(your_package::swap_amount::Amount::From(swap_amount::From { amount: Some(DecimalString { value: amount.to_string() }) })),
}),
price_change_tolerance: "0.02".to_string(),
withdraw_sending_funds: false,
withdraw_receiving_funds: withdraw_to_wallet,
});
let response = client.simple_swap(swap_req).await?.into_inner();
Ok(response.output.unwrap().simple_swap_id)
}
// Example usage
let swap_id = one_click_swap(
&mut client,
btc_currency,
eth_currency,
"10000000".to_string(), // 0.1 BTC
true, // Auto-withdraw ETH to wallet
).await?;
println!("Swap started: {}", swap_id);
println!("Monitor progress with SubscribeSimpleSwaps");
Monitor swap progress
TypeScript
async function waitForSwapCompletion(
client: SwapServiceClient,
swapId: string
): Promise<void> {
return new Promise((resolve, reject) => {
const stream = client.subscribeSimpleSwaps(new SubscribeSimpleSwapsRequest(), {})
stream.on('data', (update) => {
if (update.getSimpleSwapId() !== swapId) return
if (update.hasSimpleSwapCompleted()) {
stream.cancel()
resolve()
}
if (update.hasSimpleSwapError()) {
stream.cancel()
reject(new Error(update.getSimpleSwapError()?.getError()))
}
})
stream.on('error', (err) => reject(err))
})
}
// Usage
try {
await waitForSwapCompletion(client, swapId)
console.log('Swap completed successfully!')
} catch (error) {
console.error('Swap failed:', error.message)
}
Go
func waitForSwapCompletion(
client pb.SwapServiceClient,
swapId string,
) error {
req := &pb.SubscribeSimpleSwapsRequest{}
stream, err := client.SubscribeSimpleSwaps(context.Background(), req)
if err != nil {
return err
}
for {
update, err := stream.Recv()
if err != nil {
return err
}
if update.GetSimpleSwapId() != swapId {
continue
}
if update.GetSimpleSwapCompleted() != nil {
return nil
}
if swapError := update.GetSimpleSwapError(); swapError != nil {
return fmt.Errorf("swap error: %s", swapError.GetError())
}
}
}
// Usage
err := waitForSwapCompletion(client, swapId)
if err != nil {
log.Printf("Swap failed: %v", err)
} else {
log.Println("Swap completed successfully!")
}
Rust
async fn wait_for_swap_completion(
client: &mut SwapServiceClient<Channel>,
swap_id: String,
) -> Result<(), Box<dyn std::error::Error>> {
let request = Request::new(SubscribeSimpleSwapsRequest {});
let mut stream = client.subscribe_simple_swaps(request).await?.into_inner();
while let Some(update) = stream.message().await? {
if update.simple_swap_id != swap_id {
continue;
}
if update.simple_swap_completed.is_some() {
return Ok(());
}
if let Some(error) = update.simple_swap_error {
return Err(format!("Swap error: {}", error.error).into());
}
}
Err("Stream ended unexpectedly".into())
}
// Usage
match wait_for_swap_completion(&mut client, swap_id).await {
Ok(_) => println!("Swap completed successfully!"),
Err(e) => eprintln!("Swap failed: {}", e),
}
Price Change Tolerance
The price_change_tolerance parameter protects you from excessive slippage:
TypeScript
// Formula: (max_tolerated_price - intended_price) / intended_price
// Example: BTC/ETH at 28.5 ETH per BTC
// With 1% tolerance (0.01):
// - Max buy price: 28.5 * 1.01 = 28.785 ETH/BTC
// - Min sell price: 28.5 * 0.99 = 28.215 ETH/BTC
request.setPriceChangeTolerance('0.01') // 1% tolerance
Go
// Formula: (max_tolerated_price - intended_price) / intended_price
// Example: BTC/ETH at 28.5 ETH per BTC
// With 1% tolerance (0.01):
// - Max buy price: 28.5 * 1.01 = 28.785 ETH/BTC
// - Min sell price: 28.5 * 0.99 = 28.215 ETH/BTC
request.PriceChangeTolerance = "0.01" // 1% tolerance
Rust
// Formula: (max_tolerated_price - intended_price) / intended_price
// Example: BTC/ETH at 28.5 ETH per BTC
// With 1% tolerance (0.01):
// - Max buy price: 28.5 * 1.01 = 28.785 ETH/BTC
// - Min sell price: 28.5 * 0.99 = 28.215 ETH/BTC
request.price_change_tolerance = "0.01".to_string(); // 1% tolerance
Recommended values:
- 0.005 (0.5%) - Very tight, may fail in volatile markets
- 0.01 (1%) - Standard for most swaps
- 0.02 (2%) - More lenient for illiquid pairs
- 0.05 (5%) - Very lenient, use with caution
Best Practices
- Always estimate first.
EstimateSimpleSwapis what tells you which of the setup paths a swap will take, and what it will cost. - Branch on every estimate variant, not just the three success ones — see Error Handling.
- Set a realistic
price_change_tolerance— 1–2% for liquid pairs, more for illiquid ones. Too tight and the execute fails after the setup transactions have already been paid for. - Read the rails, not just the fees.
railandexit_raildecide whether the user pays gas from their own balance or a fee out of the swap. Show which one applies. - Quantise amounts to the market grid before sending. The orderbook rejects an over-precise amount rather than rounding it.
- Follow the swap with
SubscribeSimpleSwaps. A simple swap is many on-chain steps; the stream is the only view of where it is. - Cancel deliberately.
CancelSimpleSwapstops a swap that has not yet committed — it does not unwind transactions already broadcast. - Use
Swaponly when both channels already hold funds. It does no setup;SimpleSwapis the path that arranges liquidity for you.
Error Handling
Most "failures" of a simple swap are not errors: they are variants of the SimpleSwapEstimate one-of, returned by a successful EstimateSimpleSwap call. Branch on them explicitly — a switch that only handles instant / deferred / dual_fund_deferred will misreport every one of these.
| Estimate variant | What it means | What to do |
|---|---|---|
no_liquidity | The market cannot serve this pair | Try another amount or pair, or wait |
fees_higher_than_amount | Channel-setup fees exceed the amount being swapped | Swap more, or pick a pair that needs less setup |
lease_too_big | The receiving lease exceeds the provider's available or maximum capacity | Reduce the amount, wait for liquidity, or open the receiving channel yourself via the Node API |
insufficient_sending_balance | The wallet has nothing to send | Fund the sending asset |
insufficient_native_balance | The wallet holds the asset but cannot pay gas | Top up the native asset to required |
Genuine RPC errors follow the usual status codes. The two you will meet most:
| Error | Description | Solution |
|---|---|---|
FAILED_PRECONDITION | Price moved past price_change_tolerance between estimate and execute, or a channel was no longer usable | Re-estimate and retry; widen the tolerance if the pair is volatile |
INVALID_ARGUMENT | An amount carries more decimals than the market side allows | Quantise to that market's base_precision / quote_precision first — the orderbook rejects rather than rounds |