RGB Swaps in Practice: Workflow and API
How RGB Swaps Work on the Lightning Network describes the protocol design: one circular Lightning payment, one payment_hash, and a taker at the conversion point.
This post describes the swap workflow and HTTP API that Control Panel exposes today, so third parties can integrate maker and taker flows, try them in a real environment, and send feedback.
For route structure, payment interception, and channel state, see the article linked above. Field reference and multi-hop details are in the appendices.
What the API does
Control Panel’s swap API lets two Lightning RGB nodes exchange BTC and one RGB asset at a pre-agreed price.
- Maker: creates the offer and starts the payment once the counterparty is ready.
- Taker: reviews and accepts the offer, then forwards the payment when it arrives.
- Offer terms are carried in a
swap_string(rgb-swap:v1:…orrgb-swap:v2:…), which can be sent by QR code, chat, or clipboard. - The API does not negotiate price. Parties agree on amounts and direction first; the API executes that agreement.
Roles are defined per trade, not per node. The same node can be maker on one trade and taker on another.
Main flow: single-hop swap
Single-hop is the common case: the two parties share a usable channel, and Control Panel can execute from local channel data. Integrators should implement single-hop first.
In order, a client should:
- Maker calls
POST /swap/offersand stores the returnedswap_stringandpayment_hash. - Maker sends the
swap_stringto the counterparty. - Taker optionally calls
POST /swap/decodeto preview terms, thenPOST /swap/accept. - Maker waits until local status is
Accepted. If acceptance was confirmed only out of band and no network notice arrived, setforce: trueon execute. - Maker calls
POST /swap/execute. - Both sides poll their own node with
GET /swap/{payment_hash}until status isSettledorFailed.
Endpoints used on the main path:
| Step | Endpoint | Caller |
|---|---|---|
| Create offer | POST /swap/offers | Maker |
| Preview terms (optional) | POST /swap/decode | Taker |
| Accept offer | POST /swap/accept | Taker |
| Execute swap | POST /swap/execute | Maker |
| Query progress | GET /swap/{payment_hash} | Each side on its own node |
List, cancel, and multi-hop create are in Appendix A.
Create and execute request bodies
When creating an offer, the maker specifies the counterparty, channel, asset, amounts, who pays RGB, and how long the offer is valid.
{
"counterparty_node_id": "<taker pubkey>",
"channel_scid": "<channel id, string>",
"contract_id": "contract:…",
"asset_amount": "<RGB amount, string>",
"btc_amount_msat": "<BTC amount in msat, string>",
"btc_carrier_amount_msat": "<carrier BTC in msat, string>",
"maker_gives_rgb": true,
"expiry_secs": 3600
}maker_gives_rgb: true: maker pays RGB; taker pays BTC.maker_gives_rgb: false: the opposite direction.btc_carrier_amount_msat: a small BTC amount so payment legs stay non-zero; it is not part of price negotiation.
Prefer swap_string on execute. For single-hop, payment_hash alone is also accepted.
{
"swap_string": "rgb-swap:v1:…",
"force": false
}Field notes and JSON encoding of large integers are in Appendix B.
Three points that matter in implementation
1. A successful execute response does not mean the trade is done
- HTTP 200 from
POST /swap/executemeans the node accepted and started sending the payment. - Whether balances moved and whether the trade succeeded is determined by later
GETresults. - If the
executeHTTP call itself fails (for example the counterparty has not accepted, or the channel is unusable), local status does not always becomeFailed; it may remainOfferedorAccepted. Treat the HTTP response as the result of this attempt; confirm the final outcome by polling.
2. Accept before execute
After the taker calls /swap/accept, the offer is registered on the taker node, and the node tries to notify the maker over the network. It is more reliable to execute only after the maker status moves from Offered to Accepted.
If the maker executes before the taker has accepted, the taker cannot recognize the payment and the trade fails later.
Exception: if acceptance was confirmed only out of band, or the counterparty node does not send a network notice, the maker may set force: true on execute. That flag explicitly skips the “counterparty has accepted” check and is not the default path.
3. One payment_hash, two independent records
The maker node and the taker node each store their own swap record. Both use payment_hash as the key, but status advances independently.
- Accept is written mainly on the taker; a network notice can at most move the maker to
Accepted. - Execute is written on the maker.
GET /swap/{payment_hash}on each side does not return the same object.
If the UI shows progress for both parties, it must query both nodes. Do not infer one side from the other.
Maker and taker status
Maker
Taker
Status meanings:
| Status | Meaning | UI guidance |
|---|---|---|
Offered | Maker only: offer created | Wait for counterparty accept |
Accepted | Accepted, or maker has learned of accept | Maker may execute; taker waits for payment |
InFlight | In progress | Show in progress; do not mark success |
Forwarded | May appear in older data | Treat like InFlight |
Settled | Success terminal state | Mark complete |
Failed | Failure terminal state | Show last_error; create a new offer |
Only Settled means the trade succeeded. InFlight and Forwarded are intermediate states.
Offer validity is created_at_unix_secs + expiry_secs. Expiry is checked mainly when the taker handles the intercepted payment. If the maker never executes, the taker may remain Accepted for a long time; the UI should show its own timeout. The server does not rewrite idle rows to Failed only because the clock passed.
Implementation checklist
Maker
- After creating an offer, store both
swap_stringandpayment_hash. - Send the
swap_stringto the counterparty. - Wait until local status is
Accepted, or execute withforce: trueafter out-of-band confirmation. - Continue on the success path only after a successful
executeHTTP response; on HTTP failure, show an error immediately instead of waiting only onGET. - Poll until status is
SettledorFailed.
Taker
- Call
decodefirst when a human-readable preview is needed. - After
accept, start polling the local record. - For
InFlightorForwarded, show in progress only. - Mark complete only on
Settled; onFailed, show the reason and ask the counterparty for a new offer (the oldpayment_hashcannot be reused).
Both sides
- Poll every 2–5 seconds until a terminal status.
- When showing both sides’ progress, query each node separately.
GETreturning 404 means this node has no record for that hash.
Multi-hop notes
Multi-hop swaps are created with POST /swap/offers/multihop; the caller supplies the full path. In the main article, only two rules matter:
- Execute must include the original
swap_string. The node does not keep the full multi-hop route under the payment hash alone; keeping onlypayment_hashmakes a later execute impossible. - Execute checks that the
swap_stringstill matches the original offer, so modified terms cannot be executed under the samepayment_hash.
Path fields and common errors are in Appendix C. Stabilize single-hop before adding multi-hop.
Summary
A practical integration follows this line:
- Maker creates an offer and sends the
swap_string. - Taker accepts (ideally the maker status also becomes
Accepted). - Maker executes; both sides poll until status is
Settled.
Protocol background: How RGB Swaps Work on the Lightning Network. Feedback is welcome on accept signaling, execute errors, and status presentation.
Atomic settlement is provided by Lightning. Product correctness still depends on three rules: each side keeps its own local record; accept before execute; use the execute HTTP response for this attempt, and poll to Settled for final completion.
Appendix A: API reference
| Method | Path | Caller | Purpose |
|---|---|---|---|
POST | /swap/offers | Maker | Create a single-hop offer |
POST | /swap/offers/multihop | Maker | Create a multi-hop offer |
POST | /swap/decode | Taker | Preview terms without storing |
POST | /swap/accept | Taker | Accept an offer |
POST | /swap/execute | Maker | Start the payment |
GET | /swap/{payment_hash} | Either | Read this node’s record |
GET | /swap/list | Either | List all local swaps (both roles) |
DELETE | /swap/{payment_hash} | Maker | Local cancel; only while Offered or Accepted; counterparty is not notified |
GET /swap/list uses role ("Maker" / "Taker").
Appendix B: SwapInfo and request fields
Create, accept, decode, list, and get all return SwapInfo (as in the live response):
| Field | Notes |
|---|---|
payment_hash | Primary key |
role | "Maker" or "Taker" |
status | See status section above |
counterparty_node_id | Counterparty node pubkey |
channel_scid | Channel used for the forward hop |
contract_id | RGB contract id; precision for display can come from contract APIs |
asset_amount | RGB amount |
btc_amount_msat | BTC amount for the exchange (msat) |
btc_carrier_amount_msat | Carrier BTC amount (msat) |
maker_gives_rgb | Trade direction |
expiry_secs / created_at_unix_secs | Offer validity window |
is_multihop | Multi-hop flag; when true, execute must send swap_string |
last_error | Failure reason when present |
channel_scid, amount fields, and created_at_unix_secs are JSON strings to avoid JavaScript precision loss. expiry_secs is a number.
Execute request fields:
| Field | Notes |
|---|---|
swap_string | Preferred; required for multi-hop |
payment_hash | Allowed instead of swap_string for single-hop only |
force | When true, skip the “counterparty accepted” check; default false |
execute may also reject before sending, for example if the counterparty has not accepted, is offline or the channel is unusable, or lacks capacity to forward the BTC leg.
Appendix C: Multi-hop
On create, provide:
rgb_path: maker → … → takerbtc_path: taker → … → maker
plus the same asset, amount, direction, and expiry fields as single-hop.
Implementation notes:
- Always persist both
swap_stringandpayment_hash. - Use
is_multihopin the UI: disable execute when the localswap_stringis missing. - Path finding is the caller’s responsibility; this API does not auto-route.
- If execute reports invalid amount, node, or
payment_hash, check whether theswap_stringwas modified or a single-hop hash was combined with a multi-hop string.