> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kaleidoswap.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Swap BTC and USDT on RGB

> Swap bitcoin and USDT-RGB with the Swap SDK, over Lightning with HTLC swaps or on chain with single-transaction atomic swaps, with an rgb-lib wallet handling the asset

Your user holds USDT on RGB, or wants to. RGB assets live in Bitcoin outputs, but their ownership is checked by RGB wallets, not by Bitcoin itself. So every RGB swap has two parts:

* **The Swap SDK** handles the Bitcoin side: it checks the maker's contract and builds the transaction.
* **Your rgb-lib wallet** handles the asset: it moves it, validates its history, and marks ("colors") the transaction with the asset. The SDK does not bundle rgb-lib.

## Which swap to use

| You want | Use | How it settles | If it fails |
| - | - | - | - |
| USDT-RGB on chain → bitcoin on Lightning | [HTLC swap](#htlc-swaps-over-lightning) (submarine) | A lockup carrying the asset, claimed when the maker pays your invoice | You refund the lockup after the deadline |
| Bitcoin on Lightning → USDT-RGB on chain | [HTLC swap](#htlc-swaps-over-lightning) (reverse) | The maker locks the asset; you claim it | Your Lightning payment is released |
| BTC ↔ USDT-RGB, both on chain | [Atomic swap](#atomic-swaps-on-l1) | One transaction moves both sides | Nothing moved: the transaction never confirmed |

## Setup

The code on this page assumes this setup. It connects to the signet maker and loads your swap mnemonic. See [Installation](https://docs.kaleidoswap.com/sdk/installation.md) and [Keys and recovery](https://docs.kaleidoswap.com/sdk/keys-and-recovery.md) if you have neither yet.

```typescript TypeScript theme={null}
import { init, SwapClient, SwapMasterKey, SwapScript, toJson } from "@kaleidorg/swap-sdk";

await init();

const network = "signet";
const makerUrl = "https://maker.signet.kaleidoswap.com/v2";
const bitcoinEsploraUrl = "https://esplora.signet.kaleidoswap.com";
const liquidEsploraUrl = "https://blockstream.info/liquidtestnet/api";

const client = new SwapClient(makerUrl);
const master = SwapMasterKey.fromSwapMnemonic(process.env.SWAP_MNEMONIC!, network);

// Two things your app provides:
//   nextSwapIndex(): Promise<bigint>  a persisted counter, never reused
//   store                             durable storage for swap records
```

```python Python theme={null}
import asyncio, os
import kaleidorg_swap_sdk as sdk

network = sdk.Network.SIGNET
client = sdk.SwapClient("https://maker.signet.kaleidoswap.com/v2", None)
master = sdk.SwapMasterKey.from_swap_mnemonic(os.environ["SWAP_MNEMONIC"], network)
```

## HTLC swaps over Lightning

### Pin the contract

Pass the expected RGB contract ID when you create the swap. It is required for RGB, checked locally against the response, and never sent to the maker. Pair cards do not identify the contract, so take the ID from your trusted wallet or asset registry.

```typescript TypeScript theme={null}
// USDT-RGB -> Lightning
const swap = await client.createSubmarineSwap(
  "signet",
  { from: "USDT-RGB", to: "BTC", invoice, refundPublicKey: refundKey.publicKey, pairHash: card.hash },
  usdtContractId,
  2_000n, // optional cap on the BTC collateral (default 1,000 sats), never sent to the maker
);

// Lightning -> USDT-RGB
const reverse = await client.createReverseSwap(
  "signet",
  { from: "BTC", to: "USDT-RGB", invoiceAmount, preimageHash: preimage.sha256, claimPublicKey: claimKey.publicKey, pairHash },
  usdtContractId,
);
```

```python Python theme={null}
request = sdk.CreateReverseRequest(
    invoice_amount=invoice_amount,
    _from=btc,
    to=usdt_rgb,               # Currency.USDT_RGB on the on-chain side
    preimage_hash=preimage.sha256(),
    claim_public_key=claim_key.public(),
    rgb_contract_id=usdt_contract_id,
)
```

Amounts: `rgb.amount` is in contract units (six decimals for USDT-RGB); `rgb.htlcSat` and fees are in sats. A submarine lockup carries a small amount of BTC collateral, capped at 1,000 sats unless you raise the cap. The maker receives all of it on a successful claim.

### Fund or accept the lock

* **Submarine:** your rgb-lib wallet funds the HTLC with the asset.
* **Reverse:** your rgb-lib wallet accepts the maker's lock consignment and validates the asset proofs. There is no `transaction.confirmed` event for reverse RGB swaps: enforce lock confirmations through your chain backend and rgb-lib before claiming. Before you pay the invoice, compare the response's `claimFeeRate` with current fees and the remaining timeout.

### Claim or refund

The SDK prepares the spend, your wallet colors it, and the SDK signs the HTLC input:

```typescript TypeScript theme={null}
const script = SwapScript.fromReverse("bitcoin", "signet", reverse, claimKey.publicKey);
const spend = await script.prepareRgbClaim({
  outputAddress: rgbWalletReceiveAddress,       // an RGB wallet witness receive
  funding: { kind: "htlcValue", feeRateSatVb: reverse.rgb.claimFeeRate },
  maxFee: 10_000n,
  swapId: reverse.id,
  makerBaseUrl,
  network: "signet",
  bitcoinEsploraUrl,
  lockupTxHex,
});

// 1. Color spend.template() with rgb-lib's psbt_op_prepare_with_expiry,
//    assigning the asset to paymentOutputIndex.
// 2. Return the colored PSBT and its allocations to the SDK:
const finalized = spend.finalizeClaim(coloredPsbt, claimKey.secretKey, preimage.preimage);
// 3. Sign any remaining wallet inputs, broadcast, then complete the rgb-lib
//    operation (psbt_op_mark_broadcast, psbt_op_apply, psbt_op_provide_receive_consignment).
```

* If the HTLC's sats cannot cover the fee, preparation fails with `rgb_fee_input_required`. Use `funding: { kind: "callerInputs" }`, add BTC inputs and change, and keep the new spend from `spend.fund(fundedPsbt)`. Fund before coloring or signing.
* Refunds use `prepareRgbRefund` and `finalizeRefund` after the timeout, with the original colored lock transaction.
* A Bitcoin broadcast alone does not complete the RGB receive. Finish the rgb-lib operation.

### Cooperative refunds

When the response advertises `rgb.cooperativeRefund === "rgb-coop-refund-v1"`, the maker can co-sign an early refund of a submarine swap with `prepareRgbCooperativeRefund` and `getRgbRefundPartialSig`. The CLTV refund remains the fallback. Without that capability the SDK refuses the cooperative path.

## Atomic swaps on L1

> [!NOTE]
>
> The signet maker does not offer atomic pairs yet; `atomic.pairs()` returns them empty. Test against a regtest maker.

An atomic swap moves both legs in one Bitcoin transaction. There is no HTLC and no refund. The SDK quotes, verifies, and tracks; rgb-lib accepts the offer, signs the proposal, and accepts the transfers.

```typescript TypeScript theme={null}
import { AtomicSwaps, SwapClient, ATOMIC_BTC_TO_USDT_RGB } from "@kaleidorg/swap-sdk";

const atomic = new AtomicSwaps(new SwapClient(makerUrl));

// 1. Quote. Throws unless the quote, and the rgb-lib offer inside it, match what you asked.
const quote = await atomic.quote(
  { pair: ATOMIC_BTC_TO_USDT_RGB, amount: 100_000n, direction: "from" },
  {
    assetId: usdtContractId,
    bitcoinNetwork: "Signet",       // rgb-lib's network name for your wallet
    maxNetworkFeeSat: 10_000n,
    minToAmount,                    // the least you accept, in base units: this is the price protection
    maxOfferTtlSecs: 3_600n,
  },
);
// Feed quote.offer to rgb-lib's accept_swap_offer.

// 2. Request. Pass every outpoint your wallet holds (rgb-lib's list_unspents).
const { proposal, makerInputs } = await atomic.request(quote.id, rgbLibRequest, walletOutpoints);
// Feed proposal to complete_swap_proposal.

// 3. Complete. The maker signs last and broadcasts.
const done = await atomic.complete(quote.id, rgbLibCompletion);
// Feed done.completion to accept_swap_transfers.

const final = await atomic.track(quote.id, { onStatus: (s) => console.log(s.status) });
```

What the SDK checks for you:

* The quote answers your pair, direction, amount, asset, and network, stays within your fee and offer-lifetime limits, and has not passed its deadline. The rgb-lib offer inside it moves the same amounts and asset as the quote, so what your wallet signs is what you were shown.
* The proposal lists none of your own coins as the maker's, and no coin twice.
* The completion broadcasts the transaction you agreed to.

Expectations are in base units: sats, or the contract's smallest unit (six decimals for USDT-RGB), never the pair card's display units. `maxServiceFee` bounds only the fee the maker declares; `minToAmount` and `maxFromAmount` bound the price.

rgb-lib messages cross the JavaScript and Python boundaries as JSON text, because their 64-bit values do not survive JavaScript numbers.

## If something goes wrong

* **HTLC swap, maker never pays (USDT-RGB → Lightning):** refund with `prepareRgbRefund` and `finalizeRefund` after the deadline, or earlier through a cooperative refund if the maker offers it.
* **HTLC swap, maker never locks (Lightning → USDT-RGB):** your Lightning payment is released. Nothing to do.
* **Atomic swap stops before `complete`:** the maker signs last and broadcasts, so before that nothing has moved.
* **Any RGB spend:** a Bitcoin broadcast alone does not finish the RGB transfer. Complete the rgb-lib operation, or the wallet will not show the asset.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.