Api

Swap API

Direct swaps and automated swaps

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

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:

NameTypeRequiredDescription
sending_currencyOrderbookCurrencyYESCurrency to send
receiving_currencyOrderbookCurrencyYESCurrency to receive
amountSwapAmountYESAmount to swap (sending or receiving)

SwapAmount Object (one of):

FieldTypeDescription
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 allowsfrom is 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 (see MarketInfo).

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.

to is 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:

FieldTypeDescription
order_matchOrderMatchEstimated 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:

NameTypeRequiredDescription
sending_currencyOrderbookCurrencyYESCurrency to send
receiving_currencyOrderbookCurrencyYESCurrency to receive
amountSwapAmountYESAmount to swap
settlementOrderSettlementNOAdded 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:

FieldTypeDescription
order_idstringSwap 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:

NameTypeRequiredDescription
sending_currencyOrderbookCurrencyYESCurrency to send
receiving_currencyOrderbookCurrencyYESCurrency to receive
amountSwapAmountYESAmount to swap
price_change_toleranceDecimalStringYESMax price slippage (e.g., "0.01" = 1%)
withdraw_sending_fundsboolYESAuto-withdraw sent funds after swap
withdraw_receiving_fundsboolYESAuto-withdraw received funds after swap

Response:

FieldTypeDescription
estimateSimpleSwapEstimateEstimate details (one of multiple variants)

SimpleSwapEstimate Variants:

Instant

Ready to swap immediately - you have sufficient offchain balance.

FieldTypeDescription
order_matchOrderMatchExpected execution
sending_withdrawal_feeWithdrawalFeeFee to withdraw sent funds (optional)
receiving_withdrawal_feeWithdrawalFeeFee to withdraw received funds (optional)

Deferred

Need to set up channels via deposits / leases first.

FieldTypeDescription
order_matchOrderMatchExpected execution
sending_channel_depositSendingChannelDepositRequired deposit (optional)
receiving_channel_leaseReceivingChannelLeaseRequired lease (optional)
sending_withdrawal_feeWithdrawalFeeWithdrawal fee (optional)
receiving_withdrawal_feeWithdrawalFeeWithdrawal fee (optional)
token_approval_feeDecimalStringNative-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_id means 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 one max_sendable reports.

DualFundDeferred

Need to dual-fund a channel.

FieldTypeDescription
order_matchOrderMatchExpected execution
dual_fund_depositDualFundDepositRequired dual-fund deposit
sending_withdrawal_feeWithdrawalFeeWithdrawal fee (optional)
receiving_withdrawal_feeWithdrawalFeeWithdrawal fee (optional)
token_approval_feeDecimalStringNative-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.

FieldTypeDescription
sending_feeDecimalStringTotal fee in sending currency
receiving_feeDecimalStringTotal fee in receiving currency

LeaseTooBig

The receiving lease needed exceeds available liquidity or the provider's max capacity.

FieldTypeDescription
needed_leaseDecimalStringRequired lease amount
available_lease_liquidityDecimalStringCurrently available
max_lease_capacityDecimalStringProvider'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).

FieldTypeDescription
availableDecimalStringThe spendable balance the wallet does have for the sending asset

Added 2026-04-27. If you match / switch on the estimate one-of, add a branch for InsufficientSendingBalance — 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.

FieldTypeDescription
requiredDecimalStringNative-asset amount the funding transactions need, including the broadcast envelope the chain's admission check reserves on top of the effective fee
availableDecimalStringNative-asset balance currently usable on the sending network

Gas and the sending asset are separate resources

This is neither InsufficientSendingBalance (the wallet has plenty of the asset) nor NoLiquidity (the market is fine). It is the specific case of holding, say, USDC on Arbitrum with no ETH to move it. Fund the wallet with required of the native asset and estimate again.

required is above the raw fee on purpose: an EVM node admits a transaction only when the sender can cover gas × max_fee_per_gas, so a balance that merely equals the expected fee is still rejected. Topping up to exactly required is what clears it.

When it can and cannot fire

Only 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 NotEnoughBalance and RentalTooBig variant. Those names no longer exist — they were renamed to FeesHigherThanAmount and LeaseTooBig respectively 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.

ValueConstantMeaning
0DEPOSIT_RAIL_UNSPECIFIEDTreated as DEPOSIT_RAIL_LOCAL
1DEPOSIT_RAIL_LOCALYour node broadcasts the funding transaction and pays gas from its on-chain native balance
2DEPOSIT_RAIL_LIQUIDITY_SERVICEThe 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.fee is charged in the sending asset, never in native gas — so fee_payment_currency on such a deposit is never FEE_PAYMENT_CURRENCY_NATIVE. The same holds for WithdrawalFee on the EXIT_RAIL_LIQUIDITY_SERVICE rail.

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.

FieldTypeDescription
feeDecimalStringThe fee amount
fee_payment_currencyFeePaymentCurrencyWhich asset pays it: ..._SENDING (1), ..._RECEIVING (2), or ..._NATIVE (3)
exit_railExitRail(added 2026-07-23) Which rail broadcasts this side's exit — see below

ExitRail — who broadcasts and pays gas

Added 2026-07-23.

ValueConstantMeaning
0EXIT_RAIL_UNSPECIFIEDTreated as EXIT_RAIL_LOCAL
1EXIT_RAIL_LOCALYour node broadcasts the exit transaction and pays gas from its on-chain native balance
2EXIT_RAIL_LIQUIDITY_SERVICEThe 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:

NameTypeRequiredDescription
sending_currencyOrderbookCurrencyYESCurrency to send
receiving_currencyOrderbookCurrencyYESCurrency to receive
amountSwapAmountYESAmount to swap
price_change_toleranceDecimalStringYESMax price slippage
withdraw_sending_fundsboolYESAuto-withdraw sent funds
withdraw_receiving_fundsboolYESAuto-withdraw received funds

Response:

FieldTypeDescription
outputSimpleSwapOutputExecution details

SimpleSwapOutput:

FieldTypeDescription
simple_swap_idstringUnique 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:

NameTypeRequiredDescription
sending_currencyOrderbookCurrencyYESCurrency to sell
receiving_currencyOrderbookCurrencyYESCurrency to buy
price_change_toleranceDecimalStringYESSame semantic as on EstimateSimpleSwapRequest
withdraw_sending_fundsboolYESWhether the eventual swap will withdraw sent funds on-chain
withdraw_receiving_fundsboolYESWhether the eventual swap will withdraw received funds on-chain

Response: EstimateSimpleSwappableAmountsResponse

FieldTypeDescription
amountsSimpleSwappableAmountsChanged 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:

FieldTypeDescription
min_sendingDecimalStringSmallest sending amount the user can swap
min_receivingDecimalStringReceiving amount when sending min_sending
max_sendingDecimalStringLargest sending amount the user can swap
max_receivingDecimalStringReceiving 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 (CancelOrder on the orderbook) instead.

Method: CancelSimpleSwap

Parameters:

NameTypeRequiredDescription
simple_swap_idstringYESID 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:

FieldTypeDescription
timestampTimestampUpdate time
simple_swap_idstringSwap 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.

UpdateDescription
waiting_for_depositThe 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_receivedThe on-chain deposit arrived and the balance is now sufficient
waiting_for_native_asset_depositThe 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_receivedThe native-asset deposit arrived
setting_token_allowanceA token approval was broadcast because the current allowance did not cover the upcoming deposit or dual-fund — { asset_id, amount, txid }
token_allowance_setThe token approval confirmed
funding_sending_channelSending channel being funded, by opening or depositing — { txid, channel_id, amount, is_opening }
leasing_receiving_channelReceiving channel being leased — { txid, channel_id, amount }
dual_funding_channelChannel being dual-funded — { txid, channel_id, self_amount, counterparty_amount, is_opening }
sending_channel_readySending channel active — { channel_id }
receiving_channel_readyReceiving channel active — { channel_id }
dual_fund_channel_readyDual-funded channel active — { channel_id }
waiting_for_balancesWaiting for balances to be usable — { needed_sending, needed_receiving }
balances_readyBalances sufficient
order_createdSwap order placed — { order_id }
order_completedSwap executed — { order_id, sent_amount, received_amount }
withdrawing_sending_fundsWithdrawing sent funds — { txids[], self_amount, counterparty_amount }
withdrawing_receiving_fundsWithdrawing received funds — { txids[], self_amount, counterparty_amount }
withdrawing_dual_funded_fundsWithdrawing dual-fund — { txids[], sending_self_amount, sending_counterparty_amount, receiving_self_amount, receiving_counterparty_amount }
sending_funds_withdrawnSend withdrawal complete
receiving_funds_withdrawnReceive withdrawal complete
dual_funded_funds_withdrawnDual-fund withdrawal complete
simple_swap_completedSwap fully complete
simple_swap_errorError 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 two waiting_for_* updates are the ones that need a UI

Every 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 via SwapRequest.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

  1. Always estimate first. EstimateSimpleSwap is what tells you which of the setup paths a swap will take, and what it will cost.
  2. Branch on every estimate variant, not just the three success ones — see Error Handling.
  3. 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.
  4. Read the rails, not just the fees. rail and exit_rail decide whether the user pays gas from their own balance or a fee out of the swap. Show which one applies.
  5. Quantise amounts to the market grid before sending. The orderbook rejects an over-precise amount rather than rounding it.
  6. Follow the swap with SubscribeSimpleSwaps. A simple swap is many on-chain steps; the stream is the only view of where it is.
  7. Cancel deliberately. CancelSimpleSwap stops a swap that has not yet committed — it does not unwind transactions already broadcast.
  8. Use Swap only when both channels already hold funds. It does no setup; SimpleSwap is 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 variantWhat it meansWhat to do
no_liquidityThe market cannot serve this pairTry another amount or pair, or wait
fees_higher_than_amountChannel-setup fees exceed the amount being swappedSwap more, or pick a pair that needs less setup
lease_too_bigThe receiving lease exceeds the provider's available or maximum capacityReduce the amount, wait for liquidity, or open the receiving channel yourself via the Node API
insufficient_sending_balanceThe wallet has nothing to sendFund the sending asset
insufficient_native_balanceThe wallet holds the asset but cannot pay gasTop up the native asset to required

Genuine RPC errors follow the usual status codes. The two you will meet most:

ErrorDescriptionSolution
FAILED_PRECONDITIONPrice moved past price_change_tolerance between estimate and execute, or a channel was no longer usableRe-estimate and retry; widen the tolerance if the pair is volatile
INVALID_ARGUMENTAn amount carries more decimals than the market side allowsQuantise to that market's base_precision / quote_precision first — the orderbook rejects rather than rounds

← Back to API Reference | Next: Node API →


Copyright © 2025