Bitlight Labs Blog
RGB Swaps in Practice: Workflow and API

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:… or rgb-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:

  1. Maker calls POST /swap/offers and stores the returned swap_string and payment_hash.
  2. Maker sends the swap_string to the counterparty.
  3. Taker optionally calls POST /swap/decode to preview terms, then POST /swap/accept.
  4. Maker waits until local status is Accepted. If acceptance was confirmed only out of band and no network notice arrived, set force: true on execute.
  5. Maker calls POST /swap/execute.
  6. Both sides poll their own node with GET /swap/{payment_hash} until status is Settled or Failed.

Endpoints used on the main path:

StepEndpointCaller
Create offerPOST /swap/offersMaker
Preview terms (optional)POST /swap/decodeTaker
Accept offerPOST /swap/acceptTaker
Execute swapPOST /swap/executeMaker
Query progressGET /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/execute means the node accepted and started sending the payment.
  • Whether balances moved and whether the trade succeeded is determined by later GET results.
  • If the execute HTTP call itself fails (for example the counterparty has not accepted, or the channel is unusable), local status does not always become Failed; it may remain Offered or Accepted. 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:

StatusMeaningUI guidance
OfferedMaker only: offer createdWait for counterparty accept
AcceptedAccepted, or maker has learned of acceptMaker may execute; taker waits for payment
InFlightIn progressShow in progress; do not mark success
ForwardedMay appear in older dataTreat like InFlight
SettledSuccess terminal stateMark complete
FailedFailure terminal stateShow 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

  1. After creating an offer, store both swap_string and payment_hash.
  2. Send the swap_string to the counterparty.
  3. Wait until local status is Accepted, or execute with force: true after out-of-band confirmation.
  4. Continue on the success path only after a successful execute HTTP response; on HTTP failure, show an error immediately instead of waiting only on GET.
  5. Poll until status is Settled or Failed.

Taker

  1. Call decode first when a human-readable preview is needed.
  2. After accept, start polling the local record.
  3. For InFlight or Forwarded, show in progress only.
  4. Mark complete only on Settled; on Failed, show the reason and ask the counterparty for a new offer (the old payment_hash cannot be reused).

Both sides

  • Poll every 2–5 seconds until a terminal status.
  • When showing both sides’ progress, query each node separately.
  • GET returning 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:

  1. Execute must include the original swap_string. The node does not keep the full multi-hop route under the payment hash alone; keeping only payment_hash makes a later execute impossible.
  2. Execute checks that the swap_string still matches the original offer, so modified terms cannot be executed under the same payment_hash.

Path fields and common errors are in Appendix C. Stabilize single-hop before adding multi-hop.


Summary

A practical integration follows this line:

  1. Maker creates an offer and sends the swap_string.
  2. Taker accepts (ideally the maker status also becomes Accepted).
  3. 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

MethodPathCallerPurpose
POST/swap/offersMakerCreate a single-hop offer
POST/swap/offers/multihopMakerCreate a multi-hop offer
POST/swap/decodeTakerPreview terms without storing
POST/swap/acceptTakerAccept an offer
POST/swap/executeMakerStart the payment
GET/swap/{payment_hash}EitherRead this node’s record
GET/swap/listEitherList all local swaps (both roles)
DELETE/swap/{payment_hash}MakerLocal 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):

FieldNotes
payment_hashPrimary key
role"Maker" or "Taker"
statusSee status section above
counterparty_node_idCounterparty node pubkey
channel_scidChannel used for the forward hop
contract_idRGB contract id; precision for display can come from contract APIs
asset_amountRGB amount
btc_amount_msatBTC amount for the exchange (msat)
btc_carrier_amount_msatCarrier BTC amount (msat)
maker_gives_rgbTrade direction
expiry_secs / created_at_unix_secsOffer validity window
is_multihopMulti-hop flag; when true, execute must send swap_string
last_errorFailure 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:

FieldNotes
swap_stringPreferred; required for multi-hop
payment_hashAllowed instead of swap_string for single-hop only
forceWhen 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 → … → taker
  • btc_path: taker → … → maker

plus the same asset, amount, direction, and expiry fields as single-hop.

Implementation notes:

  1. Always persist both swap_string and payment_hash.
  2. Use is_multihop in the UI: disable execute when the local swap_string is missing.
  3. Path finding is the caller’s responsibility; this API does not auto-route.
  4. If execute reports invalid amount, node, or payment_hash, check whether the swap_string was modified or a single-hop hash was combined with a multi-hop string.