Atomic Swaps
Complete reference for BTC ⇄ RGB atomic swap endpoints. All paths carry the /api/v1 prefix. Swaps run on the node's native swap engine and settle the on-chain asset and the Lightning HTLC atomically.
Offer and execution flow
A typical swap:
- Maker creates an offer —
POST /swap/offers(single-hop) orPOST /swap/offers/multihop(multi-hop), returning aswap_stringandpayment_hash. - Taker decodes and accepts —
POST /swap/decodevalidates the offer,POST /swap/acceptsubmits the acceptance. - Maker executes —
POST /swap/execute; the swap moves toInFlighton return. - Query status — poll
GET /swap/{payment_hash}untilSettledorFailed.
maker_gives_rgb indicates whether the maker gives RGB: true means the maker gives RGB and the taker pays BTC (buy direction); false reverses it (sell direction).
Create an offer
POST /api/v1/swap/offers
Create a single-hop offer: the RGB leg and the BTC leg land on the same channel.
Request:
{
"maker_gives_rgb": true,
"contract_id": "contract:...",
"asset_amount": "100",
"btc_amount_msat": "3000000",
"btc_carrier_amount_msat": "3000000",
"counterparty_node_id": "02def456...",
"channel_scid": "144927727168913409",
"expiry_secs": 3600
}Parameter notes:
maker_gives_rgb(required): whether the maker gives RGBcontract_id(required): RGB contract ID being swappedasset_amount(required): RGB asset amount (raw units)btc_amount_msat(required): BTC counter-value (millisatoshis)btc_carrier_amount_msat(required): BTC carrier amount (millisatoshis) carrying the RGB paymentcounterparty_node_id(required): counterparty node pubkeychannel_scid(required): short channel ID the swap binds toexpiry_secs(required): offer validity in seconds
Response:
{
"swap_string": "swap1...",
"payment_hash": "abc123..."
}POST /api/v1/swap/offers/multihop
Create a multi-hop offer: the RGB leg and the BTC leg may bind different channels, described by rgb_path / btc_path.
Request:
{
"rgb_path": [
{ "node_id": "02def456...", "channel_scid": "144927727168913409" }
],
"btc_path": [
{ "node_id": "03abc789...", "channel_scid": "144948000000000000" }
],
"contract_id": "contract:...",
"asset_amount": "100",
"btc_amount_msat": "3000000",
"btc_carrier_amount_msat": "3000000",
"maker_gives_rgb": true,
"expiry_secs": 3600
}Parameter notes:
rgb_path/btc_path: the two route halves, each an array of hops withnode_id(that leg's endpoint node) andchannel_scid. The two names are historical and do not denote channel type: the channel holding the RGB asset and the channel carrying the BTC counter-value may live in different halves- The remaining fields match a single-hop offer (no
counterparty_node_id/channel_scid; the paths decide them)
Response: same as a single-hop offer (swap_string, payment_hash).
Fill must match direction
The node_id / channel_scid of the two multi-hop halves must match the swap direction (maker_gives_rgb). A wrong fill is rejected with errors such as SwapInvalidRoute or InvalidNodeId, or leaves the swap stuck in Offered. Validate the offer with POST /swap/decode before execution.
Decode, accept and execute
POST /api/v1/swap/decode
Decode a swap_string and return its direction, amounts, channels and status.
Request:
{
"swap_string": "swap1..."
}Response: a swap info object; see swap info below.
POST /api/v1/swap/accept
Accept an offer (called by the taker). The maker can execute only after acceptance.
Request:
{
"swap_string": "swap1..."
}Response:
{ "ok": true }POST /api/v1/swap/execute
Execute the swap (called by the maker).
Request:
{
"swap_string": "swap1...",
"force": false
}force is optional; true skips the strict peer-accepted check, for cases where the peer has accepted but this node has not observed it yet.
Response:
{
"ok": true,
"payment_hash": "abc123...",
"status": "InFlight"
}200 means the swap was initiated only; settlement requires polling GET /swap/{payment_hash}.
Swap status
GET /api/v1/swap/
Query swap info by payment hash.
Response:
{
"payment_hash": "abc123...",
"role": "Maker",
"status": "Settled",
"counterparty_node_id": "02def456...",
"channel_scid": "144927727168913409",
"contract_id": "contract:...",
"asset_amount": "100",
"btc_amount_msat": "3000000",
"btc_carrier_amount_msat": "3000000",
"maker_gives_rgb": true,
"expiry_secs": 3600,
"created_at_unix_secs": "1704067200",
"is_multihop": false,
"last_error": null
}GET /api/v1/swap/list
List this node's current swaps. The response is a list of swap info objects with the same fields.
Swap info
| Field | Description |
|---|---|
payment_hash | The swap's payment hash, also the status query key |
role | This node's role, Maker or Taker |
status | Swap status, see below |
counterparty_node_id | Counterparty node pubkey |
channel_scid | Bound short channel ID |
contract_id | RGB contract ID |
asset_amount | RGB asset amount |
btc_amount_msat | BTC counter-value (millisatoshis) |
btc_carrier_amount_msat | BTC carrier amount (millisatoshis) |
maker_gives_rgb | Whether the maker gives RGB |
expiry_secs | Offer validity in seconds |
created_at_unix_secs | Creation time (unix seconds) |
is_multihop | Whether this is a multi-hop swap |
last_error | Failure reason (present when status=Failed) |
Status values:
Offered: offer created, waiting for the peer to acceptAccepted: peer accepted, waiting for the maker to executeInFlight: swap in progressSettled: swap completed atomicallyFailed: swap failed, seelast_error
Multi-hop swaps
Multi-hop swaps are for cases where both legs cannot land on the same route, or where intermediate nodes/channels are needed:
- Create them with
POST /swap/offers/multihop, describing the RGB and BTC legs throughrgb_pathandbtc_path - Each hop settles atomically through a Lightning HTLC
- The final result: RGB or BTC is deducted per direction and the counter-value is received
Multi-hop swaps are more sensitive to channel capacity and routing: insufficient outbound capacity, a channel's per-HTLC maximum or reserves on any half can fail execution (for example HTLCMaximum, RetriesExhausted). Check capacity with the network graph and channel list before creating an offer.
Error handling
400: invalid swap parameters, invalid route, or peer has not accepted404: offer not found or expired409: swap balance or state conflict410: offer already accepted422: swap validation failed500: swap execution error
Swaps are atomic: channel balances do not change when execution fails. After a timeout or dropped connection, query the real state with GET /swap/{payment_hash} instead of retrying the execution blindly.
Examples
Create a single-hop offer (maker gives RGB)
curl -X POST https://<your-node-domain>/api/v1/swap/offers \
-H "Authorization: Bearer <node-credential>" \
-H "Content-Type: application/json" \
-d '{
"maker_gives_rgb": true,
"contract_id": "contract:...",
"asset_amount": "100",
"btc_amount_msat": "3000000",
"btc_carrier_amount_msat": "3000000",
"counterparty_node_id": "02def456...",
"channel_scid": "144927727168913409",
"expiry_secs": 3600
}'Accept and execute a swap
# Taker accepts
curl -X POST https://<your-node-domain>/api/v1/swap/accept \
-H "Authorization: Bearer <node-credential>" \
-H "Content-Type: application/json" \
-d '{ "swap_string": "swap1..." }'
# Maker executes
curl -X POST https://<your-node-domain>/api/v1/swap/execute \
-H "Authorization: Bearer <node-credential>" \
-H "Content-Type: application/json" \
-d '{ "swap_string": "swap1...", "force": false }'Query swap status
curl -X GET https://<your-node-domain>/api/v1/swap/abc123... \
-H "Authorization: Bearer <node-credential>"