--- url: /index.md description: Introduction to the Boltz API and overview of swap integration. --- # 👋 Introduction Boltz exposes a RESTful HTTP API that can be used to query information such as supported pairs as well as to create and monitor swaps. All swap clients, such as Boltz Web App, use Boltz API under the hood. ## Instances We offer Boltz on [regtest](https://github.com/BoltzExchange/regtest) for development & testing purposes and our production service on [mainnet](https://boltz.exchange). Our testnet deployment is deprecated and we currently do not provide support for it. Our REST APIs can be accessed at: * Mainnet: `https://api.boltz.exchange/` * Mainnet via [Tor](https://www.torproject.org/): `http://boltzzzbnus4m7mta3cxmflnps4fp7dueu2tgurstbvrbt6xswzcocyd.onion/api/` ## Lightning Nodes We operate the following lightning nodes: [CLN](https://amboss.space/node/02d96eadea3d780104449aca5c93461ce67c1564e2e1d73225fa67dd3b997a6018) | [LND](https://amboss.space/node/026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2) In the following sections we'll describe the available clients, SDKs, and libraries for our API, the REST API itself, walk through swap types & states, how to craft transactions, handle refunds and more. ## For AI Agents & LLMs These docs are also published in an LLM-friendly format following the [llms.txt](https://llmstxt.org/) convention: * [`/llms.txt`](/llms.txt): table of contents with a short description of each page, suitable for letting a model decide which page to fetch. * [`/llms-full.txt`](/llms-full.txt): the entire documentation concatenated into a single plain-text file, suitable for loading into a model's context. * Per-page Markdown sources: append `.md` to any page URL (for example `/api-v2.md` or `/lifecycle.md`) to get the raw Markdown. --- --- url: /libraries.md description: >- Official clients, SDKs and libraries for integrating with Boltz — start here instead of integrating from scratch. --- # 📙 Clients, SDKs & Libraries ::: danger ⚠️ DO NOT INTEGRATE BOLTZ API FROM SCRATCH ⚠️ Securely integrating Boltz API from scratch is extremely involved. Based on our experience, integration is complex, error-prone, and likely a multi-month effort with numerous edge cases to cover, which will result in **loss of funds** if not handled correctly. Therefore, we strongly recommend using one of the official clients, SDKs, or libraries listed below instead. **If the libraries below do not serve your needs and you are absolutely committed to the task, do not integrate Boltz API directly without first contacting us via [Support Chat](https://boltz.exchange/) or [Email](mailto:hi@bol.tz).** **We do not provide support for custom integrations that we were not involved with.** ::: ## Server-Side Integrations ### [Boltz Client](https://github.com/BoltzExchange/boltz-client) ⭐ Recommended **Covers most server-side use cases (e.g., swap server creating swaps for clients).** Our battle-tested reference client for accepting Lightning payments without running a node and rebalancing existing Lightning nodes; it also provides a full-fledged Go library for Boltz API. Used by e.g.: [Boltz BTCPay Plugin](https://github.com/BoltzExchange/boltz-btcpay-plugin/) Supported currencies: `LN`, `BTC`, `LBTC` ## Client-Side Integrations ### [Breez SDK - Nodeless](https://github.com/breez/breez-sdk-liquid) ⭐ Recommended **Covers most client-side use cases (e.g., mobile or browser app).** A polished end-to-end solution for developers that includes a wallet, notification system, WebAssembly support, and bindings for Kotlin, Flutter, Python, React Native, and Swift. Used by e.g.: [Klever Wallet](https://klever.io/) and [Misty Breez](https://breez.technology/misty/) Supported currencies: `LN`, `BTC`, `LBTC` ## Other Libraries ### [Boltz Core](https://github.com/BoltzExchange/boltz-core) (TypeScript) Our reference library in TypeScript. Used by e.g.: [Boltz Web App](https://github.com/BoltzExchange/boltz-web-app) and [Boltz Backend](https://github.com/BoltzExchange/boltz-backend) Supported currencies: `LN`, `BTC`, `LBTC`, `RBTC`, `TBTC`, `USDT` ### [Boltz Rust](https://github.com/SatoshiPortal/boltz-rust) (Rust) Our reference library in Rust, developed and maintained by the amazing folks at [Bull Bitcoin](https://www.bullbitcoin.com/) and Boltz. Features bindings for Python. Used by e.g.: [Bull Bitcoin Mobile](https://github.com/SatoshiPortal/bullbitcoin-mobile) and [Aqua Wallet](https://github.com/AquaWallet/aqua-wallet) via [Boltz Dart](https://github.com/SatoshiPortal/boltz-dart) Supported currencies: `LN`, `BTC`, `LBTC` --- --- url: /api-v2.md description: >- Endpoint reference for the latest Boltz REST API v2, recommended for all integrations. --- # 🤖 REST API (latest) This page introduces Boltz API v2, the latest and recommended API for all integrations. ## REST Endpoints The Swagger specifications of the latest Boltz REST API can be found :point\_right: [here](https://api.boltz.exchange/swagger) :point\_left:! ## Examples ::: danger ⚠️ WARNING: Code for educational purposes only The sample code below is not feature-complete, does not cover important edge cases, and almost certainly will lead to loss of funds if used in production. ::: Below are some examples covering the flow of a given swap type from beginning to end, using API v2 and its WebSocket. ### Submarine Swap (Chain -> Lightning) ::: code-group ```typescript [TypeScript Bitcoin] import zkpInit from '@vulpemventures/secp256k1-zkp'; import axios from 'axios'; import { crypto } from 'bitcoinjs-lib'; import bolt11 from 'bolt11'; import { Musig, SwapTreeSerializer, TaprootUtils } from 'boltz-core'; import { randomBytes } from 'crypto'; import { ECPairFactory } from 'ecpair'; import * as ecc from 'tiny-secp256k1'; import ws from 'ws'; // Endpoint of the Boltz instance to be used const endpoint = 'http://127.0.0.1:9001'; const webSocketEndpoint = 'ws://127.0.0.1:9004'; // The invoice you want to have paid const invoice = ''; const submarineSwap = async () => { const keys = ECPairFactory(ecc).makeRandom(); // Create a Submarine Swap const createdResponse = ( await axios.post(`${endpoint}/v2/swap/submarine`, { invoice, to: 'BTC', from: 'BTC', refundPublicKey: keys.publicKey.toString('hex'), }) ).data; console.log('Created swap'); console.log(createdResponse); console.log(); // Create a WebSocket and subscribe to updates for the created swap const webSocket = new ws(`${webSocketEndpoint}/v2/ws`); webSocket.on('open', () => { webSocket.send( JSON.stringify({ op: 'subscribe', channel: 'swap.update', args: [createdResponse.id], }), ); }); webSocket.on('message', async (rawMsg) => { const msg = JSON.parse(rawMsg.toString('utf-8')); if (msg.event !== 'update') { return; } console.log('Got WebSocket update'); console.log(msg); console.log(); switch (msg.args[0].status) { // "invoice.set" means Boltz is waiting for an onchain transaction to be sent case 'invoice.set': { console.log('Waiting for onchain transaction'); break; } // Create a partial signature to allow Boltz to do a key path spend to claim the mainchain coins case 'transaction.claim.pending': { console.log('Creating cooperative claim transaction'); // Get the information request to create a partial signature const claimTxDetails = ( await axios.get( `${endpoint}/v2/swap/submarine/${createdResponse.id}/claim`, ) ).data; // Verify that Boltz actually paid the invoice by comparing the preimage hash // of the invoice to the SHA256 hash of the preimage from the response const invoicePreimageHash = Buffer.from( bolt11 .decode(invoice) .tags.find((tag) => tag.tagName === 'payment_hash')!.data as string, 'hex', ); if ( !crypto .sha256(Buffer.from(claimTxDetails.preimage, 'hex')) .equals(invoicePreimageHash) ) { console.error('Boltz provided invalid preimage'); return; } const boltzPublicKey = Buffer.from( createdResponse.claimPublicKey, 'hex', ); // Create a musig signing instance const musig = new Musig(await zkpInit(), keys, randomBytes(32), [ boltzPublicKey, keys.publicKey, ]); // Tweak that musig with the Taptree of the swap scripts TaprootUtils.tweakMusig( musig, SwapTreeSerializer.deserializeSwapTree(createdResponse.swapTree).tree, ); // Aggregate the nonces musig.aggregateNonces([ [boltzPublicKey, Buffer.from(claimTxDetails.pubNonce, 'hex')], ]); // Initialize the session to sign the transaction hash from the response musig.initializeSession( Buffer.from(claimTxDetails.transactionHash, 'hex'), ); // Give our public nonce and the partial signature to Boltz await axios.post( `${endpoint}/v2/swap/submarine/${createdResponse.id}/claim`, { pubNonce: Buffer.from(musig.getPublicNonce()).toString('hex'), partialSignature: Buffer.from(musig.signPartial()).toString('hex'), }, ); break; } case 'transaction.claimed': console.log('Swap successful'); webSocket.close(); break; } }); }; (async () => { await submarineSwap(); })(); ``` ```typescript [TypeScript Liquid] import zkpInit from '@vulpemventures/secp256k1-zkp'; import axios from 'axios'; import { crypto } from 'bitcoinjs-lib'; import bolt11 from 'bolt11'; import { Musig, SwapTreeSerializer } from 'boltz-core'; import { TaprootUtils } from 'boltz-core/dist/lib/liquid'; import { randomBytes } from 'crypto'; import { ECPairFactory } from 'ecpair'; import * as ecc from 'tiny-secp256k1'; import ws from 'ws'; // Endpoint of the Boltz instance to be used const endpoint = 'http://127.0.0.1:9001'; const webSocketEndpoint = 'ws://127.0.0.1:9004'; // The invoice you want to have paid const invoice = ''; const submarineSwap = async () => { const keys = ECPairFactory(ecc).makeRandom(); // Create a Submarine Swap const createdResponse = ( await axios.post(`${endpoint}/v2/swap/submarine`, { invoice, to: 'BTC', from: 'L-BTC', refundPublicKey: keys.publicKey.toString('hex'), }) ).data; console.log('Created swap'); console.log(createdResponse); console.log(); // Create a WebSocket and subscribe to updates for the created swap const webSocket = new ws(`${webSocketEndpoint}/v2/ws`); webSocket.on('open', () => { webSocket.send( JSON.stringify({ op: 'subscribe', channel: 'swap.update', args: [createdResponse.id], }), ); }); webSocket.on('message', async (rawMsg) => { const msg = JSON.parse(rawMsg.toString('utf-8')); if (msg.event !== 'update') { return; } console.log('Got WebSocket update'); console.log(msg); console.log(); switch (msg.args[0].status) { // "invoice.set" means Boltz is waiting for an onchain transaction to be sent case 'invoice.set': { console.log('Waiting for onchain transaction'); break; } // Create a partial signature to allow Boltz to do a key path spend to claim the Liquid bitcoin case 'transaction.claim.pending': { console.log('Creating cooperative claim transaction'); // Get the information request to create a partial signature const claimTxDetails = ( await axios.get( `${endpoint}/v2/swap/submarine/${createdResponse.id}/claim`, ) ).data; // Verify that Boltz actually paid the invoice by comparing the preimage hash // of the invoice to the SHA256 hash of the preimage from the response const invoicePreimageHash = Buffer.from( bolt11 .decode(invoice) .tags.find((tag) => tag.tagName === 'payment_hash')!.data as string, 'hex', ); if ( !crypto .sha256(Buffer.from(claimTxDetails.preimage, 'hex')) .equals(invoicePreimageHash) ) { console.error('Boltz provided invalid preimage'); return; } const boltzPublicKey = Buffer.from( createdResponse.claimPublicKey, 'hex', ); // Create a musig signing instance const musig = new Musig(await zkpInit(), keys, randomBytes(32), [ boltzPublicKey, keys.publicKey, ]); // Tweak that musig with the Taptree of the swap scripts TaprootUtils.tweakMusig( musig, SwapTreeSerializer.deserializeSwapTree(createdResponse.swapTree).tree, ); // Aggregate the nonces musig.aggregateNonces([ [boltzPublicKey, Buffer.from(claimTxDetails.pubNonce, 'hex')], ]); // Initialize the session to sign the transaction hash from the response musig.initializeSession( Buffer.from(claimTxDetails.transactionHash, 'hex'), ); // Give our public nonce and the partial signature to Boltz await axios.post( `${endpoint}/v2/swap/submarine/${createdResponse.id}/claim`, { pubNonce: Buffer.from(musig.getPublicNonce()).toString('hex'), partialSignature: Buffer.from(musig.signPartial()).toString('hex'), }, ); break; } case 'transaction.claimed': console.log('Swap successful'); webSocket.close(); break; } }); }; (async () => { await submarineSwap(); })(); ``` ```typescript [TypeScript RBTC] import axios from 'axios'; import bolt11 from 'bolt11'; import EtherSwapArtifact from 'boltz-core/out/EtherSwap.sol/EtherSwap.json'; import { EtherSwap } from 'boltz-core/typechain/EtherSwap'; import { Contract, JsonRpcProvider, Wallet } from 'ethers'; import ws from 'ws'; const satoshiWeiFactor = 10n ** 10n; // Endpoint of the Boltz instance to be used const endpoint = 'http://127.0.0.1:9001'; const webSocketEndpoint = 'ws://127.0.0.1:9004'; // EVM config const providerEndpoint = 'http://127.0.0.1:8545'; const signerMnemonic = 'test test test test test test test test test test test junk'; // The invoice you want to have paid const invoice = 'lnbcrt1m1pnkl4sppp5vc2zmdw9x2xr63nngr73da6u863kfhmxc68nm7eycarwq760xgdqdqqcqzzsxqyz5vqsp5vz26y0ckx205lrm2d3mz23ynkp26kshumn0zjvc7xgkjtfh7l8mq9qxpqysgqn45lz8rn990f77ftrk3rvg03dlmnj9ze2ue9p3eypzau84w3wluz2g25ydj94kefur0v8ln6e4f76n29jsqjraatpq0mdazrl5klpdcp5v4dg8'; const submarineSwap = async () => { // Create a Submarine Swap const createdResponse = ( await axios.post(`${endpoint}/v2/swap/submarine`, { invoice, to: 'BTC', from: 'RBTC', }) ).data; console.log('Created swap'); console.log(createdResponse); console.log(); // Create a WebSocket and subscribe to updates for the created swap const webSocket = new ws(`${webSocketEndpoint}/v2/ws`); webSocket.on('open', () => { webSocket.send( JSON.stringify({ op: 'subscribe', channel: 'swap.update', args: [createdResponse.id], }), ); }); webSocket.on('message', async (rawMsg) => { const msg = JSON.parse(rawMsg.toString('utf-8')); if (msg.event !== 'update') { return; } console.log('Got WebSocket update'); console.log(msg); console.log(); switch (msg.args[0].status) { // "invoice.set" means Boltz is waiting for an onchain transaction to be sent case 'invoice.set': { const provider = new JsonRpcProvider(providerEndpoint); const contracts = ( await axios.get(`${endpoint}/v2/chain/RBTC/contracts`) ).data; const contract = new Contract( contracts.swapContracts.EtherSwap, EtherSwapArtifact.abi, Wallet.fromPhrase(signerMnemonic).connect(provider), ) as unknown as EtherSwap; const invoicePreimageHash = Buffer.from( bolt11 .decode(invoice) .tags.find((tag) => tag.tagName === 'payment_hash')!.data as string, 'hex', ); const tx = await contract.lock( invoicePreimageHash, createdResponse.claimAddress, createdResponse.timeoutBlockHeight, { value: BigInt(createdResponse.expectedAmount) * satoshiWeiFactor, }, ); console.log(`Sent RBTC in: ${tx.hash}`); break; } case 'transaction.claimed': // Boltz is batch claiming on EVM chains, so we can treat this status as success case 'transaction.claim.pending': console.log('Swap successful'); webSocket.close(); break; } }); }; (async () => { await submarineSwap(); })(); ``` ```go \[Go Bitcoin] package main import ( "bytes" "crypto/sha256" "encoding/json" "fmt" "os" "github.com/btcsuite/btcd/btcec/v2" "github.com/BoltzExchange/boltz-client/v2/pkg/boltz" "github.com/lightningnetwork/lnd/zpay32" ) const endpoint = "" const invoice = " Chain) ::: code-group ```typescript [TypeScript Bitcoin] import zkpInit from '@vulpemventures/secp256k1-zkp'; import axios from 'axios'; import { Transaction, address, crypto, initEccLib, networks, } from 'bitcoinjs-lib'; import { Musig, OutputType, SwapTreeSerializer, TaprootUtils, constructClaimTransaction, detectSwap, targetFee, } from 'boltz-core'; import { randomBytes } from 'crypto'; import { ECPairFactory } from 'ecpair'; import * as ecc from 'tiny-secp256k1'; import ws from 'ws'; // Endpoint of the Boltz instance to be used const endpoint = 'http://127.0.0.1:9001'; const webSocketEndpoint = 'ws://127.0.0.1:9004'; // Amount you want to swap const invoiceAmount = 100_000; // Address to which the swap should be claimed const destinationAddress = ''; const network = networks.regtest; const reverseSwap = async () => { initEccLib(ecc); // Create a random preimage for the swap; has to have a length of 32 bytes const preimage = randomBytes(32); const keys = ECPairFactory(ecc).makeRandom(); // Create a Reverse Swap const createdResponse = ( await axios.post(`${endpoint}/v2/swap/reverse`, { invoiceAmount, to: 'BTC', from: 'BTC', claimPublicKey: keys.publicKey.toString('hex'), preimageHash: crypto.sha256(preimage).toString('hex'), }) ).data; console.log('Created swap'); console.log(createdResponse); console.log(); // Create a WebSocket and subscribe to updates for the created swap const webSocket = new ws(`${webSocketEndpoint}/v2/ws`); webSocket.on('open', () => { webSocket.send( JSON.stringify({ op: 'subscribe', channel: 'swap.update', args: [createdResponse.id], }), ); }); webSocket.on('message', async (rawMsg) => { const msg = JSON.parse(rawMsg.toString('utf-8')); if (msg.event !== 'update') { return; } console.log('Got WebSocket update'); console.log(msg); console.log(); switch (msg.args[0].status) { // "swap.created" means Boltz is waiting for the invoice to be paid case 'swap.created': { console.log('Waiting invoice to be paid'); break; } // "transaction.mempool" means that Boltz sent an onchain transaction case 'transaction.mempool': { console.log('Creating claim transaction'); const boltzPublicKey = Buffer.from( createdResponse.refundPublicKey, 'hex', ); // Create a musig signing session and tweak it with the Taptree of the swap scripts const musig = new Musig(await zkpInit(), keys, randomBytes(32), [ boltzPublicKey, keys.publicKey, ]); const tweakedKey = TaprootUtils.tweakMusig( musig, SwapTreeSerializer.deserializeSwapTree(createdResponse.swapTree).tree, ); // Parse the lockup transaction and find the output relevant for the swap const lockupTx = Transaction.fromHex(msg.args[0].transaction.hex); const swapOutput = detectSwap(tweakedKey, lockupTx); if (swapOutput === undefined) { console.error('No swap output found in lockup transaction'); return; } // Create a claim transaction to be signed cooperatively via a key path spend const claimTx = targetFee(2, (fee) => constructClaimTransaction( [ { ...swapOutput, keys, preimage, cooperative: true, type: OutputType.Taproot, txHash: lockupTx.getHash(), }, ], address.toOutputScript(destinationAddress, network), fee, ), ); // Get the partial signature from Boltz const boltzSig = ( await axios.post( `${endpoint}/v2/swap/reverse/${createdResponse.id}/claim`, { index: 0, transaction: claimTx.toHex(), preimage: preimage.toString('hex'), pubNonce: Buffer.from(musig.getPublicNonce()).toString('hex'), }, ) ).data; // Aggregate the nonces musig.aggregateNonces([ [boltzPublicKey, Buffer.from(boltzSig.pubNonce, 'hex')], ]); // Initialize the session to sign the claim transaction musig.initializeSession( claimTx.hashForWitnessV1( 0, [swapOutput.script], [swapOutput.value], Transaction.SIGHASH_DEFAULT, ), ); // Add the partial signature from Boltz musig.addPartial( boltzPublicKey, Buffer.from(boltzSig.partialSignature, 'hex'), ); // Create our partial signature musig.signPartial(); // Witness of the input to the aggregated signature claimTx.ins[0].witness = [musig.aggregatePartials()]; // Broadcast the finalized transaction await axios.post(`${endpoint}/v2/chain/BTC/transaction`, { hex: claimTx.toHex(), }); break; } case 'invoice.settled': console.log('Swap successful'); webSocket.close(); break; } }); }; (async () => { await reverseSwap(); })(); ``` ```typescript [TypeScript Liquid] import zkpInit from '@vulpemventures/secp256k1-zkp'; import axios from 'axios'; import { Musig, OutputType, SwapTreeSerializer, detectSwap, targetFee, } from 'boltz-core'; import { TaprootUtils, constructClaimTransaction, init, } from 'boltz-core/dist/lib/liquid'; import { randomBytes } from 'crypto'; import { ECPairFactory } from 'ecpair'; import { Transaction, address, crypto, networks } from 'liquidjs-lib'; import * as ecc from 'tiny-secp256k1'; import ws from 'ws'; // Endpoint of the Boltz instance to be used const endpoint = 'http://127.0.0.1:9001'; const webSocketEndpoint = 'ws://127.0.0.1:9004'; // Amount you want to swap const invoiceAmount = 100_000; // Address to which the swap should be claimed const destinationAddress = ''; const network = networks.regtest; const reverseSwap = async () => { const zkp = await zkpInit(); init(zkp); // Create a random preimage for the swap; has to have a length of 32 bytes const preimage = randomBytes(32); const keys = ECPairFactory(ecc).makeRandom(); // Create a Reverse Swap const createdResponse = ( await axios.post(`${endpoint}/v2/swap/reverse`, { invoiceAmount, to: 'L-BTC', from: 'BTC', claimPublicKey: keys.publicKey.toString('hex'), preimageHash: crypto.sha256(preimage).toString('hex'), }) ).data; console.log('Created swap'); console.log(createdResponse); console.log(); // Create a WebSocket and subscribe to updates for the created swap const webSocket = new ws(`${webSocketEndpoint}/v2/ws`); webSocket.on('open', () => { webSocket.send( JSON.stringify({ op: 'subscribe', channel: 'swap.update', args: [createdResponse.id], }), ); }); webSocket.on('message', async (rawMsg) => { const msg = JSON.parse(rawMsg.toString('utf-8')); if (msg.event !== 'update') { return; } console.log('Got WebSocket update'); console.log(msg); console.log(); switch (msg.args[0].status) { // "swap.created" means Boltz is waiting for the invoice to be paid case 'swap.created': { console.log('Waiting invoice to be paid'); break; } // "transaction.mempool" means that Boltz sent an onchain transaction case 'transaction.mempool': { console.log('Creating claim transaction'); const boltzPublicKey = Buffer.from( createdResponse.refundPublicKey, 'hex', ); // Create a musig signing session and tweak it with the Taptree of the swap scripts const musig = new Musig(zkp, keys, randomBytes(32), [ boltzPublicKey, keys.publicKey, ]); const tweakedKey = TaprootUtils.tweakMusig( musig, SwapTreeSerializer.deserializeSwapTree(createdResponse.swapTree).tree, ); // Parse the lockup transaction and find the output relevant for the swap const lockupTx = Transaction.fromHex(msg.args[0].transaction.hex); const swapOutput = detectSwap(tweakedKey, lockupTx); if (swapOutput === undefined) { console.error('No swap output found in lockup transaction'); return; } // Create a claim transaction to be signed cooperatively via a key path spend const claimTx = targetFee(0.1, (fee) => constructClaimTransaction( [ { ...swapOutput, keys, preimage, cooperative: true, type: OutputType.Taproot, txHash: lockupTx.getHash(), blindingPrivateKey: Buffer.from( createdResponse.blindingKey, 'hex', ), }, ], address.toOutputScript(destinationAddress, network), fee, false, network, address.fromConfidential(destinationAddress).blindingKey, ), ); // Get the partial signature from Boltz const boltzSig = ( await axios.post( `${endpoint}/v2/swap/reverse/${createdResponse.id}/claim`, { index: 0, transaction: claimTx.toHex(), preimage: preimage.toString('hex'), pubNonce: Buffer.from(musig.getPublicNonce()).toString('hex'), }, ) ).data; // Aggregate the nonces musig.aggregateNonces([ [boltzPublicKey, Buffer.from(boltzSig.pubNonce, 'hex')], ]); // Initialize the session to sign the claim transaction musig.initializeSession( claimTx.hashForWitnessV1( 0, [swapOutput.script], [{ value: swapOutput.value, asset: swapOutput.asset }], Transaction.SIGHASH_DEFAULT, network.genesisBlockHash, ), ); // Add the partial signature from Boltz musig.addPartial( boltzPublicKey, Buffer.from(boltzSig.partialSignature, 'hex'), ); // Create our partial signature musig.signPartial(); // Witness of the input to the aggregated signature claimTx.ins[0].witness = [musig.aggregatePartials()]; // Broadcast the finalized transaction await axios.post(`${endpoint}/v2/chain/L-BTC/transaction`, { hex: claimTx.toHex(), }); break; } case 'invoice.settled': console.log('Swap successful'); webSocket.close(); break; } }); }; (async () => { await reverseSwap(); })(); ``` ```typescript [TypeScript RBTC] import axios from 'axios'; import { crypto } from 'bitcoinjs-lib'; import EtherSwapArtifact from 'boltz-core/out/EtherSwap.sol/EtherSwap.json'; import { EtherSwap } from 'boltz-core/typechain/EtherSwap'; import { randomBytes } from 'crypto'; import { Contract, JsonRpcProvider, Wallet } from 'ethers'; import ws from 'ws'; const satoshiWeiFactor = 10n ** 10n; // Endpoint of the Boltz instance to be used const endpoint = 'http://127.0.0.1:9001'; const webSocketEndpoint = 'ws://127.0.0.1:9004'; // EVM config const providerEndpoint = 'http://127.0.0.1:8545'; const signerMnemonic = 'test test test test test test test test test test test junk'; // Amount you want to swap const invoiceAmount = 100_000; const reverseSwap = async () => { // Create a random preimage for the swap; has to have a length of 32 bytes const preimage = randomBytes(32); const signer = Wallet.fromPhrase(signerMnemonic); // Create a Reverse Swap const createdResponse = ( await axios.post(`${endpoint}/v2/swap/reverse`, { invoiceAmount, to: 'RBTC', from: 'BTC', claimAddress: await signer.getAddress(), preimageHash: crypto.sha256(preimage).toString('hex'), }) ).data; console.log('Created swap'); console.log(createdResponse); console.log(); // Create a WebSocket and subscribe to updates for the created swap const webSocket = new ws(`${webSocketEndpoint}/v2/ws`); webSocket.on('open', () => { webSocket.send( JSON.stringify({ op: 'subscribe', channel: 'swap.update', args: [createdResponse.id], }), ); }); webSocket.on('message', async (rawMsg) => { const msg = JSON.parse(rawMsg.toString('utf-8')); if (msg.event !== 'update') { return; } console.log('Got WebSocket update'); console.log(msg); console.log(); switch (msg.args[0].status) { case 'swap.created': { console.log('Waiting for invoice to be paid'); break; } // "transaction.confirmed" means we can claim the RBTC case 'transaction.confirmed': { const provider = new JsonRpcProvider(providerEndpoint); const contracts = ( await axios.get(`${endpoint}/v2/chain/RBTC/contracts`) ).data; const contract = new Contract( contracts.swapContracts.EtherSwap, EtherSwapArtifact.abi, signer.connect(provider), ) as unknown as EtherSwap; const tx = await contract['claim(bytes32,uint256,address,uint256)']( preimage, BigInt(createdResponse.onchainAmount) * satoshiWeiFactor, createdResponse.refundAddress, createdResponse.timeoutBlockHeight, ); console.log(`Claimed RBTC in: ${tx.hash}`); break; } case 'invoice.settled': console.log('Swap successful'); webSocket.close(); break; } }); }; (async () => { await reverseSwap(); })(); ``` ```go \[Go Bitcoin] package main import ( "crypto/rand" "crypto/sha256" "encoding/json" "fmt" "os" "github.com/BoltzExchange/boltz-client/v2/pkg/boltz" "github.com/btcsuite/btcd/btcec/v2" ) const endpoint = "" const invoiceAmount = 100000 const destinationAddress = "
" // Swap from Lightning to BTC mainchain var toCurrency = boltz.CurrencyBtc var network = boltz.Regtest func printJson(v interface{}) { b, err := json.MarshalIndent(v, "", " ") if err != nil { fmt.Println(err) os.Exit(1) } fmt.Println(string(b)) } func reverseSwap() error { ourKeys, err := btcec.NewPrivateKey() if err != nil { return err } preimage := make([]byte, 32) _, err = rand.Read(preimage) if err != nil { return err } preimageHash := sha256.Sum256(preimage) boltzApi := &boltz.Api{URL: endpoint} reversePairs, err := boltzApi.GetReversePairs() if err != nil { return fmt.Errorf("could not get reverse pairs: %s", err) } pair := boltz.Pair{From: boltz.CurrencyBtc, To: toCurrency} pairInfo, err := boltz.FindPair(pair, reversePairs) if err != nil { return fmt.Errorf("could not find reverse pair: %s", err) } fees := pairInfo.Fees serviceFee := boltz.Percentage(fees.Percentage) fmt.Printf("Service Fee: %dsat\n", boltz.CalculatePercentage(serviceFee, invoiceAmount)) fmt.Printf("Network Fee: %dsat\n", fees.MinerFees.Lockup+fees.MinerFees.Claim) swap, err := boltzApi.CreateReverseSwap(boltz.CreateReverseSwapRequest{ From: boltz.CurrencyBtc, To: toCurrency, ClaimPublicKey: ourKeys.PubKey().SerializeCompressed(), PreimageHash: preimageHash[:], InvoiceAmount: invoiceAmount, PairHash: pairInfo.Hash, }) if err != nil { return fmt.Errorf("Could not create swap: %s", err) } boltzPubKey, err := btcec.ParsePubKey(swap.RefundPublicKey) if err != nil { return err } tree := swap.SwapTree.Deserialize() if err := tree.Init(toCurrency, true, ourKeys, boltzPubKey); err != nil { return err } if err := tree.Check(boltz.ReverseSwap, swap.TimeoutBlockHeight, preimageHash[:]); err != nil { return err } fmt.Println("Swap created") printJson(swap) boltzWs := boltzApi.NewWebsocket() if err := boltzWs.Connect(); err != nil { return fmt.Errorf("Could not connect to Boltz websocket: %w", err) } if err := boltzWs.Subscribe([]string{swap.Id}); err != nil { return err } for update := range boltzWs.Updates { parsedStatus := boltz.ParseEvent(update.Status) printJson(update) switch parsedStatus { case boltz.SwapCreated: fmt.Println("Waiting for invoice to be paid") break case boltz.TransactionMempool: lockupTransaction, err := boltz.NewTxFromHex(toCurrency, update.Transaction.Hex, nil) if err != nil { return err } vout, _, err := lockupTransaction.FindVout(network, swap.LockupAddress) if err != nil { return err } satsPerVbyte := float64(2) claimTransaction, _, err := boltz.ConstructTransaction( network, boltz.CurrencyBtc, []boltz.OutputDetails{ { SwapId: swap.Id, SwapType: boltz.ReverseSwap, Address: destinationAddress, LockupTransaction: lockupTransaction, Vout: vout, Preimage: preimage, PrivateKey: ourKeys, SwapTree: tree, Cooperative: true, }, }, boltz.Fee{SatsPerVbyte: &satsPerVbyte}, boltzApi, ) if err != nil { return fmt.Errorf("could not create claim transaction: %w", err) } txHex, err := claimTransaction.Serialize() if err != nil { return fmt.Errorf("could not serialize claim transaction: %w", err) } txId, err := boltzApi.BroadcastTransaction(toCurrency, txHex) if err != nil { return fmt.Errorf("could not broadcast transaction: %w", err) } fmt.Printf("Broadcast claim transaction: %s\n", txId) break case boltz.InvoiceSettled: fmt.Println("Swap succeeded", swap.Id) if err := boltzWs.Close(); err != nil { return err } break } } return nil } func main() { if err := reverseSwap(); err != nil { fmt.Println(err) os.Exit(1) } } ``` ::: ### Chain Swap (Chain -> Chain) ::: code-group ```typescript [TypeScript Bitcoin -> Liquid] import zkpInit, { Secp256k1ZKP } from '@vulpemventures/secp256k1-zkp'; import axios from 'axios'; import { Musig, OutputType, SwapTreeSerializer, TaprootUtils, detectSwap, targetFee, } from 'boltz-core'; import { TaprootUtils as LiquidTaprootUtils, constructClaimTransaction, init, } from 'boltz-core/dist/lib/liquid'; import { randomBytes } from 'crypto'; import { ECPairFactory, ECPairInterface } from 'ecpair'; import { Transaction, address, crypto, networks } from 'liquidjs-lib'; import * as ecc from 'tiny-secp256k1'; import ws from 'ws'; // Endpoint of the Boltz instance to be used const endpoint = 'http://127.0.0.1:9001'; const webSocketEndpoint = 'ws://127.0.0.1:9004'; // Amount you want to swap const userLockAmount = 100_000; // Address to which the swap should be claimed const destinationAddress = ''; const network = networks.regtest; const createClaimTransaction = ( zkp: Secp256k1ZKP, claimKeys: ECPairInterface, preimage: Buffer, createdResponse: any, lockupTransactionHex: string, ) => { const boltzPublicKey = Buffer.from( createdResponse.claimDetails.serverPublicKey, 'hex', ); // Create a musig signing session and tweak it with the Taptree of the swap scripts const musig = new Musig(zkp, claimKeys, randomBytes(32), [ boltzPublicKey, claimKeys.publicKey, ]); const tweakedKey = LiquidTaprootUtils.tweakMusig( musig, SwapTreeSerializer.deserializeSwapTree( createdResponse.claimDetails.swapTree, ).tree, ); // Parse the lockup transaction and find the output relevant for the swap const lockupTx = Transaction.fromHex(lockupTransactionHex); const swapOutput = detectSwap(tweakedKey, lockupTx); if (swapOutput === undefined) { throw 'No swap output found in lockup transaction'; } // Create a claim transaction to be signed cooperatively via a key path spend const transaction = targetFee(2, (fee) => constructClaimTransaction( [ { ...swapOutput, preimage, keys: claimKeys, cooperative: true, type: OutputType.Taproot, txHash: lockupTx.getHash(), blindingPrivateKey: Buffer.from( createdResponse.claimDetails.blindingKey, 'hex', ), }, ], address.toOutputScript(destinationAddress, network), fee, false, network, address.fromConfidential(destinationAddress).blindingKey, ), ); return { musig, transaction, swapOutput, boltzPublicKey }; }; const getBoltzPartialSignature = async ( zkp: Secp256k1ZKP, refundKeys: ECPairInterface, preimage: Buffer, createdResponse: any, claimPubNonce: Buffer, claimTransaction: Transaction, ) => { const serverClaimDetails = ( await axios.get(`${endpoint}/v2/swap/chain/${createdResponse.id}/claim`) ).data; // Sign the claim transaction of the server const boltzPublicKey = Buffer.from( createdResponse.lockupDetails.serverPublicKey, 'hex', ); const musig = new Musig(zkp, refundKeys, randomBytes(32), [ boltzPublicKey, refundKeys.publicKey, ]); TaprootUtils.tweakMusig( musig, SwapTreeSerializer.deserializeSwapTree( createdResponse.lockupDetails.swapTree, ).tree, ); musig.aggregateNonces([ [boltzPublicKey, Buffer.from(serverClaimDetails.pubNonce, 'hex')], ]); musig.initializeSession( Buffer.from(serverClaimDetails.transactionHash, 'hex'), ); const partialSig = musig.signPartial(); // When the server is happy with our signature, we get its partial signature // for our transaction in return const ourClaimDetails = ( await axios.post(`${endpoint}/v2/swap/chain/${createdResponse.id}/claim`, { preimage: preimage.toString('hex'), signature: { partialSignature: Buffer.from(partialSig).toString('hex'), pubNonce: Buffer.from(musig.getPublicNonce()).toString('hex'), }, toSign: { index: 0, transaction: claimTransaction.toHex(), pubNonce: claimPubNonce.toString('hex'), }, }) ).data; return { pubNonce: Buffer.from(ourClaimDetails.pubNonce, 'hex'), partialSignature: Buffer.from(ourClaimDetails.partialSignature, 'hex'), }; }; const chainSwap = async () => { const zkp = await zkpInit(); init(zkp); // Create a random preimage for the swap; has to have a length of 32 bytes const preimage = randomBytes(32); const claimKeys = ECPairFactory(ecc).makeRandom(); const refundKeys = ECPairFactory(ecc).makeRandom(); // Create a Chain Swap const createdResponse = ( await axios.post(`${endpoint}/v2/swap/chain`, { userLockAmount, to: 'L-BTC', from: 'BTC', preimageHash: crypto.sha256(preimage).toString('hex'), claimPublicKey: claimKeys.publicKey.toString('hex'), refundPublicKey: refundKeys.publicKey.toString('hex'), }) ).data; console.log('Created swap'); console.log(createdResponse); console.log(); // Create a WebSocket and subscribe to updates for the created swap const webSocket = new ws(`${webSocketEndpoint}/v2/ws`); webSocket.on('open', () => { webSocket.send( JSON.stringify({ op: 'subscribe', channel: 'swap.update', args: [createdResponse.id], }), ); }); webSocket.on('message', async (rawMsg) => { const msg = JSON.parse(rawMsg.toString('utf-8')); if (msg.event !== 'update') { return; } console.log('Got WebSocket update'); console.log(msg); console.log(); switch (msg.args[0].status) { // "swap.created" means Boltz is waiting for coins to be locked case 'swap.created': { console.log('Waiting for coins to be locked'); break; } // "transaction.server.mempool" means that Boltz sent an onchain transaction case 'transaction.server.mempool': { console.log('Creating claim transaction'); const claimDetails = createClaimTransaction( zkp, claimKeys, preimage, createdResponse, msg.args[0].transaction.hex, ); // Get the partial signature from Boltz const boltzPartialSig = await getBoltzPartialSignature( zkp, refundKeys, preimage, createdResponse, Buffer.from(claimDetails.musig.getPublicNonce()), claimDetails.transaction, ); // Aggregate the nonces claimDetails.musig.aggregateNonces([ [claimDetails.boltzPublicKey, boltzPartialSig.pubNonce], ]); // Initialize the session to sign the claim transaction claimDetails.musig.initializeSession( claimDetails.transaction.hashForWitnessV1( 0, [claimDetails.swapOutput.script], [ { value: claimDetails.swapOutput.value, asset: claimDetails.swapOutput.asset, }, ], Transaction.SIGHASH_DEFAULT, network.genesisBlockHash, ), ); // Add the partial signature from Boltz claimDetails.musig.addPartial( claimDetails.boltzPublicKey, boltzPartialSig.partialSignature, ); // Create our partial signature claimDetails.musig.signPartial(); // Witness of the input to the aggregated signature claimDetails.transaction.ins[0].witness = [ claimDetails.musig.aggregatePartials(), ]; // Broadcast the finalized transaction await axios.post(`${endpoint}/v2/chain/L-BTC/transaction`, { hex: claimDetails.transaction.toHex(), }); break; } case 'transaction.claimed': console.log('Swap successful'); webSocket.close(); break; } }); }; (async () => { await chainSwap(); })(); ``` ```typescript [TypeScript Bitcoin -> RBTC] import zkpInit from '@vulpemventures/secp256k1-zkp'; import axios from 'axios'; import { crypto } from 'bitcoinjs-lib'; import { Musig, SwapTreeSerializer, TaprootUtils } from 'boltz-core'; import EtherSwapArtifact from 'boltz-core/out/EtherSwap.sol/EtherSwap.json'; import { EtherSwap } from 'boltz-core/typechain/EtherSwap'; import { randomBytes } from 'crypto'; import ECPairFactory from 'ecpair'; import { Contract, JsonRpcProvider, Wallet } from 'ethers'; import * as ecc from 'tiny-secp256k1'; import ws from 'ws'; const satoshiWeiFactor = 10n ** 10n; // Endpoint of the Boltz instance to be used const endpoint = 'http://127.0.0.1:9001'; const webSocketEndpoint = 'ws://127.0.0.1:9004'; // Amount you want to swap const userLockAmount = 100_000; // EVM config const providerEndpoint = 'http://127.0.0.1:8545'; const signerMnemonic = 'test test test test test test test test test test test junk'; const chainSwap = async () => { // Create a random preimage for the swap; has to have a length of 32 bytes const preimage = randomBytes(32); const refundKeys = ECPairFactory(ecc).makeRandom(); const signer = Wallet.fromPhrase(signerMnemonic); const createdResponse = ( await axios.post(`${endpoint}/v2/swap/chain`, { userLockAmount, to: 'RBTC', from: 'BTC', claimAddress: await signer.getAddress(), preimageHash: crypto.sha256(preimage).toString('hex'), refundPublicKey: refundKeys.publicKey.toString('hex'), }) ).data; console.log('Created swap'); console.log(createdResponse); console.log(); const webSocket = new ws(`${webSocketEndpoint}/v2/ws`); webSocket.on('open', () => { webSocket.send( JSON.stringify({ op: 'subscribe', channel: 'swap.update', args: [createdResponse.id], }), ); }); webSocket.on('message', async (rawMsg) => { const msg = JSON.parse(rawMsg.toString('utf-8')); if (msg.event !== 'update') { return; } console.log('Got WebSocket update'); console.log(msg); console.log(); switch (msg.args[0].status) { case 'swap.created': { console.log('Waiting for coins to be locked'); break; } // "transaction.server.confirmed" means we can claim the RBTC case 'transaction.server.confirmed': { const provider = new JsonRpcProvider(providerEndpoint); const contracts = ( await axios.get(`${endpoint}/v2/chain/RBTC/contracts`) ).data; const contract = new Contract( contracts.swapContracts.EtherSwap, EtherSwapArtifact.abi, signer.connect(provider), ) as unknown as EtherSwap; const tx = await contract['claim(bytes32,uint256,address,uint256)']( preimage, BigInt(createdResponse.claimDetails.amount) * satoshiWeiFactor, createdResponse.claimDetails.refundAddress, createdResponse.claimDetails.timeoutBlockHeight, ); console.log(`Claimed RBTC in: ${tx.hash}`); break; } // We can help the server claim the BTC cooperatively in this status, // else the server will batch claim via script path on interval case 'transaction.claim.pending': { const claimDetails = ( await axios.get( `${endpoint}/v2/swap/chain/${createdResponse.id}/claim`, ) ).data; const boltzPubicKey = Buffer.from(claimDetails.publicKey, 'hex'); const musig = new Musig(await zkpInit(), refundKeys, randomBytes(32), [ boltzPubicKey, refundKeys.publicKey, ]); TaprootUtils.tweakMusig( musig, SwapTreeSerializer.deserializeSwapTree( createdResponse.lockupDetails.swapTree, ).tree, ); musig.aggregateNonces([ [boltzPubicKey, Buffer.from(claimDetails.pubNonce, 'hex')], ]); musig.initializeSession( Buffer.from(claimDetails.transactionHash, 'hex'), ); const partialSignature = musig.signPartial(); await axios.post( `${endpoint}/v2/swap/chain/${createdResponse.id}/claim`, { signature: { pubNonce: Buffer.from(musig.getPublicNonce()).toString('hex'), partialSignature: Buffer.from(partialSignature).toString('hex'), }, }, ); break; } case 'transaction.claimed': console.log('Swap successful'); webSocket.close(); break; } }); }; (async () => { await chainSwap(); })(); ``` ::: ## WebSocket The mainnet endpoint is available at: `wss://api.boltz.exchange/v2/ws` ### Swap Updates Instead of polling for swap status updates, clients can subscribe to updates with a WebSocket. To subscribe to swap status updates, send a message like below. `args` is a list of swap ids to subscribe to. ```json { "op": "subscribe", "channel": "swap.update", "args": ["swap id 1", "swap id 2"] } ``` Boltz API will respond with a message like below, to confirm that the subscription was created successfully. ```json { "event": "subscribe", "channel": "swap.update", "args": ["swap id 1", "swap id 2"] } ``` After the initial subscription confirmation message and whenever a swap status is updated, Boltz API will send a message containing details about the status update. `args` is a list of objects. These objects correspond to responses of `GET /swap/{id}`, but additionally contain the id of the swap. ```json { "event": "update", "channel": "swap.update", "args": [ { "id": "swap id 1", "status": "invoice.set" } ] } ``` To unsubscribe from the updates of one or more swaps, send an `unsubscribe` message. ```json { "op": "unsubscribe", "channel": "swap.update", "args": ["swap id 1"] } ``` The backend will respond with a message that contains all swap ids the WebSocket is still subscribed to. ```json { "op": "unsubscribe", "channel": "swap.update", "args": ["swap id 2"] } ``` ### BOLT12 Invoice Requests Clients can subscribe to invoice requests for BOLT12 offers they created via WebSockets. That is alternative to webhook calls for environments that can't receive webhook calls. To subscribe to invoice requests for specific offers, send a message like below. `args` is a list of objects, each containing the BOLT12 `offer` string and a schnorr signature by the signing key of the offer of the SHA256 hash of `SUBSCRIBE` serialized as HEX. ```json { "op": "subscribe", "channel": "invoice.request", "args": [ { "offer": "lno1...", "signature": "0fcdee..." } ] } ``` Boltz API will respond with a message like below to confirm the subscription, echoing the subscribed offers. ```json { "event": "subscribe", "channel": "invoice.request", "args": ["lno1..."] } ``` When an invoice for one of the subscribed offers is requested, it will send an `invoice.request` event. This message includes an unique `id` for this specific request, the `offer` for which an invoice is requested and the `invoiceRequest` itself serialized as HEX. ```json { "event": "request", "channel": "invoice.request", "args": [ { "id": "1234567890123456789", "offer": "lno1...", "invoiceRequest": "0010fbb94b0461..." } ] } ``` The client must then generate the requested BOLT12 invoice and send it back using the `invoice` operation, referencing the `id` of the invoice request. ```json { "op": "invoice", "id": "1234567890123456789", "invoice": "lni1..." } ``` In case the client can't create the BOLT12 invoice, it should send an error with a message that will be passed through to the requester of the invoice. ```json { "op": "invoice.error", "id": "1234567890123456789", "error": "could not create invoice" } ``` To unsubscribe from invoice requests for specific offers, send an `unsubscribe` message. `args` should contain the offer strings to unsubscribe from. ```json { "op": "unsubscribe", "channel": "invoice.request", "args": ["lno1..."] } ``` The backend will respond with a message containing all offer ids the WebSocket is still subscribed to for invoice requests. ```json { "op": "unsubscribe", "channel": "invoice.request", "args": ["lno2..."] } ``` ### Magic Routing Hints To help clients detect transactions to a magic routing hint address as quickly as possible, we emit an event whenever we observe a transaction to the magic routing hint in the mempool of a swap the client is subscribed to. ```json { "event": "update", "channel": "swap.update", "args": [ { "id": "", "status": "transaction.direct", "transaction": { "id": "", "hex": "" } } ], "timestamp": "1751717655632" } ``` ### Application Level Pings To ensure the connection is alive, besides the native WebSocket pings, Boltz API will also respond to application-level pings, which is useful when the WebSocket client cannot control the low-level WebSocket connection (like on browsers). To send a ping, send a message like below. ```json { "op": "ping" } ``` The backend will respond with a `pong` message. ```json { "event": "pong" } ``` ## Authentication Referral-related API endpoints require authentication using HMAC-based request signing. This authentication mechanism is used to ensure secure access to sensitive referral data and statistics. ### Authentication Headers Authenticated requests must include three headers: * `API-KEY` - The unique API key identifier * `TS` - Unix timestamp (in seconds) of when the request is made * `API-HMAC` - HMAC signature of the request ### HMAC Signature Generation The HMAC signature is generated using the HMAC-SHA256 algorithm with the API secret as the key. The message to be signed is constructed as follows: For GET requests: `timestamp + method + path` For POST requests: `timestamp + method + path + body` Where: * `timestamp` is the same Unix timestamp sent in the `TS` header * `method` is the HTTP method in uppercase (e.g., `GET`, `POST`) * `path` is the full request path including query parameters * `body` is the raw request body (only for POST requests) The resulting HMAC digest should be encoded as a hexadecimal string and sent in the `API-HMAC` header. ### Timestamp Validation The timestamp in the `TS` header must be within 60 seconds of the server's current time. Requests with timestamps outside this window will be rejected to prevent replay attacks. ### Examples ::: code-group ```typescript [TypeScript] import axios from 'axios'; import { createHmac } from 'crypto'; const endpoint = 'https://api.boltz.exchange'; const apiKey = ''; const apiSecret = ''; const sendAuthenticatedRequest = async (path: string) => { // Get current Unix timestamp in seconds const ts = Math.floor(Date.now() / 1000); // Create HMAC signature: timestamp + method + path const hmac = createHmac('sha256', apiSecret) .update(`${ts}GET${path}`) .digest('hex'); // Send request with authentication headers return axios.get(`${endpoint}${path}`, { headers: { TS: ts.toString(), 'API-KEY': apiKey, 'API-HMAC': hmac, }, }); }; // Example: Query referral stats const response = await sendAuthenticatedRequest('/v2/referral/stats'); console.log(JSON.stringify(response.data, undefined, 2)); ``` ```rust [Rust] use hmac::{Hmac, Mac}; use reqwest::Client; use sha2::Sha256; use std::time::{SystemTime, UNIX_EPOCH}; const ENDPOINT: &str = "https://api.boltz.exchange"; const API_KEY: &str = ""; const API_SECRET: &str = ""; async fn send_authenticated_request(client: &Client, path: &str) -> Result> { // Get current Unix timestamp in seconds let ts = SystemTime::now() .duration_since(UNIX_EPOCH)? .as_secs(); // Create HMAC signature: timestamp + method + path let mut mac = Hmac::::new_from_slice(API_SECRET.as_bytes())?; mac.update(format!("{}GET{}", ts, path).as_bytes()); let hmac = hex::encode(mac.finalize().into_bytes()); // Send request with authentication headers let response = client .get(format!("{}{}", ENDPOINT, path)) .header("TS", ts.to_string()) .header("API-KEY", API_KEY) .header("API-HMAC", hmac) .send() .await?; let json: serde_json::Value = response.json().await?; Ok(json) } #[tokio::main] async fn main() -> Result<(), Box> { let client = Client::new(); // Example: Query referral stats let response = send_authenticated_request(&client, "/v2/referral/stats").await?; println!("{}", serde_json::to_string_pretty(&response)?); Ok(()) } ``` ```go [Go] package main import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "time" ) const endpoint = "https://api.boltz.exchange" const apiKey = "" const apiSecret = "" func sendAuthenticatedRequest(path string) (map[string]any, error) { // Get current Unix timestamp in seconds ts := time.Now().Unix() // Create HMAC signature: timestamp + method + path message := fmt.Sprintf("%dGET%s", ts, path) mac := hmac.New(sha256.New, []byte(apiSecret)) mac.Write([]byte(message)) hmacSignature := hex.EncodeToString(mac.Sum(nil)) // Create HTTP client and request client := &http.Client{} req, err := http.NewRequest("GET", endpoint+path, nil) if err != nil { return nil, err } // Add authentication headers req.Header.Set("TS", fmt.Sprintf("%d", ts)) req.Header.Set("API-KEY", apiKey) req.Header.Set("API-HMAC", hmacSignature) // Send request resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() // Read and parse JSON response body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } var jsonResponse map[string]any if err := json.Unmarshal(body, &jsonResponse); err != nil { return nil, err } return jsonResponse, nil } func main() { // Example: Query referral stats response, err := sendAuthenticatedRequest("/v2/referral/stats") if err != nil { panic(err) } // Pretty print JSON response prettyJSON, err := json.MarshalIndent(response, "", " ") if err != nil { panic(err) } fmt.Println(string(prettyJSON)) } ``` ::: --- --- url: /lifecycle.md description: >- Swap types offered by Boltz and the states each swap type traverses during its lifecycle. --- # 🔁 Swap Types & States Boltz offers several different swap types. This document describes the types and different states a particular swap type traverses. ## Swap Types Boltz currently offers three types of [Atomic Swaps](https://en.bitcoin.it/wiki/Atomic_swap): * [Normal Submarine Swaps](lifecycle.md#normal-submarine-swaps) (Chain -> Lightning) * [Reverse Submarine Swaps](lifecycle.md#reverse-submarine-swaps) (Lightning -> Chain) * [Chain Swaps](lifecycle.md#chain-swaps) (Chain -> Chain) ## Swap States ### Normal Submarine Swaps Normal Submarine Swaps move bitcoin from the **chain to Lightning**. "Chain" can be the Bitcoin mainchain or, for instance, the Liquid sidechain. Typically, the user creates a Lightning invoice for Boltz, sends bitcoin to a *neutral* lockup address on the chain and Boltz sets up the Lightning payment. Once the Lightning payment was settled by the user's wallet/node, Boltz can use the revealed preimage to claim the bitcoin in the lockup address to complete the swap. When a Normal Submarine Swap is created, it passes through the following states: 1. `swap.created`: initial state of the swap; *optionally* the initial state can also be `invoice.set` in case the invoice was already specified in the `/createswap` request. [Boltz Web App](https://github.com/BoltzExchange/boltz-web-app) is an example for a client that sets the invoice with `/createswap` already. 2. `transaction.mempool`: a transaction that sends bitcoin to the chain address is found in the mempool, meaning user sent funds to the lockup chain address. 3. `transaction.confirmed`: the lockup transaction was included in a block. For mainchain swaps, Boltz always waits for one confirmation before continuing with the swap. The [`getpairs`](api-v1.md#supported-pairs) call provides amount limits for which Boltz accepts [0-conf](0-conf.md) per pair. 4. `invoice.set`: if the invoice was *not* set as part of the `/createswap` request, this state confirms that an invoice with the correct amount and hash was set. 5. Once the user's lockup transaction is included in a block (or found in the mempool if [0-conf](0-conf.md) applies), Boltz will try to pay the invoice provided by the user. When successful, Boltz obtains the preimage needed to claim the chain bitcoin. State of the Lightning payment is either: * `invoice.pending`: if paying the invoice is in progress * `invoice.paid`: if paying the invoice was successful or * `invoice.failedToPay`: if paying the invoice failed. In this case the user needs to broadcast a refund transaction to reclaim the locked up chain bitcoin. 6. `transaction.claim.pending`: This status indicates that Boltz is ready for the creation of a cooperative signature for a key path spend. Taproot Swaps are not claimed immediately by Boltz after the invoice has been paid, but instead Boltz waits for the API client to post a signature for a key path spend. If the API client does not cooperate in a key path spend, Boltz will eventually claim via the script path. 7. `transaction.claimed`: indicates that after the invoice was successfully paid, the chain bitcoin were successfully claimed *by Boltz*. This is the final status of a successful Normal Submarine Swap. If the user doesn't send chain bitcoin and the swap expires, Boltz will set the state of the swap to `swap.expired`, which means that it was cancelled and chain bitcoin shouldn't be sent anymore. When the client fails to lock coins in a way that is satisfactory to the server, like sending too little or too much, it will set the status to `transaction.lockupFailed`. For the swap states `invoice.failedToPay`, `swap.expired` where bitcoin were sent, and `transaction.lockupFailed`, the user needs to submit a refund transaction to reclaim the locked chain bitcoin. For more information about how Boltz API clients can construct and submit refund transactions for users, check the [Claim & Refund Transactions](claiming-swaps.md) section. The state `transaction.lockupFailed` is *not* final and changes to `swap.expired` after the swap expired; the failure reason will be kept and informs e.g. if the user sending too little or too much was the reason for the swap to fail. The states `invoice.failedToPay` and `swap.expired` are final. Boltz is *not* monitoring user's refund transactions. A 0-conf rejection of the client's lockup transaction is *not* a separate status: it is reported as `transaction.mempool` with `zeroConfRejected: true`. The `Refund` column marks states in which Boltz will provide a cooperative key-path refund signature; an `x` additionally requires the swap to be Taproot and no pending or successful lightning payment to exist for the swap. A non-cooperative script-path refund is always possible after the swap expired once the client has locked up coins. | Status | Cooperative refund | | --------------------------- | ------------------ | | `swap.created` | | | `invoice.set` | | | `transaction.mempool` | | | `transaction.confirmed` | | | `invoice.pending` | | | `invoice.paid` | | | `invoice.failedToPay` | x | | `transaction.claim.pending` | | | `transaction.claimed` | | | `transaction.lockupFailed` | x | | `swap.expired` | x | ### Reverse Submarine Swaps Reverse Submarine Swaps move bitcoin from **Lightning to the chain**. Again, "chain" can refer to the Bitcoin mainchain or, for instance, the Liquid sidechain. Reverse Submarine Swaps start with the client generating a preimage, then calculating the SHA256 hash of it and sending that hash to Boltz. With this hash, Boltz creates a hold invoice that can only be settled when the preimage is revealed by the user. The user pays the invoice, but the Lightning payment doesn't execute, because Boltz doesn't know the preimage yet. Next, Boltz locks up chain bitcoin using the same hash so that these can be claimed with the previously generated preimage by the client. Once the client successfully invoked the API call to get Boltz's partial signature for the claim transaction and supplied the preimage, Boltz can then use the preimage to settle the Lightning invoice. The [Claim & Refund Transactions](claiming-swaps.md) section contains details about how Boltz API clients can construct claim transactions for their users. The following states are traversed in the course of a Reverse Submarine Swap: 1. `swap.created`: initial state of a newly created Reverse Submarine Swap 2. `minerfee.paid`: optional and currently not enabled on Boltz. If Boltz requires prepaying miner fees via a separate Lightning invoice, this state is set when the miner fee invoice was successfully paid 3. `transaction.mempool`: Boltz's lockup transaction is found in the mempool which will only happen after the user paid the Lightning hold invoice 4. `transaction.confirmed`: the lockup transaction was included in a block. This state is skipped, if the client optionally accepts the transaction without confirmation. Boltz broadcasts chain transactions non-RBF only. 5. `invoice.settled`: the client successfully invoked the API call to get Boltz's partial signature for the claim transaction and supplied the preimage and Boltz then used the preimage to settle the Lightning invoice. This is the final status of a successful Reverse Submarine Swap. Boltz does not track if the client broadcasts the claim transaction. The status `invoice.expired` is set when the invoice of Boltz expired and pending HTLCs are cancelled. Boltz invoices currently expire after 50% of the swap expiry. If the swap expires without the lightning invoice being paid, the final status of the swap will be `swap.expired`. In the unlikely event that Boltz is unable to lock the agreed amount of chain bitcoin after the user set up the payment to the provided Lightning invoice, the final status of the swap will be `transaction.failed` and the pending Lightning HTLC will be cancelled. The Lightning bitcoin automatically bounce back to the user, no further action or refund is required and the user didn't pay any fees. If the user successfully set up the Lightning payment and Boltz successfully locked up bitcoin on the chain, but the user did not claim the locked chain bitcoin until swap expiry, Boltz will automatically refund its own locked chain bitcoin. The final status of such a swap will be `transaction.refunded`. The `Claim` column marks states in which Boltz will provide a cooperative key-path claim signature; an `x` additionally requires the swap to be Taproot. A non-cooperative script-path claim is always possible while Boltz's lockup is spendable. | Status | Cooperative claim | | ----------------------- | ----------------- | | `swap.created` | | | `minerfee.paid` | | | `transaction.mempool` | x | | `transaction.confirmed` | x | | `invoice.settled` | x | | `invoice.expired` | | | `transaction.failed` | | | `swap.expired` | | | `transaction.refunded` | | ### Chain Swaps Chain Swaps move bitcoin between **two different chains**, e.g. between the Bitcoin mainchain and Liquid. Chain Swaps are similar to Reverse Submarine Swaps, but without lightning and both sides being onchain. First, the API client sends a SHA256 hash of a 32 bytes long preimage and two public keys to the server. One key is used to sign a transaction on the chain the user wants to receive on, the other key is needed to sign a refund transaction on the chain the client locks coins on, in case the swap fails. Based on the details provided to the server, it creates one address for the client to lock coins and one for the server to lock coins. When the server locks coins, the client can claim those to its wallet by invoking Boltz's API to get Boltz's partial signature for the claim transaction and revealing the preimage. By revealing the preimage, the server can claim the coins the user locked. It is the client's responsibility to broadcast the claim transaction, Boltz does not track if the client broadcasts the claim transaction. All Chain Swaps are Taproot based, so instead of claiming by revealing scripts and secrets onchain, a key path spend can be done by the server and client cooperating to create a single signature together. More details about this can be found in the [Claim & Refund Transactions](claiming-swaps.md) section. A Chain Swap has the following states: 1. `swap.created`: initial state of the swap 2. `transaction.mempool`: the lockup transaction of the client was found in the mempool 3. `transaction.confirmed`: the lockup transaction of the client was confirmed in a block. When the server accepts 0-conf, for the lockup transaction, this state is skipped 4. `transaction.server.mempool`: the lockup transaction of the server has been broadcast 5. `transaction.server.confirmed`: the lockup transaction of the server has been included in a block 6. `transaction.claim.pending`: Boltz is ready to cooperatively sign for a key path spend. This state is entered once Boltz knows the preimage, which it learns when the client claims non-cooperatively on-chain. If the client does not provide a cooperative signature, Boltz will eventually claim via the script path in a batch transaction. 7. `transaction.claimed`: the server claimed the coins that the client locked If the user doesn't send chain bitcoin and the swap expires, Boltz will set the state of the swap to `swap.expired`, which means that it was cancelled and chain bitcoin shouldn't be sent anymore. When the client fails to lock coins in a way that is satisfactory to the server, like sending too little or too much, it will set the status to `transaction.lockupFailed`. When a Chain Swap enters the `transaction.lockupFailed` state due to the client sending an amount that differs from what was expected, the swap becomes eligible for renegotiation instead of requiring an immediate refund. This feature allows the swap to be salvaged by recalculating the server's lockup amount based on the actual amount the client locked. API clients can renegotiate a Chain Swap in state `transaction.lockupFailed` using the quote endpoint: 1. **Get a new quote**: The client calls `GET /swap/chain/{id}/quote` to retrieve a new quote based on the actual amount locked by the client 2. **Accept the quote**: If the client agrees with the new quote, they call `POST /swap/chain/{id}/quote` with the quoted amount. Boltz validates that the quote hasn't changed and that sufficient liquidity is available, then proceeds to lock the adjusted amount on the destination chain, allowing the swap to continue as usual. A Chain Swap is eligible for renegotiation only if: * The swap status is `transaction.lockupFailed` * The status was set because of over- or underpayment * No refund signature has been created yet for the swap * The actual amount locked by the client respects the pair's minimal limit and does not exceed the maximal limit beyond the configured positive-slippage tolerance * There is at least 60 minutes remaining until the swap expires Requesting and accepting a new quote is optional; swap clients may revert to refunding instead of accepting a new quote. For failed Chain Swap states that cannot be renegotiated, like `swap.expired` but coins were sent, the user needs to submit a refund transaction to reclaim the locked bitcoin. For more information about how Boltz API clients can construct and submit refund transactions for users, check the [Claim & Refund Transactions](claiming-swaps.md) section. In the unlikely event that Boltz is unable to lock the agreed amount of chain bitcoin, the final status will be `transaction.failed`. If the user and Boltz both successfully locked up bitcoin on the chain, but the user did not claim the locked chain bitcoin until swap expiry, Boltz will automatically refund its own locked chain bitcoin. The final status of such a swap will be `transaction.refunded`. The state `transaction.lockupFailed` is *not* final and changes to `swap.expired` after the swap expired; the failure reason will be kept and informs e.g. if the user sending too little or too much was the reason for the swap to fail. The states `swap.expired` and `transaction.refunded` are final. Boltz is *not* monitoring user's refund transactions. A 0-conf rejection on the client's lockup transaction is *not* a separate status: it is reported as `transaction.mempool` with `zeroConfRejected: true`. The `Claim` and `Refund` columns mark states in which Boltz will provide a cooperative key-path signature; an `x` additionally requires the swap to be Taproot and, for refunds, Boltz's own lockup refund transaction to be confirmed. Non-cooperative script-path claims and refunds are always possible while the respective lockup is spendable. | Status | Cooperative claim | Cooperative refund | | ------------------------------ | ----------------- | ----------------------------------------- | | `swap.created` | | | | `transaction.mempool` | | | | `transaction.confirmed` | | | | `transaction.server.mempool` | x | | | `transaction.server.confirmed` | x | | | `transaction.claim.pending` | x | | | `transaction.claimed` | | | | `transaction.lockupFailed` | | x (or renegotiate via the quote endpoint) | | `swap.expired` | | x | | `transaction.failed` | | x | | `transaction.refunded` | | x | --- --- url: /swap-limits-and-fees.md description: 'Fee structure, amount calculations and limits for all Boltz swap types.' --- # 💰 Swap Limits & Fees Understanding how fees and limits work is essential for building a reliable Boltz integration. This guide covers the fee structure, amount calculations, and important considerations for all swap types. ## Fee Overview Every swap involves two types of fees: percentage fees and miner fees. Boltz covers all Lightning routing fees for you. ### Percentage Fee A variable fee calculated as a percentage of the swap amount. The rate varies by swap type and pair; check the `/v2/swap/{submarine,reverse,chain}` endpoints for current rates. The percentage fee basis differs by swap type: * **Submarine**: calculated on the **Lightning invoice amount** * **Reverse**: calculated on the **onchain amount** * **Chain**: calculated on the **server lock amount** ### Miner Fee Miner fees cover blockchain transaction costs. These are calculated from transaction sizes and current network fee rates. | Swap Type | Deducted From Your Amount | | ------------- | -------------------------------------------------------------- | | **Submarine** | Claim fee (included in the amount you send) | | **Reverse** | Lockup fee (deducted from onchain amount you receive) | | **Chain** | Server fee (Boltz's lockup + claim, deducted from your amount) | Additionally, you pay transaction fees when broadcasting your own transactions: | Swap Type | You Broadcast & Pay For | | ------------- | ------------------------------------------------ | | **Submarine** | Lockup transaction (sending onchain funds) | | **Reverse** | Claim transaction (receiving onchain funds) | | **Chain** | Lockup (sending chain) + Claim (receiving chain) | ::: info For reverse swaps, the `onchainAmount` in the API response already has the lockup fee subtracted—this is the exact amount Boltz will send to the HTLC. The `claimFee` is an estimate to help you budget for the claim transaction you'll broadcast. ::: ### Extra Fees You can optionally add extra fees on top of the base fees when creating a swap. This is useful if you're building an integration and want to charge your own markup. The extra fee percentage is added to the base percentage fee. To add extra fees, include an `extraFees` object when creating a swap: ```json { "from": "BTC", "to": "BTC", "invoice": "lnbc...", "extraFees": { "id": "my-integration", "percentage": 0.5 } } ``` | Field | Type | Required | Description | | ------------ | ------ | -------- | ---------------------------------------------------- | | `id` | string | Yes | Identifier for the extra fee (e.g., your app's name) | | `percentage` | number | No | Additional fee percentage (0-10). Defaults to 0 | **How it works:** The extra fee percentage is added to Boltz's base percentage fee. For example, if Boltz charges 0.1% and you add 0.5% extra, the total percentage fee becomes 0.6%. ::: warning Maximum extra fee is **10%**. Requests with a higher percentage will be rejected. ::: ## Swap Limits Each pair has limits that define acceptable swap amounts. Check the pair endpoints for current limits. Limits are enforced on the following amounts: | Swap Type | Enforced On | | ------------- | ---------------- | | **Submarine** | Invoice amount | | **Reverse** | Invoice amount | | **Chain** | User lock amount | ### Minimum Amount The minimum ensures the swap covers miner fees with sufficient margin. Minimums may be adjusted dynamically based on current network fee rates to ensure swaps remain economically viable. For submarine swaps, there may be two minimums: * **Standard minimum** (`minimal`): For regular claims * **Batched minimum** (`minimalBatched`): Lower minimum when swaps are aggregated into a single claim transaction If a swap amount is below the standard minimum but above the batched minimum, it will only be processed as part of a batch. ### Maximum Amount The per-swap limit representing the largest single swap Boltz will accept for a pair. ### Zero-Confirmation Limit Transactions at or below this amount may be accepted without blockchain confirmations, provided they meet these criteria: * The transaction doesn't signal RBF (Replace-By-Fee) * The fee rate is adequate * The swap amount is within the zero-confirmation limit ## Calculating Amounts All calculations use integer arithmetic (satoshis). Percentage fees are always rounded up using `ceil()`. ::: info All Bitcoin trading pairs use a rate of 1:1. The formulas below reflect this; no exchange rate conversion is needed. ::: ### Submarine Swaps For submarine swaps, the backend calculates the invoice amount from the onchain amount: ``` invoiceAmount = floor((onchainAmount - minerFee) / (1 + percentageFeeRate)) ``` **Given an invoice amount**, calculate the onchain amount to send: ``` percentageFee = ceil(invoiceAmount × percentageFeeRate) amountToSend = invoiceAmount + minerFee + percentageFee ``` **Example:** * Invoice: 100,000 sats * Miner fee: 4,379 sats * Percentage fee rate: 0.1% (0.001) ``` percentageFee = ceil(100,000 × 0.001) = 100 amountToSend = 100,000 + 4,379 + 100 = 104,479 sats ``` ::: info For submarine swaps, the percentage fee is calculated on the **invoice amount**, not the total send amount. This differs from reverse and chain swaps. ::: ### Reverse Swaps You can specify either an invoice amount or a desired onchain amount. **Given invoice amount:** ``` percentageFee = ceil(invoiceAmount × percentageFeeRate) onchainAmount = floor(invoiceAmount - percentageFee - minerFeeLockup) ``` **Example:** * Invoice: 100,000 sats * Miner fee (lockup): 2,772 sats * Percentage fee rate: 0.5% (0.005) ``` percentageFee = ceil(100,000 × 0.005) = 500 onchainAmount = floor(100,000 - 500 - 2,772) = 96,728 sats ``` **Given onchain amount:** ``` invoiceAmount = ceil((onchainAmount + minerFeeLockup) / (1 - percentageFeeRate)) percentageFee = ceil(invoiceAmount × percentageFeeRate) ``` **Example:** * Desired onchain: 96,728 sats * Miner fee (lockup): 2,772 sats * Percentage fee rate: 0.5% (0.005) ``` invoiceAmount = ceil((96,728 + 2,772) / 0.995) = ceil(100,000) = 100,000 sats percentageFee = ceil(100,000 × 0.005) = 500 ``` ### Chain Swaps You can specify either a user lock amount or a desired server lock amount. **Given user lock amount:** ``` percentageFee = ceil(userLockAmount × percentageFeeRate) serverLockAmount = floor(userLockAmount - percentageFee - serverMinerFee) ``` **Example:** * User locks: 100,000 sats * Server miner fee: 7,035 sats * Percentage fee rate: 0.5% (0.005) ``` percentageFee = ceil(100,000 × 0.005) = 500 serverLockAmount = floor(100,000 - 500 - 7,035) = 92,465 sats ``` **Given server lock amount:** ``` userLockAmount = ceil((serverLockAmount + serverMinerFee) / (1 - percentageFeeRate)) percentageFee = ceil(userLockAmount × percentageFeeRate) ``` **Example:** * Desired server lock: 92,465 sats * Server miner fee: 7,035 sats * Percentage fee rate: 0.5% (0.005) ``` userLockAmount = ceil((92,465 + 7,035) / 0.995) = ceil(100,000) = 100,000 sats percentageFee = ceil(100,000 × 0.005) = 500 ``` ::: info For chain swaps, the server miner fee (deducted from your amount) covers Boltz's transactions. You also broadcast and pay for your own lockup and claim transactions. The API response shows estimated fees for your transactions under `fees.user`. ::: ## Pair Hash Validation Each pair response includes a hash representing the current fees and limits. The hash is a SHA-256 digest of the JSON-serialized pair data (rate, fees, and limits). Including the hash when creating swaps is **optional but recommended**: ```json { "from": "BTC", "to": "BTC", "invoice": "lnbc...", "pairHash": "90ab5c8e6ece5db52173e9423a0dd3071f5894dc8d35ed592a439ccabcdebbd5" } ``` If provided and the hash doesn't match the current configuration, you'll receive an `invalid pair hash` error, indicating the fee data is outdated. ::: warning Always fetch fresh pair data before creating swaps. Fee rates and miner fees can change based on network conditions. Using the pair hash ensures the user sees accurate fee information. ::: ## Important Notes ### Fee Precision All fee calculations use integer arithmetic (satoshis/wei). Key rounding rules: * Percentage fees: always rounded **up** (`ceil`) * Received amounts: always rounded **down** (`floor`) * Send-side invoice/lock amounts (reverse invoices, chain user locks): always rounded **up** (`ceil`) This ensures Boltz always receives sufficient funds to cover fees. ### Lightning Routing Fees **Boltz covers all Lightning routing fees** when paying invoices in submarine swaps. You don't need to account for routing fees in your calculations. ### Zero-Amount Swaps Chain swaps support zero-amount creation where no amounts are specified upfront. Fees are calculated when you lock funds, and Boltz responds with a quote for how much it will lock in return. See [Renegotiating](./renegotiating.md) for more details. ### Dynamic Fee Updates Miner fees are updated periodically based on network conditions. For ERC20 tokens, fees also depend on exchange rates between the token and the native currency. Always fetch fresh pair data before displaying fees to users. --- --- url: /renegotiating.md description: >- Salvaging Chain Swaps that failed due to an incorrect lockup amount instead of refunding them. --- # ♻️ Renegotiating Chain Swaps Renegotiation allows Chain Swaps that failed due to an incorrect lockup amount to be salvaged instead of requiring a refund. It applies only to Chain Swaps, not Submarine or Reverse Swaps. ## What is Renegotiation? Renegotiation enables Chain Swaps that entered `transaction.lockupFailed` because the lockup amount was incorrect to continue. Instead of refunding the locked funds and creating a new swap, the client can request a new quote from Boltz based on the actual amount sent, and if accepted, the swap proceeds as normal. A Chain Swap becomes eligible for renegotiation when the user sends **less** or **more** than the expected amount. In both cases, the swap enters the `transaction.lockupFailed` status because the locked amount doesn't match what was originally quoted. Renegotiation allows Boltz to recalculate the appropriate amount to lock on the destination chain based on the actual amount received, rather than failing the swap entirely. ## Why is Renegotiation Useful? Renegotiation offers several advantages over refunding and creating a new swap: * **Lower fees**: Avoids additional on-chain transaction fees by avoiding refund transactions * **Simpler workflow**: No need to generate new preimages or derive new keys * **Better UX**: Users can recover from mistakes without starting over ## When Can Renegotiation Be Used? A Chain Swap can only be renegotiated if **all** of the following conditions are met: ### Correct Failure Reason The swap must have failed specifically because of an amount mismatch. The failure reason must indicate either: * Underpayment: `locked X is less than expected Y` * Overpayment: `locked X is greater than expected Y` Other failure reasons (such as invalid address, invalid timelock, etc.) do not qualify for renegotiation. ### No Refund Signature Created A refund signature must not have been created for the swap yet. Once a refund transaction is signed, renegotiation is no longer possible. ### Sufficient Time Remaining At least **60 minutes** must remain until the swap expires. This ensures sufficient time for quote validation and acceptance, server lockup transaction broadcasting, and client claim transaction creation. Swaps closer to expiry must be refunded instead. ### Amount Within Pair Limits The actual locked amount must respect the pair's minimum limit. Positive slippage above the maximum limit is accepted only within the configured overpayment tolerance. Boltz also validates liquidity availability before accepting renegotiation. If the new amount exceeds the allowed limit or liquidity is insufficient, renegotiation is not possible. ## API Renegotiation uses two endpoints: * `GET /v2/swap/chain/{id}/quote` - Request a new quote based on the actual locked amount * `POST /v2/swap/chain/{id}/quote` - Accept the quote to continue the swap See the [REST API v2](./api-v2) documentation for details. --- --- url: /claiming-swaps.md description: >- How clients build claim and refund transactions, plus emergency procedures for recovering funds from failed swaps. --- # 🙋‍♂️ Claims & Refunds This document provides an overview of claims and refunds, describes how Boltz API clients create claim and refund transactions and outlines emergency procedures for recovering funds from failed swaps. If local client state was lost, see [Swap Restore](./swap-restore.md) for deriving keys and restoring swaps using an xpub. ## Summary Boltz API clients must broadcast claim transactions for Reverse and Chain Swaps and handle refund transactions for failed Submarine and Chain Swaps. | | Submarine Swaps | Reverse Swaps | Chain Swaps | | ------ | --------------- | ------------- | ----------- | | Claim | | X | X | | Refund | X | | X | ## Refunds Refunds differ depending on the swap type. Refunds for failed Submarine and Chain Swaps require users to have a refund or rescue key and actively broadcast a refund transaction. Failed Reverse Swaps do not need active refunding; funds bounce back to the user via the Lightning Network automatically when the swap expires. | | Submarine Swaps | Reverse Swaps | Chain Swaps | | -------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------ | | Active Refunds | X | | X | | Automatic Refunds | | X | | | Emergency Refunding Method | UTXO: Refund file or invoice preimageEVM: Contract logs | Not needed | UTXO: Refund fileEVM: Contract logs | ## Taproot ::: info If you are not familiar with Taproot yet, or want to refresh your memory, we recommend watching the [Taproot workshop of Bitcoin Optech](https://bitcoinops.org/en/schorr-taproot-workshop/). ::: Boltz Taproot Swaps are using tweaked public keys aggregated with [Musig2](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki). When a swap is created, the client provides its public key in the request and Boltz returns its public key in the response. These two public keys are aggregated with **Boltz's public key always coming first**. The aggregated public key is then tweaked with the tagged hash of the Taptree of the scripts of the swap. The result is the public key used in the [P2TR address](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki) of the swap. ``` OP_1 ``` Key path spends should be preferred over script path spends. One reason is the smaller chain footprint and thus cheaper miner fees and another reason is because key path spends are better for privacy as they don't reveal anything about the atomic swap on the chain. Examples for constructing Taproot Swap transactions can be found in [boltz-core](https://github.com/BoltzExchange/boltz-core/blob/v2.1.0/lib/swap/Claim.ts#L124). Partial signatures from Boltz use `SIGHASH_DEFAULT`. ### Submarine Swaps #### Claim In order for Boltz to claim Submarine Swaps cooperatively, use `GET /swap/submarine/{id}/claim` to obtain the necessary information to create a partial signature. Provide your partial signature to Boltz with `POST /swap/submarine/{id}/claim` and Boltz will broadcast the key path spend claim transaction. In case the client does not cooperate, Boltz will eventually broadcast a script path claim transaction to sweep the UTXO. #### Refund If a Submarine Swap failed (e.g. status `invoice.failedToPay` or `transaction.lockupFailed`), a key path refund can be done. Get a partial signature from Boltz with `POST /swap/submarine/{id}/refund`, aggregate the partials and broadcast the transaction. ::: info The primary advantage of key path refunds is their immediacy: they can be processed as soon as a swap fails, eliminating the need to wait for the timelock to expire. ::: Script path refunds are also possible after the time lock expired and should be implemented as fallback, in case Boltz isn't cooperating. Set the locktime of the transaction to >= the time lock of the swap and make sure to not use the max sequence in the inputs. Structure the input witness like this: ``` ``` ### Reverse Swaps Calling `POST /swap/reverse/{id}/claim` returns the values required to create an aggregated signature and broadcast a key path spend. In case Boltz is not cooperating, a script path spend can be done via a witness structured like this: ``` ``` ### Chain Swaps #### Claim To create a cooperative claim transaction for a Chain Swap, the client has to call `GET /swap/chain/{id}/claim` to fetch the details of the claim transaction the server would like to do. After creating a partial signature for the transaction of the server and creating its own unsigned claim transaction, it calls `POST /swap/chain/{id}/claim`. If Boltz is not cooperating, the script path should be taken. It's the same as for Reverse Swaps. The witness of the input will look like this: ``` ``` #### Refund Refunds of Chain Swaps can be done cooperatively by calling `POST /swap/chain/{id}/refund` with the refund transaction the client would like to do. In case the server refuses to create a partial signature for the refund of the client, a script path spend can be done in the same way as for Submarine Swaps. After the time lock has expired the locked coins can be spent with a witness structured like this: ``` ``` ## EVM On EVM chains, a contract is used for enforcing swaps onchain. The source code of Boltz's contracts can be found [here](https://github.com/BoltzExchange/boltz-core/tree/v2.1.3/contracts). To fetch the current addresses of Boltz's swap contracts and verify the contracts, use [`GET /chain/contracts`](https://api.boltz.exchange/swagger#/Chain/get_chain_contracts). ### Submarine Swaps The `lock` function of the swap contract is used to lock up coins for a Submarine Swap. All values for the parameters required to call the function of the contract are returned in the API response when creating the swap. With the `refund` function of the contract, locked coins can be refunded in case the swap fails. This function takes similar parameters as `lock`, so the values from the response of the swap creation should be stored. To refund before the time lock of the swap has expired, an EIP-712 signature can be requested from Boltz. Use [`GET /swap/submarine/{id}/refund`](https://api.boltz.exchange/swagger#/Submarine/get_swap_submarine__id__refund) to get this signature and use it in the `refundCooperative` function of the contract. Similarly to cooperative Taproot refunds, Boltz will only return such a signature if the swap has failed already. ### Reverse Swaps To claim coins locked in the contract, use the function `claim`. All parameters apart from the `preimage` are returned in the response when creating the swap. The API client is responsible for securely storing the preimage after creating the swap. ## Legacy Swaps Boltz supports two non-Taproot output types: * [P2SH nested P2WSH](https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#user-content-P2WSH_nested_in_BIP16_P2SH) for Normal Submarine Swaps * [P2WSH](https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#user-content-P2WSH) for Reverse Submarine Swaps Claiming works a little different for every output type, but you always need the preimage, private key and original redeem script. Hence, Boltz API clients need to ensure a safe way to store these values until the swap reaches a [final state](lifecycle.md). The witness script of the input always looks like this: ``` ``` #### P2SH nested P2WSH When spending a P2SH nested P2WSH output, the signature, preimage and original redeem script are added to the witness of the input and one adds the Opcode `OP_0` and the SHA256 hash of the redeem script to the signature script of the input. #### P2WSH To spend a P2WSH output, signature, preimage and original redeem script have to be added to the *witness* of the input. #### Examples Examples for all output types can be found in the [`boltz-core`](https://github.com/BoltzExchange/boltz-core/blob/v2.0.1/lib/swap/Claim.ts#L83) reference library. ### Submarine Swaps: Refund transactions Boltz API clients need to be able to craft and broadcast **refund transactions** for failed Legacy **Submarine Swaps**. Refunding an output works just like claiming. Since the refund process doesn't need the preimage (or knows it but can't use it since that would require the claim keys) any value apart from the actual preimage can be used but there has to be a value to prevent the signature from being hashed and compared to the preimage hash. To save on transaction fees, we recommend using a 0 value. There is one more difference when compared to claim transactions: the `nLockTime` of the transaction has to be set to a value equal to or greater than the timeout block height in the redeem script. Otherwise, the Bitcoin network will not accept your transaction. #### Examples An example can be found in the [`boltz-core`](https://github.com/BoltzExchange/boltz-core/blob/v2.0.1/lib/swap/Refund.ts) reference library. The function uses the claim function but requires the timeout block height as argument and sets an empty preimage. ## Emergency Procedures ### UTXO Chains #### Swap Restore For information about refund files, rescue keys, and restoring swaps, please refer to the [Swap Restore documentation](./swap-restore.md). #### Invoice Preimage If a user lost all refund information but still has access to the lightning invoice and can extract the preimage, this can be used to claim the locked bitcoin back to a user-controlled address. Feel free to contact us via [Support Chat](https://boltz.exchange/) or [Email](mailto:hi@bol.tz) should you be in such a situation. We are happy to help! ### EVM Chains If [refund information](claiming-swaps.md#submarine-swaps-1) was lost, all parameters required for a refund can also be queried from the `Lockup` event logs of the contract. The event logs are indexed by `refundAddress`, which is the address with which the client locked the coins. Alternatively, affected users can connect their EVM wallet to [Boltz Web App](https://boltz.exchange/refund/external/) to perform a contract log scan and broadcast refund transactions. --- --- url: /commitment-swaps.md description: >- Locking funds first and committing to a swap afterwards, without knowing the amount when creating it. --- # 🔐 Commitment Swaps Commitment swaps allow locking funds first and committing to a swap after, without knowing the amount when creating the swap. This document explains how commitment swaps work, when to use them, and how to integrate them into your application. ## Overview In standard EVM swaps, you must know the exact amount when creating the swap because the preimage hash is included in the lockup. This creates a problem when the amount isn't known until the transaction executes. Commitment swaps solve this by: 1. Locking funds with `bytes32(0)` as the preimage hash instead of the actual hash 2. Providing an EIP-712 signature that commits to the real preimage hash and the actual locked amount 3. Allowing claims only when both the preimage and the commitment signature are provided This decouples the lockup from the swap creation, enabling flexible amount handling while maintaining the security guarantees of the atomic swap. ## When to Use Commitment Swaps Commitment swaps are beneficial when: * **Sweeping wallets**: Send your entire balance without needing to calculate the exact amount minus fees upfront * **DEX trade outputs**: Lock the output of a DEX swap where slippage means the exact amount isn't known until execution * **Multi-step transactions**: Any scenario where the amount to lock depends on prior transaction results ## Contract Support Commitment swaps require contract version 5 or higher. You can verify contract support by checking the `features` array returned by the contracts endpoint: ```json { "supportedContracts": { "5": { "EtherSwap": "0x...", "ERC20Swap": "0x...", "features": ["BatchClaim", "CommitmentSwap"] } } } ``` ## How It Works ### Standard Swap vs Commitment Swap | Aspect | Standard Swap | Commitment Swap | | --------------------- | ------------------------------ | ---------------------------------------- | | Amount known | Must know exact amount upfront | Can lock any amount, commit afterward | | Lockup `preimageHash` | SHA256 hash of preimage | `bytes32(0)` | | Claim requirement | Preimage only | Preimage + EIP-712 commitment signature | | Contract version | Any | Version 5+ with `CommitmentSwap` feature | ### EIP-712 Commitment Signature The commitment is an EIP-712 typed signature. The type hash differs between EtherSwap (native currency) and ERC20Swap (tokens): **EtherSwap:** ```solidity bytes32 public constant TYPEHASH_COMMIT = keccak256( "Commit(bytes32 preimageHash,uint256 amount,address claimAddress,address refundAddress,uint256 timelock)" ); ``` **ERC20Swap:** ```solidity bytes32 public constant TYPEHASH_COMMIT = keccak256( "Commit(bytes32 preimageHash,uint256 amount,address tokenAddress,address claimAddress,address refundAddress,uint256 timelock)" ); ``` The signature must be created by the `refundAddress` (the address that locked the funds) and commits to: * `preimageHash`: The actual SHA256 hash of the preimage (not `bytes32(0)`) * `amount`: The amount you locked (in wei for ETH, or smallest token units for ERC20) * `tokenAddress`: (ERC20 only) The address of the token contract * `claimAddress`: Boltz's claim address * `refundAddress`: Your address that locked the funds * `timelock`: The block height after which refunds are possible ## API Reference ### Get Lockup Details Before creating a commitment swap, fetch the lockup parameters from Boltz: ``` GET /v2/commitment/{currency}/details ``` **Response:** ```json { "contract": "0x5FbDB2315678afecb367f032d93F642f64180aa3", "claimAddress": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "timelock": 20160 } ``` | Field | Description | | -------------- | ------------------------------------------------ | | `contract` | Address of the swap contract to lock funds in | | `claimAddress` | Boltz's address that will claim the locked funds | | `timelock` | Suggested block height for the commitment lockup | ::: warning The `timelock` returned by this endpoint is a suggested value that should be safely above the highest swap timeout. Always use this value or higher to ensure your commitment has a longer timeout than the swap you create for it. ::: ### Submit Commitment After locking funds and creating a swap, submit the commitment signature: ``` POST /v2/commitment/{currency} ``` **Request Body:** ```json { "swapId": "abc123", "signature": "0x...", "transactionHash": "0x...", "logIndex": 0 } ``` | Field | Required | Description | | -------------------------- | -------- | -------------------------------------------------------- | | `swapId` | Yes | ID of the swap this commitment is for | | `signature` | Yes | EIP-712 commitment signature (hex encoded) | | `transactionHash` | Yes | Transaction hash containing the lockup | | `logIndex` | No | Log index if multiple lockups exist in the transaction | | `maxOverpaymentPercentage` | No | Override for the configured positive-slippage percentage | **Response:** `201 Created` with empty object `{}` ## Integration Guide ### Submarine Swaps (Chain → Lightning) For Submarine Swaps where you send EVM assets to receive Lightning: 1. **Create the swap** via `POST /v2/swap/submarine` as usual 2. **Get commitment lockup details**: ``` GET /v2/commitment/{currency}/details ``` 3. **Lock funds** by calling the contract's `lock` function: **For EtherSwap (native currency like RBTC):** * `preimageHash`: `bytes32(0)` (32 zero bytes) * `claimAddress`: The `claimAddress` from step 2 * `timelock`: The `timelock` from step 2 (or higher) * `value`: Any amount you want to swap (sent as transaction value) **For ERC20Swap (tokens):** * `preimageHash`: `bytes32(0)` (32 zero bytes) * `amount`: Any amount you want to swap (in smallest token units) * `tokenAddress`: Address of the token contract * `claimAddress`: The `claimAddress` from step 2 * `timelock`: The `timelock` from step 2 (or higher) Note: For ERC20 tokens, you must first approve the swap contract to spend your tokens. 4. **Create the commitment signature** using EIP-712 with the actual locked amount. The committed amount must not be below the swap amount and may only exceed it within the configured positive-slippage tolerance. 5. **Submit the commitment**: ``` POST /v2/commitment/{currency} { "swapId": "...", "signature": "0x...", "transactionHash": "0x...", "logIndex": 0 } ``` 6. The swap proceeds as normal once the commitment is accepted ### Chain Swaps (Chain → Chain) For Chain Swaps where you send EVM assets to receive assets on another chain: The flow is identical to Submarine Swaps above. Create the Chain Swap first, then follow steps 2-6 to lock with a commitment instead of the actual preimage hash. ## Refunding Commitment Swaps ### Commitment Not Linked to a Swap If the lockup is not linked to any swap yet, request a cooperative refund signature using the lockup transaction: ``` POST /v2/commitment/{currency}/refund ``` **Request body:** ```json { "transactionHash": "0x...", "logIndex": 0, "refundAddressSignature": "0x..." } ``` * `transactionHash`: transaction containing the lockup event * `logIndex`: optional, required when multiple lockups are in the same transaction * `refundAddressSignature`: signature from your `refundAddress` over: ``` Boltz commitment refund authorization chain: {currency} transactionHash: {transactionHash} logIndex: {logIndex or "none"} ``` **Response:** ```json { "signature": "0x..." } ``` Use that signature with `refundCooperative` and the exact lockup values from the transaction. ### Commitment Linked to a Swap Once the commitment is linked to a swap, use the swap refund endpoint: ``` GET /v2/swap/submarine/{id}/refund ``` Then execute `refundCooperative` with the returned signature. ### Timelock Refund After timelock expiry, refunds work the same as standard EVM swaps by calling `refund` with the lockup parameters --- --- url: /swap-restore.md description: >- Recovery flow for clients that lost local swap state, using the Boltz Rescue Key mnemonic. --- # 🛟 Swap Restore Swap Restore is a recovery flow for situations where a swap client lost all local swap state and backups. Swap Restore enables refunding failed swaps as well as resuming ongoing swaps using a simple mnemonic known as the **Boltz Rescue Key**. Swap Restore targets UTXO chains. On EVM chains, swap parameters are persisted onchain in contract event logs. Recovery on EVM is performed by reading those logs instead of using Swap Restore. For more details, see the [EVM contract logs section](./claiming-swaps.md#evm-chains). ## What Swap Restore Does At a high level, Swap Restore means the swap client derives the xpub used for swaps from the mnemonic and submits it to `/v2/swap/restore`, receives all associated swaps, and reconstructs the local data. With that state restored, the app can perform claims for ongoing swaps or refunds for failed ones as needed. For claims to work, the client also needs to be able to deterministically derive preimages. One option to do that is to [hash the private key of a swap and use that hash as the preimage](#preimages). ## Trust Model The private keys never leave the device and remain client-side. The backend only receives the xpub, which cannot be used to derive private keys or spend funds. All transaction signing happens locally. Sharing an xpub reveals your swap history to Boltz's backend, but it cannot spend your funds. ## API Call [`POST /v2/swap/restore`](https://api.boltz.exchange/swagger#/Swap/post_swap_restore) with the xpub in the request body: ```json { "xpub": "xpub6C..." } ``` The API will respond with an array of swaps belonging to that xpub. A simplified example shape is shown below: ```json [ { "id": "abc123", "type": "submarine|reverse|chain", "status": "transaction.lockupFailed", "from": "BTC", "to": "LBTC", ... } ] ``` ## Key Derivation Starting from a mnemonic, derive a seed and then a BIP32 root. From there, derive the account at `m/44/0/0/0` (the default path) and export its xpub for server-side swap matching. Individual swap keys are derived at `m/44/0/0/0/{index}`. This default path must be used for compatibility with [Boltz Web App](https://boltz.exchange/rescue/external?mode=rescue-key), though a different derivation path can be specified when using the [Boltz API](https://api.boltz.exchange/swagger#/Swap/post_swap_restore) directly. ### BIP85: Avoiding Derivation Path Collisions For wallets that already use standard derivation paths like `m/44/0/0/0` for regular transactions, using the same path for swap keys creates a risk of derivation path collisions. If the wallet later generates keys at the same indices for regular spending, it could inadvertently reuse keys that were previously used in swaps, compromising both privacy and security. [BIP85](https://github.com/bitcoin/bips/blob/master/bip-0085.mediawiki) provides a solution by deriving application-specific entropy from a master seed. This entropy can then be used to create a child seed exclusively for Boltz swaps, isolating swap keys from the wallet's main key derivation tree without requiring users to back up any additional information. #### Implementation 1. Derive entropy from the master seed via BIP85 using the application number for Boltz (e.g., `26589` for "BOLTZ" in T9) 2. Use the derived entropy to create a seed 3. From this derived child seed, derive the swap account at `m/44/0/0/0` and export its xpub 4. All swap keys and preimages are then derived from this isolated child seed This approach ensures that: * Swap keys never collide with the main wallet's key derivation * The entire swap key hierarchy is deterministically recoverable from the master mnemonic * Privacy is preserved by separating swap activity from regular wallet activity * The same master mnemonic can safely be used across both regular wallet operations and Boltz swaps ### Preimages We recommend deriving preimages deterministically with `sha256(privateKey(index))`. This allows swaps to be fully restored from the mnemonic alone. Using deterministic preimages in this exact way is necessary for restored swaps to be claimable in our Web App. --- --- url: /asset-rescue.md description: >- Cooperative recovery flow for Liquid assets accidentally sent to Boltz swap lockup addresses. --- # ⛑️ Asset Rescue Asset Rescue enables cooperative recovery of Liquid assets accidentally sent to swap lockup addresses. ## What is Asset Rescue? Boltz only accepts L-BTC for Liquid swaps. However, users sometimes accidentally send the wrong asset (like USDt or other Liquid tokens) to a swap lockup address. These tokens become stuck because refunding them requires L-BTC to pay transaction fees, which the lockup output doesn't contain. Asset Rescue solves this by having Boltz cooperatively sign a rescue transaction with the client using MuSig2. Boltz provides the L-BTC needed for fees, and the accidentally locked asset is sent to the client's destination address. ## Why is Asset Rescue Useful? Asset Rescue enables recovery of accidentally locked tokens: * **Recovers stuck assets**: Without cooperative signing, tokens accidentally sent to lockup addresses would be unspendable since spending them requires L-BTC for fees * **No additional funds needed**: Boltz provides the L-BTC for transaction fees * **Simple flow**: Two API calls handle the entire recovery process ## When Can Asset Rescue Be Used? A swap can only use Asset Rescue if **all** of the following conditions are met: ### Correct Swap Status The swap must be in one of these states: * `transaction.lockupFailed` - The lockup transaction was invalid (e.g., wrong asset was sent) * `swap.expired` - The swap timed out before completion ### Taproot Swap The swap must be a Taproot swap. Legacy swaps do not support Asset Rescue because they lack MuSig2 key-spend paths. ### Non-L-BTC Liquid Asset The accidentally locked asset must **not** be L-BTC. L-BTC lockups can use the standard refund flow since L-BTC can pay for its own fees. Asset Rescue is specifically for other Liquid tokens that were sent by mistake. ### Submarine or Chain Swap from Liquid Asset Rescue works for swaps where the user locks funds on Liquid: * Submarine swaps (user locks on Liquid, receives on Lightning) * Chain swaps where the sending side is Liquid ## How It Works Asset Rescue uses MuSig2 cooperative signing to spend the locked output via the key-spend path: 1. **Setup**: Client calls the setup endpoint with the swap ID, lockup transaction details, and destination address. Boltz creates a rescue transaction, selects an L-BTC UTXO to pay fees, and returns the unsigned transaction along with MuSig2 signing data. 2. **Sign**: Client verifies the transaction sends the asset to their destination, generates their MuSig2 nonce, and creates a partial signature. 3. **Broadcast**: Client sends their nonce and partial signature to the broadcast endpoint. Boltz aggregates the signatures, signs the L-BTC fee input, and broadcasts the transaction. ## API Asset Rescue uses two endpoints: * `POST /v2/asset/{currency}/rescue/setup` - Create a rescue transaction and get MuSig2 signing data * `POST /v2/asset/{currency}/rescue/broadcast` - Submit partial signature and broadcast the transaction See the [REST API v2](./api-v2) documentation for details. --- --- url: /common-mistakes.md description: >- Frequently encountered misunderstandings and pitfalls when integrating Boltz API. --- # ⚠️ Common Mistakes This document collects frequently encountered misunderstandings and common mistakes when integrating Boltz API. ## Permanently Store Swap Information It is essential to safely store swap information like claim and refund keys along with all other swap data in order to successfully pick up pending swaps or refund in case a submarine or chain swap fails. Permanently storing historical swap data is useful to e.g. provide the user with a proof of payment with a Lightning invoice and its corresponding preimage of a submarine swap. ## Client-Side All Boltz API integrations should be client-side, meaning in end-user-controlled software on end-user-controlled devices. Boltz's non-custodial nature can only be maintained if the end user is in full control of Boltz Swaps. This concretely means that integrations should never run Boltz API clients on behalf of end users or store refund/swap information on their servers. The best way to get the broader Bitcoin community to accept and use a Boltz Swap Client is for the code to be open source and reviewed by the Boltz Team. ## Online Requirement It is important to design Boltz API clients, especially on mobile, in a way that they can come online and broadcast claim transactions before a swap expires. We added [WebHook calls](https://github.com/BoltzExchange/boltz-backend/issues/605) informing about swap updates that developers can use to wake up clients with push notifications. ## Retry Mechanism API clients should account for temporary network failures and, e.g., retry claiming swaps or sending payments. On mobile, claim logic should be triggered on app start and work independently of which part of the app the user navigates to. This also goes for WebView integrations of our [Web App](https://github.com/BoltzExchange/boltz-web-app). ## Lockup Transaction Handling API clients must ensure to fund Boltz swaps via a single lockup transaction. If multiple transactions fund the same lockup address, only the first is used for the swap; others are ignored. The swap will fail if this first transaction's amount is insufficient. Following a swap failure, Boltz facilitates cooperative refunds for all outputs sent to the address. --- --- url: /dont-trust-verify.md description: >- How API clients should verify information returned by Boltz to minimize trust assumptions. --- # 🚫 Don't trust. Verify! This document gives an overview of how Boltz API clients should verify information generated by Boltz and other general measurements that should be undertaken to minimize trust. ## Basics As a general concept, Boltz API clients should not trust any critical information obtained via the Boltz API. This applies *especially* to chain addresses and lightning invoices where users will send bitcoin to. ## UTXO Chain Address Verification ### Taproot The [scripting section](claiming-swaps.md) explains how to verify P2TR addresses in Taproot Swaps. ### Legacy Boltz currently supports two types of addresses for legacy swaps: * [P2SH nested P2WSH](https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#user-content-P2WSH_nested_in_BIP16_P2SH) for Normal Submarine Swaps * [P2WSH](https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#user-content-P2WSH) for Reverse Submarine Swaps Boltz API returns a redeem script and a derived chain address when creating swaps. Boltz API clients should verify that the redeem script is valid by checking preimage hash, public key, timeout block height of the HTLC and OP codes. An example of an implementation in TypeScript can be found [here](https://github.com/BoltzExchange/boltz-web-app/blob/421d8268d9c63ba77e200dfc52c1a76b921e788b/src/utils/validation.ts#L93). Boltz API clients should also verify the correctness of the given address: #### P2SH nested P2WSH Although the output script of the P2SH nested P2WSH addresses is the same as the one of plain P2SH addresses, other data is hashed. Here, not the plain redeem script is hashed, but a P2WSH output script is constructed and first hashed with SHA256 and then hashed again with RIPEMD-160. The output script will look like this: ``` OP_HASH160 OP_EQUAL ``` #### P2WSH In P2WSH addresses, the redeem script is hashed with SHA256. The output script is also a little simpler: ``` OP_0 ``` #### Examples Examples for generating all of these addresses with `Node.js` can be found in the [`boltz-core`](https://github.com/BoltzExchange/boltz-core/blob/v2.0.1/lib/swap/Scripts.ts) reference library. ::: info Address verification applies to all swap types involving a UTXO chain. A list of Bitcoin mainchain Opcodes with description can be found on the [Bitcoin Wiki](https://en.bitcoin.it/wiki/Script). ::: ## Lightning Invoice Verification Swap amounts should be calculated by the API client (taking into account network fee and Boltz fee from `/getpairs`). Boltz API clients then should decode and verify that a Lightning invoice has been generated by Boltz * Has the same preimage hash that the client previously generated and used to call `/createswap`. * Has the same amount as the amount used to call `/createswap`. Examples in JavaScript can be found [here](https://github.com/BoltzExchange/boltz-web-app/blob/421d8268d9c63ba77e200dfc52c1a76b921e788b/src/utils/validation.ts#L109). Boltz API clients should also verify that amounts in user-provided invoices match calculated swap amounts to prevent user error. ## EVM Chain Contract Verification Boltz API clients should check that EVM swaps are carried out using a legitimate contract by comparing the bytecode of it to a verified version or hardcoding the address of a verified version. ## Taproot Swaps Script Path Fallback Key path spends in the Taproot Swap protocol rely on Boltz cooperating. Should Boltz disappear or refuse to cooperate, for Chain and Normal Submarine Swaps, Boltz API clients must implement a fallback to the script path to be able to enforce claim and refund transactions. ## Blockchain Interaction Boltz provides [endpoints](https://api.boltz.exchange/swagger#/Chain/) for querying blockchain data and broadcasting transactions. For trust minimization, we advise to use self-hosted infrastructure or external services (e.g., [blockstream.info](https://blockstream.info/) or [mempool.space](https://mempool.space/)) to verify blockchain information obtained from Boltz. ## General Measurements Boltz API clients should calculate swap amounts locally to verify that `expectedAmount` in the API responses matches the locally calculated amount. --- --- url: /magic-routing-hints.md description: >- Advanced feature for cheaper, faster Lightning settlement and the additional verification responsibilities it places on clients. --- # 🪄 Magic Routing Hints ::: warning **Advanced Feature**: MRH is an advanced feature that does not have the exact same guarantees as a standard Boltz swap. While it provides cost savings and slightly faster settlement, swap clients should be aware of the additional responsibilities to recognize and verify MRH payments. ::: This page describes the mechanism that encodes a BIP21 URI into the routing hints section of an invoice, mainly for Boltz-powered wallets like [Aqua](https://aqua.net/) to pay each other directly without involving a Boltz swap and to avoid bloated, hard-to-scan BIP21 QR codes. ## Basics While paying invoices of Reverse Swaps with Submarine Swaps is supported by Boltz, it is not economical. To minimize the number of transactions and costs, swap routes can be optimized or swaps can be skipped entirely and the sender can pay the receiver directly using Magic Routing Hints, or "MRH" for short. For instance, instead of: * Chaining a Bitcoin -> Lightning Submarine Swap with a Lightning -> Liquid Reverse Swap, swap clients can instead use a Bitcoin -> Liquid Chain Swap * Chaining a Liquid -> Lightning swap with a Lightning -> Liquid swap, swap clients can directly send a Liquid transaction to the receiver The only data that the receiver and sender share in these scenarios is the Lightning invoice. Encoding arbitrary data in an invoice is impractical, so we add a hint to this invoice which indicates that one can fetch a chain address of the recipient. This hint is encoded in the routing hints section of the swap's Lightning invoice with a specific channel id. There is no actual channel with this id, the channel as routing hint only signals to supporting wallets that the magic routing hint feature is enabled by the recipient. The node public key in the routing hint is the same public key with which the receiver would need to sign to enforce the claim of the Reverse Swap. ## Steps For The Receiver When creating a Reverse Swap with a magic routing hint, there are a few additional steps that the receiver must take: * In the API call to create the swap, pass a chain address of the wallet for the chain on which the Reverse Swap will be claimed to. * In the API call to create the swap, pass a signature of the SHA-256 hash of this chain address, signed by the private key the Reverse Swap can be claimed with. * When the API call returns, verify the correctness of the invoice, including the magic routing hint with the specific channel id and the claim public key in the node public key field. ### Wallet Requirements * Use a **separate wallet** dedicated for swaps to avoid accidental address reuse, which can cause MRH payments to be falsely marked as settled * Use a custom derivation path if you plan to let your users export the wallet for use elsewhere It is the client's responsibility to handle payments to magic routing hint addresses. To help clients detect these transactions as quickly as possible, we emit [a WebSocket event](api-v2#magic-routing-hints) when we observe a transaction in the mempool to the MRH address for a swap the client is subscribed to. Boltz emits a `transaction.direct` status update for MRH transactions related to a specific swap. While these status updates are provided for convenience, swap clients should **not** use these as the sole source of truth and always independently verify transactions onchain before marking a swap as settled. ## Steps For The Sender When a wallet attempts to pay a Lightning invoice via a Submarine Swap, there are two options: either proceed with the standard Submarine Swap if no magic routing hint is present or use the magic routing hint if it exists. If a magic routing hint is found, then: * Parse the public key in the routing hint. * Send a [request to Boltz API](https://api.boltz.exchange/swagger#/Reverse/get_swap_reverse__invoice__bip21) to fetch the chain address of the receiver. * Extract the address from the BIP21, hash the address, and verify the signature returned in the API call against the public key in the magic routing hint. * Verify the amount of the BIP21 before paying to it. On Liquid, the asset id must be verified, too. ## Amounts Magic Routing Hints are designed to ensure the receiver gets the exact same amount as with chained swaps, while allowing the sender to reduce their costs. The amount in the BIP21 should be honored by the sender. ## Example code ```typescript import axios from 'axios'; import { crypto } from 'bitcoinjs-lib'; import bolt11 from 'bolt11'; import { randomBytes } from 'crypto'; import { ECPairFactory } from 'ecpair'; import * as ecc from 'tiny-secp256k1'; const ECPair = ECPairFactory(ecc); const endpoint = ''; const magicRoutingHintConstant = '0846c900051c0000'; const lbtcAssetHash = ''; const findMagicRoutingHint = (invoice: string) => { const decodedInvoice = bolt11.decode(invoice); const routingInfo = decodedInvoice.tags.find( (tag) => tag.tagName === 'routing_info', ); if (routingInfo === undefined) { return { decodedInvoice }; } const magicRoutingHint = (routingInfo.data as any[]).find( (hint) => hint.short_channel_id === magicRoutingHintConstant, ); if (magicRoutingHint === undefined) { return { decodedInvoice }; } return { magicRoutingHint, decodedInvoice }; }; const receiverSide = async () => { const preimage = randomBytes(32); const claimKeys = ECPair.makeRandom(); const address = ''; const addressHash = crypto.sha256(Buffer.from(address, 'utf-8')); const addressSignature = claimKeys.signSchnorr(addressHash); const swapRes = ( await axios.post(`${endpoint}/v2/swap/reverse`, { address, from: 'BTC', to: 'L-BTC', invoiceAmount: 10_000, addressSignature: addressSignature.toString('hex'), claimPublicKey: claimKeys.publicKey.toString('hex'), preimageHash: crypto.sha256(preimage).toString('hex'), }) ).data; // Other verification checks skipped const { magicRoutingHint } = findMagicRoutingHint(swapRes.invoice); if (magicRoutingHint === undefined) { throw 'no magic routing hint'; } if (magicRoutingHint.pubkey !== claimKeys.publicKey.toString('hex')) { throw 'invalid public key in magic routing hint'; } return swapRes.invoice; }; const senderSide = async (invoice: string) => { const { magicRoutingHint, decodedInvoice } = findMagicRoutingHint(invoice); if (magicRoutingHint === undefined) { // Pay via Swap console.log('no magic routing hint found'); return; } const bip21Res = ( await axios.get(`${endpoint}/v2/swap/reverse/${invoice}/bip21`) ).data; const receiverPublicKey = ECPair.fromPublicKey( Buffer.from(magicRoutingHint.pubkey, 'hex'), ); const receiverSignature = Buffer.from(bip21Res.signature, 'hex'); const bip21Decoded = new URL(bip21Res.bip21); const bip21Address = bip21Decoded.pathname; const addressHash = crypto.sha256(Buffer.from(bip21Address, 'utf-8')); if (!receiverPublicKey.verifySchnorr(addressHash, receiverSignature)) { throw 'invalid address signature'; } if (bip21Decoded.searchParams.get('assetid') !== lbtcAssetHash) { throw 'invalid BIP21 asset'; } // Amount in the BIP21 is the amount the recipient will actually receive // The invoice amount includes service and swap onchain fees if ( Number(bip21Decoded.searchParams.get('amount')) * 10 ** 8 > Number(decodedInvoice.satoshis) ) { throw 'invalid BIP21 amount'; } // Pay on Liquid }; (async () => { try { const invoice = await receiverSide(); await senderSide(invoice); } catch (e) { console.error(e); } })(); ``` --- --- url: /0-conf.md description: >- How Boltz handles 0-conf transactions to speed up swaps and the risks each party takes. --- # ⏩ 0-conf The use of 0-conf can make swaps a lot faster by utilizing transactions that are not included in a block yet. ## Risks Accepting 0-conf transactions doesn't come without unwarranted risk. As a precautionary measure, Boltz enforces a few rules when it comes to 0-conf. It is important to note that: * In **Normal Submarine Swaps** in which the user sends the chain transaction, *Boltz* is taking the risk by accepting unconfirmed transactions. * In **Reverse Submarine Swaps** where the user receives the chain transaction from Boltz, *the user* is at risk for accepting the unconfirmed transaction. *0-conf Swaps are subject to network conditions and only available on UTXO chains like Bitcoin or Liquid.* ::: info Because of [growing adoption of `mempoolfullrbf`](https://github.com/bitcoin/bitcoin/pull/28132) by a significant set of miners, Boltz is **not** accepting 0-conf transactions on the Bitcoin mainchain. ::: ## Limits When it comes to accepting 0-conf transactions, Boltz has configurable limits in place. These limits can be found via the [`getpairs`](api-v1.md#supported-pairs) endpoint and are enforced only for Normal Submarine Swaps. When the user receives a chain transaction from Boltz, 0-conf acceptance is entirely up to the API client. ## BIP 125 - Replace-By-Fee If a transaction locking up bitcoin is signalling Replace-By-Fee either explicitly or inherently (unconfirmed inputs of the transaction signal RBF), Boltz will not accept 0-conf for that transaction in Normal Submarine Swaps. Also note, that Boltz never sends transactions that signal RBF. For more information about RBF please read [BIP 125 - Opt-in Full Replace-by-Fee Signaling](https://github.com/bitcoin/bips/blob/master/bip-0125.mediawiki). ## Miner fees Swaps on Boltz are based on HTLCs (*Hash Time Locked Contracts*). In order to account for the *time locked* component of these contracts, transactions locking and claiming coins from such contracts have to pay a *reasonably high miner fee* in order to be included in a block timely. Boltz considers fees that are equal or higher than 80% of the `sat/vbyte` estimations of its [`getfeeestimation`](api-v1.md#fee-estimations) endpoint as *reasonably high*. If the miner fee paid by the transaction is less than that, Boltz will not accept 0-conf and wait for the transaction to be included in a block. --- --- url: /webhooks.md description: Subscribing to webhooks to receive swap status updates without polling. --- # 🪝 Webhooks Get notified about swap status updates. ## Subscribing to Webhooks To get the latest status of your swaps, you can poll the status endpoints of the REST API, subscribe to the WebSocket, or set a webhook when creating the swap. To register a webhook, add the following object to the request that creates the swap: ```json { ... "webhook": { "url": "", "hashSwapId": false, "status": ["invoice.pending", "transaction.claim.pending"] } } ``` `url` will be called on every swap status update with a JSON object structured like this: ```json { "event": "swap.update", "data": { "id": "", "status": "" } } ``` Only HTTPS URLs are allowed for webhooks. `hashSwapId` is optional and defaults to false. When it is explicitly set to `true`, the swap id is hashed with SHA256 and encoded as HEX in the webhook call. That is useful when the webhook is processed by a third party to which you do not want to leak information about your swaps. `status` is optional and is a list of swap status update events for which the webhook should be called. If not set, the webhook will be called for all events. --- --- url: /bolt12.md description: 'Swapping with the BOLT12 standard, including tips for handling BOLT12 offers.' --- # ✨ BOLT12 Information about swaps with the new BOLT12 standard and some accompanying tips & tricks for how to handle BOLT12 offers. ## Submarine Swaps We allow swaps *to* BOLT12 invoices in [Submarine Swaps](lifecycle.md#normal-submarine-swaps). The flow is the same as with BOLT11 invoices. ### Offers To make it as easy as possible for clients to fetch BOLT12 invoices for offers, we provide [this API endpoint](https://api.boltz.exchange/swagger#/Lightning/post_lightning__currency__bolt12_fetch). To verify the returned invoice belongs to the provided offer, the signing key of the invoice has to be checked. That signing key has to be either: * the signing key of the offer, if that is defined, or * the public key of the last hop of one of the message paths Reference implementations for checking the signing key: * [Rust in boltz-client](https://github.com/BoltzExchange/boltz-client/blob/63a2bdd8a7729d45fe0c9b7a016a847bc5a83976/lightning/lib/bolt12/src/lib.rs#L113) * [TypeScript in boltz-web-app](https://github.com/BoltzExchange/boltz-web-app/blob/f94e7cdb31946ccbc4bd5d2f4f29086ca63c7335/src/utils/invoice.ts#L261) ### BIP-353 A common way to share BOLT12 offers is via [BIP-353](https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki). After [resolving the BIP-353](https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki#resolution), read the URL query parameters. BOLT12 offers are in the `lno` parameter, so if it's set, a swap can be created by fetching an invoice and creating a Submarine Swap with it. ## Reverse Swaps To swap from a BOLT12 offer to onchain, the client needs to: 1. Create a BOLT12 offer 2. Register it with the Boltz API 3. Respond to invoice requests for the offer either via webhook or WebSocket 4. Follow the usual Reverse Swap protocol Offers for Reverse Swaps can be registered with [this API endpoint](https://api.boltz.exchange/swagger#/Lightning/post_lightning__currency__bolt12). These offers have to include the CLN node of the API endpoint in a blinded message path as entry point. Information about the lightning nodes of the API can be queried from the [`/nodes` endpoint](https://api.boltz.exchange/swagger#/Nodes/get_nodes). When registering the offer, a webhook URL can be specified. That webhook will be called when an invoice request for the offer is received: ```json { "offer": "", "invoiceRequest": "" } ``` As a response to that webhook call, an invoice in response to the request is expected: ```json { "invoice": "lni..." } ``` In case webhook calls are not desired, those invoice requests can also be delivered [via WebSocket](api-v2#bolt12-invoice-requests). There is also a [`PATCH` endpoint to update the webhook URL](https://api.boltz.exchange/swagger#/Lightning/patch_lightning__currency__bolt12) for an offer that is registered already. When creating invoices in response to the requests, [this endpoint](https://api.boltz.exchange/swagger#/Lightning/get_lightning__currency__bolt12__receiving_) should be called to figure out parameters the API expects to be set in the invoice. And the invoice should have the CLN node of the API as the entry point of the blinded payment path. *Before* responding with the invoice, a Reverse Swap should be created with it. The preimage hash is omitted from the call to create the swap and the `invoice` property set instead. The rest of the Reverse Swap flow stays the exact same. Magic Routing hints in those BOLT12 invoices are ***not*** signaled by including a fake routing hint with the channel id constant. They are shown to the payer when they fetch the invoice for the offer [in the API](https://api.boltz.exchange/swagger#/Lightning/post_lightning__currency__bolt12_fetch). --- --- url: /pro.md description: >- Boltz Pro dynamically adjusts swap fees based on Boltz's liquidity needs to keep wallets and Lightning channels balanced. --- # 🏅 Pro Boltz Pro is a service designed to dynamically adjust swap fees based on Boltz's liquidity needs, helping to maintain wallet and Lightning channel balances. ## Basics Boltz Pro temporarily offers incentives, like 0% or even negative fees, to encourage users to conduct swaps that work in favor of our liquidity needs. It has much stricter limits, e.g. the available fee budget for routing on Lightning, or the time limit to fund swaps and is **not designed for regular payments**. Lightning payments may be queued and dispatched with a delay of several minutes, or not sent at all if liquidity conditions change and the swap is cancelled. ## Web Usage A web app for Boltz Pro is available at [pro.boltz.exchange](https://pro.boltz.exchange/). ## API Usage Boltz Pro can be accessed via API using the [same endpoints and methods](api-v2.md) as the regular Boltz API. To switch to Boltz Pro, API consumers need to: * add a `Referral` header to all pair related `GET` requests and set it to `pro` * set the `referralId` to `pro` when creating swaps ## API Client We recommend using [Boltz Client](https://github.com/BoltzExchange/boltz-client) as API consumer for Boltz Pro, as it safely handles key generation and storage, refunds and many common edge cases. Boltz Client is our battle-tested swap client, powering e.g. the popular [Boltz BTCPay Plugin](https://github.com/BoltzExchange/boltz-btcpay-plugin). Check [this section](https://client.docs.boltz.exchange/boltz-pro) for details on how to set up Boltz Client for usage with Boltz Pro. ::: warning We strongly advise *against* using Boltz Web App for automation of Boltz Pro, as updates might break integrations. ::: --- --- url: /claim-covenants.md description: Using Liquid covenants to reduce interactivity requirements of Reverse Swaps. --- # 📜 Claim Covenants To reduce the interactivity requirements of Reverse Swaps, one can make use of covenants. Covenants are available, e.g., on the [Liquid Network](https://liquid.net/) in the form of [introspection opcodes](https://github.com/ElementsProject/elements/blob/master/doc/tapscript_opcodes.md#new-opcodes-for-additional-functionality). These opcodes allow the script in the witness to inspect the inputs, outputs, and other properties of the transaction in which it is executed. ## Disclaimer Claim covenants have different trust assumptions depending on how they are used. Below are two common use cases with their respective trust implications. ### Invoice-based Creation & Validation of Swaps When using claim covenants with an **on-demand invoice creation and validation workflow**, they can be used in a trust-minimized manner: The client initiates the swap and immediately receives and validates the covenant script setup. Since the client can verify that the covenant is properly configured before sharing payment details, this approach maintains minimal trust assumptions. **This is the recommended way to use claim covenants.** ### LNURL/LN-Address Spontaneous Offline Payments When using claim covenants for **spontaneous payments via LNURL or Lightning addresses** while the swap client is offline, they **cannot** be used in a trust-minimized manner: In this scenario, the sender pulls payment details on-demand (e.g., via LNURL or a Lightning address), but the receiving client is offline and cannot validate the covenant script setup. The swap creator retains sole control over the actual claiming conditions, and the offline recipient has no way to verify correctness. From a trust perspective, this is similar to providing an xpub or wallet descriptor as the swap destination directly to the swap creator. **We do NOT recommend using claim covenants this way.** ### General Considerations When handing over the preimage of a Reverse Swap to the swap creator, which is usually a third party like a mobile wallet provider, you have to rely on this party not to collude with the Lightning node that accepts HTLCs for the hold invoice. If that happens, the covenant script path spend would not be executed, while the Lightning HTLCs are resolved and eventually, the coins locked on Liquid will be refunded. A workaround could be using multiple servers that enforce covenant claims for the swap client. Additionally, note that Liquid swap transactions need to be unblinded for covenants and therefore cannot leverage the privacy benefits of [Confidential Transactions](https://glossary.blockstream.com/confidential-transactions/). ## Boltz API Boltz API clients can ask for a covenant to be included in the Taptree of a Reverse Swap. With that new leaf in the tree, the coins locked for the Reverse Swap can be claimed by revealing the preimage and sending the expected asset with the expected amount to an address of the client in the 0th output of the transaction. This is what the script looks like: ``` OP_SIZE 32 OP_EQUALVERIFY OP_HASH160 OP_EQUALVERIFY 0 OP_INSPECTOUTPUTSCRIPTPUBKEY OP_EQUALVERIFY OP_EQUALVERIFY 0 OP_INSPECTOUTPUTASSET OP_1 OP_EQUALVERIFY OP_EQUALVERIFY 0 OP_INSPECTOUTPUTVALUE OP_DROP OP_EQUAL ``` ## Example Code This example registers a covenant to be claimed with the reference implementation [covclaim](https://github.com/BoltzExchange/covclaim/) running locally at port 1234: ```typescript import axios from 'axios'; import { crypto } from 'bitcoinjs-lib'; import { SwapTreeSerializer, Types } from 'boltz-core'; import { randomBytes } from 'crypto'; import { ECPairFactory } from 'ecpair'; import * as ecc from 'tiny-secp256k1'; const ECPair = ECPairFactory(ecc); type CovenantParams = { claimPublicKey: Buffer; refundPublicKey: Buffer; preimage: Buffer; blindingKey: Buffer; address: string; tree: Types.SwapTree; }; const endpoint = ''; const covenantEndpoint = 'http://127.0.0.1:1234'; const createSwap = async (): Promise => { const preimage = randomBytes(32); const claimKeys = ECPair.makeRandom(); const address = ''; const swapRes = ( await axios.post(`${endpoint}/v2/swap/reverse`, { address, from: 'BTC', to: 'L-BTC', claimCovenant: true, invoiceAmount: 10_000, preimageHash: crypto.sha256(preimage).toString('hex'), claimPublicKey: claimKeys.publicKey.toString('hex'), }) ).data; // Verification checks skipped return { address, preimage, claimPublicKey: claimKeys.publicKey, blindingKey: Buffer.from(swapRes.blindingKey, 'hex'), refundPublicKey: Buffer.from(swapRes.refundPublicKey, 'hex'), tree: SwapTreeSerializer.deserializeSwapTree(swapRes.swapTree), }; }; const registerCovenant = async (params: CovenantParams) => await axios.post(`${covenantEndpoint}/covenant`, { address: params.address, preimage: params.preimage.toString('hex'), tree: SwapTreeSerializer.serializeSwapTree(params.tree), blindingKey: params.blindingKey.toString('hex'), claimPublicKey: params.claimPublicKey.toString('hex'), refundPublicKey: params.refundPublicKey.toString('hex'), }); (async () => { try { const swap = await createSwap(); await registerCovenant(swap); } catch (e) { console.error(e); } })(); ``` --- --- url: /partner-program.md description: 'Rewards program for impactful, high-quality integrations of the Boltz API.' --- # 🤝 Partner Program The Boltz Partner Program provides eligible partners with rewards for impactful, high quality integrations of Boltz API ## Who Is Eligible * Integrations that make Boltz Swaps natively available through their platform or service and actively maintain the integration, fix Boltz-related bugs, keep up with Boltz API updates etc. * Integrations maintaining Boltz's non-custodial property down to the end-user. For more details check [#client-side](common-mistakes.md#client-side "mention") Not eligible for the partner program are for instance: * Integrations using Boltz to accept payments for goods and services (e.g. [Boltz BTCPay Plugin](https://github.com/BoltzExchange/boltz-btcpay-plugin/)) * Simple linking or embedding of Boltz Web App ## Payout Terms * Payout within 14 calendar days post month-end. * Default settlement via reusable Lightning addresses or offers. * Denomination of all payout financial data in bitcoin (BTC/sats). * 1,000 sat minimum payout, balances below threshold don't roll over. ::: info Boltz reserves all rights to change conditions of the partner program at any time and to sovereignly decide which integrations to onboard and, in some cases, which to offboard. ::: --- --- url: /backend-development.md description: How to set up a Docker regtest environment for Boltz Backend development. --- # 🐳 Backend Development This document describes how to set up a Docker regtest environment for Boltz Backend development. ## Getting Started * The latest [Node.js LTS and npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) installed. We recommend using [nvm](https://github.com/nvm-sh/nvm#install--update-script) to manage npm installs: `nvm install --lts` * [Docker](https://docs.docker.com/engine/install/) The regtest environment of the Boltz Backend is based on [our regtest Docker Compose](https://github.com/BoltzExchange/regtest). To start the regtest environment run `npm run regtest:start` and to stop it again use `npm run regtest:stop`. To use the nodes in the container with the Boltz Backend, use a configuration file in `~/.boltz/boltz.conf` similar to this one: ```toml \[Boltz configuration] # Boltz Backend Configuration # All paths are relative to the config file unless absolute # Numbers with underscores are for readability (e.g., 1_000 = 1000) # ============================================================================= # Top-Level Options (must be before any [section]) # ============================================================================= # datadir = "/var/lib/boltz" # logpath = "/var/lib/boltz/boltz.log" # loglevel = "info" # error, warn, info, verbose, debug, silly (default: info in production, debug in development) # retryInterval = 15 # Seconds between retries for settling pending submarine swaps (default: 15) # otlpEndpoint = "http://127.0.0.1:4317/v1/traces" # OpenTelemetry # lokiEndpoint = "http://127.0.0.1:3100" # Loki logging # profilingEndpoint = "http://127.0.0.1:4040" # Profiling # Prepay miner fee for reverse swaps (experimental) # prepayminerfee = false # Use P2WSH addresses instead of P2SH nested P2WSH for legacy Submarine Swaps # swapwitnessaddress = false # Network identifier # network = "mainnet" # ============================================================================= # Server Configuration # ============================================================================= [api] host = "127.0.0.1" # 0.0.0.0 for all interfaces port = 9_001 [grpc] host = "127.0.0.1" port = 9_000 # certificates = "/path/to/certs" # disableSsl = false # Development only # ============================================================================= # Database & Cache # ============================================================================= [postgres] host = "127.0.0.1" port = 5432 database = "boltz" username = "boltz" password = "boltz" # Redis cache for production deployments (optional) # [cache] # redisEndpoint = "redis://127.0.0.1:6379" # ============================================================================= # Observability # ============================================================================= # [prometheus] # host = "127.0.0.1" # port = 9_099 # ============================================================================= # Notifications (Mattermost) # ============================================================================= # [notification] # mattermostUrl = "https://mattermost.example.com/" # token = "your-api-token" # channel = "boltz-swaps" # channelAlerts = "boltz-alerts" # prefix = "[Boltz]" # interval = 1 # ============================================================================= # Email Notifications (optional) # ============================================================================= # [email] # enabled = false # host = "smtp.example.com" # port = 587 # secure = false # Use SSL/TLS # user = "your-username" # pass = "your-password" # to = "admin@example.com" # from = "boltz@example.com" # subjectPrefix = "[Boltz]" # ============================================================================= # Trading Pairs # ============================================================================= [[pairs]] # Currency symbols for the trading pair base = "BTC" quote = "BTC" rate = 1 # Hardcoded rate (bypasses exchange APIs) fee = 0.5 # Fee percentage for reverse/chain swaps swapInFee = 0.1 # Fee percentage for submarine swaps (overrides `fee`) maxSwapAmount = 40_294_967 minSwapAmount = 50_000 # invoiceExpiry = 3600 # Defaults to 50% of reverse swap expiry # swapTypes = ["submarine", "reverse", "chain"] # hidden = true # Hide pair from pair list endpoints unless a referral sets "showHidden" # Timeouts in blocks for different swap types [pairs.timeoutDelta] chain = 1440 reverse = 1440 swapMinimal = 1440 swapMaximal = 2880 swapTaproot = 10080 # [pairs.submarineSwap] # minSwapAmount = 1_000 # minBatchedAmount = 21 # Minimum amount for batched swaps # Chain-specific limits (optional) # [pairs.chainSwap] # minSwapAmount = 25_000 # buyFee = 0.1 # sellFee = 0.1 [[pairs]] base = "L-BTC" quote = "BTC" fee = 0.25 swapInFee = 0.1 rate = 1 maxSwapAmount = 40_294_967 minSwapAmount = 100 [pairs.timeoutDelta] chain = 1440 reverse = 1440 swapMinimal = 1440 swapMaximal = 2880 swapTaproot = 10080 [pairs.submarineSwap] minSwapAmount = 1_000 minBatchedAmount = 21 [pairs.chainSwap] minSwapAmount = 25_000 [[pairs]] base = "RBTC" quote = "BTC" rate = 1 fee = 0.25 swapInFee = 0.1 maxSwapAmount = 4_294_967 minSwapAmount = 50_000 [pairs.timeoutDelta] chain = 1440 reverse = 1440 swapMinimal = 1440 swapMaximal = 2880 swapTaproot = 10080 [[pairs]] base = "RBTC" quote = "L-BTC" rate = 1 fee = 0.25 swapInFee = 0.1 maxSwapAmount = 4_294_967 minSwapAmount = 2_500 swapTypes = ["chain"] # Only enable chain swaps for this pair [pairs.chainSwap] buyFee = 0.1 sellFee = 0.1 minSwapAmount = 25_000 [pairs.timeoutDelta] chain = 1440 reverse = 1440 swapMinimal = 1440 swapMaximal = 2880 swapTaproot = 10080 # ============================================================================= # Currency Configuration # ============================================================================= [[currencies]] symbol = "BTC" # Network identifier (bitcoinMainnet, bitcoinTestnet, bitcoinRegtest, etc.) network = "bitcoinRegtest" # Balance requirements (in satoshis) minWalletBalance = 10_000_000 # maxWalletBalance = 100_000_000 # maxUnusedWalletBalance = 10_000_000 # Lightning channel balance requirements (in satoshis) # Note: Used for balance monitoring/alerts, not for swap operations # minLocalBalance = 10_000_000 # Minimum local channel balance (outbound liquidity) # minRemoteBalance = 10_000_000 # Minimum remote channel balance (inbound liquidity) # Swap limits (in satoshis) maxSwapAmount = 40_294_967 minSwapAmount = 10_000 # Zero-confirmation acceptance (in satoshis) maxZeroConfAmount = 0 # Max 0-conf amount (0 = disabled) # maxZeroConfRisk = 100_000 # Alternative: max 0-conf based on risk score # preferredWallet = "core" # 'core' or undefined (defaults to 'core'; 'lnd' not supported anymore) # noRoute = ["nodepubkey1", "nodepubkey2"] # Per-node routing fee overrides (fee ratio: 0.0035 = 0.35%) # [[currencies.routingOffsetExceptions]] # nodeId = "nodepubkey" # offset = 0.0050 [currencies.chain] host = "127.0.0.1" port = 18_443 # Authentication (cookie preferred if both configured) cookie = "./regtest/data/bitcoind/regtest/.cookie" # user = "rpc" # password = "password" wallet = "regtest" # NOTE: ZeroMQ must be configured in Bitcoin Core (bitcoind -zmqpubrawtx=... -zmqpubrawblock=...) # Boltz queries ZMQ endpoints automatically via RPC; they are not configured here # mempoolSpace = "https://mempool.space/api" # Legacy; comma-separated for failover # Minimum fee rate in sat/vbyte (default: 0.2 for Bitcoin, 0.1 for Elements) feeFloor = 1 # Mempool.space fee-source settings (merged with the legacy mempoolSpace above) # [currencies.chain.mempool] # urls = ["https://mempool.space/api"] # maxFeeMultiplier = 10 # Reject mempool fees > max(multiplier * bitcoindFee, bitcoindFee + delta). Default: 10 # maxFeeDelta = 25 # Absolute sat/vbyte delta. Default: 25 # maxAgeSecs = 300 # Reject cached fees older than this. Default: 300 # maxBlockLag = 2 # Reject if mempool.space tip lags bitcoind by more blocks. Default: 2 [[currencies.lnds]] host = "127.0.0.1" port = 11_009 certpath = "./regtest/data/lnd2/tls.cert" macaroonpath = "./regtest/data/lnd2/data/chain/bitcoin/regtest/admin.macaroon" # sslTargetNameOverride = "lnd.example.com" [[currencies.lnds]] host = "127.0.0.1" port = 12_009 certpath = "./regtest/data/lnd3/tls.cert" macaroonpath = "./regtest/data/lnd3/data/chain/bitcoin/regtest/admin.macaroon" [currencies.cln] host = "127.0.0.1" port = 9737 rootCertPath = "./regtest/data/cln2/regtest/ca.pem" privateKeyPath = "./regtest/data/cln2/regtest/client-key.pem" certChainPath = "./regtest/data/cln2/regtest/client.pem" # disableMpp = false # Hold invoice service (for reverse swaps) [currencies.cln.hold] host = "127.0.0.1" port = 9738 rootCertPath = "./regtest/data/cln2/regtest/hold/ca.pem" privateKeyPath = "./regtest/data/cln2/regtest/hold/client-key.pem" certChainPath = "./regtest/data/cln2/regtest/hold/client.pem" # ============================================================================= # Liquid Network Configuration # ============================================================================= [liquid] symbol = "L-BTC" network = "liquidRegtest" maxSwapAmount = 40_294_967 minSwapAmount = 10_000 maxZeroConfAmount = 40_294_967 # preferredWallet = "core" # 'core' or undefined (defaults to 'core'; 'lnd' not supported anymore) [liquid.chain] host = "127.0.0.1" port = 18_884 cookie = "./regtest/data/elements/elements.cookie" wallet = "regtest" # NOTE: ZeroMQ must be configured in Elements daemon (elementsd -zmqpubrawtx=... -zmqpubhashblock=...) # Boltz queries ZMQ endpoints automatically via RPC; they are not configured here # mempoolSpace = "https://mempool.space/liquid/api" # Legacy; comma-separated for failover # Minimum fee rate in sat/vbyte (default: 0.1) feeFloor = 0.1 # Mempool.space fee-source settings (merged with the legacy mempoolSpace above) # [liquid.chain.mempool] # urls = ["https://mempool.space/liquid/api"] # maxFeeMultiplier = 10 # maxFeeDelta = 25 # maxAgeSecs = 300 # maxBlockLag = 2 # [liquid.chain.lowball] # host = "backup.elements" # port = 18_884 # cookie = "./backup/elements.cookie" # ============================================================================= # EVM Chain Configuration (RSK) # ============================================================================= [rsk] # Network name for display networkName = "Anvil" providerEndpoint = "ws://127.0.0.1:8545" # Optional EVM mnemonic derivation path (default: m/44'/60'/0'/0/0) # derivationPath = "m/44'/60'/0'/0/0" # Suggested timelock for commitment swaps in minutes (should be safely above the highest swap timeout) # commitmentTimelock = 20160 # Number of confirmations required for lockup transactions (default: 1) # requiredConfirmations = 1 # Multiple providers for failover (optional) # [[rsk.providers]] # name = "Primary" # endpoint = "http://primary.provider:8545" # # [[rsk.providers]] # name = "Backup" # endpoint = "http://backup.provider:8545" [[rsk.contracts]] etherSwap = "0x8464135c8F25Da09e49BC8782676a84730C318bC" erc20Swap = "0x71C95911E9a5D330f4D621842EC243EE1343292e" [[rsk.tokens]] symbol = "RBTC" # Native token (no decimals/contractAddress) # [[rsk.tokens]] # symbol = "USDT" # decimals = 6 # contractAddress = "0x..." # minWalletBalance = 10_000_000 # maxWalletBalance = 100_000_000 # ============================================================================= # EVM Chain Configuration (Ethereum) # ============================================================================= # [ethereum] # networkName = "Ethereum Mainnet" # providerEndpoint = "https://eth.llamarpc.com" # derivationPath = "m/44'/60'/0'/0/0" # # Suggested timelock for commitment swaps in minutes (should be safely above the highest swap timeout) # commitmentTimelock = 20160 # # Number of confirmations required for lockup transactions (default: 1) # requiredConfirmations = 1 # # Multiple providers for failover: # [[ethereum.providers]] # name = "Infura" # endpoint = "https://mainnet.infura.io/v3/YOUR-KEY" # # [[ethereum.providers]] # name = "Alchemy" # endpoint = "https://eth-mainnet.g.alchemy.com/v2/YOUR-KEY" # # [[ethereum.contracts]] # etherSwap = "0x..." # erc20Swap = "0x..." # # [[ethereum.tokens]] # symbol = "USDT" # decimals = 6 # contractAddress = "0xdac17f958d2ee523a2206206994597c13d831ec7" # ============================================================================= # EVM Chain Configuration (Arbitrum) # ============================================================================= # [arbitrum] # networkName = "Arbitrum One" # providerEndpoint = "https://arb1.arbitrum.io/rpc" # derivationPath = "m/44'/60'/0'/0/0" # # Suggested timelock for commitment swaps in minutes (should be safely above the highest swap timeout) # commitmentTimelock = 20160 # # Number of confirmations required for lockup transactions (default: 1) # requiredConfirmations = 1 # # L1 providers for gas estimation: # [[arbitrum.l1Providers]] # name = "Ethereum Mainnet" # endpoint = "https://eth.llamarpc.com" # # [[arbitrum.contracts]] # etherSwap = "0x..." # erc20Swap = "0x..." # # [[arbitrum.tokens]] # symbol = "TBTC" # decimals = 18 # contractAddress = "0x.." # # [arbitrum.quoters] # weth = "0x.." # # [arbitrum.quoters.uniswapV3] # factory = "0x.." # quoter = "0x.." # router = "0x.." # multicall = "0x.." # Optional; only override for custom deployments # liquidTokens = ["0x..", "0x.."] # Optional; tokens to route through for better quotes # ============================================================================= # ARK Pool Configuration (optional) # ============================================================================= # [ark] # symbol = "ARK" # network = "bitcoinMainnet" # # host = "127.0.0.1" # port = 9_100 # macaroonpath = "/path/to/fulmine/macaroons/admin.macaroon" # # minWalletBalance = 10_000_000 # maxZeroConfAmount = 100_000 # # useLocktimeSeconds = true # Use seconds for locktimes instead of blocks # # [ark.unilateralDelays] # claim = 16 # In blocks # refund = 32 # refundWithoutReceiver = 64 # ============================================================================= # Swap Behavior # ============================================================================= [swap] # Currency symbols for which claims should be deferred deferredClaimSymbols = ["BTC", "L-BTC"] # Batch claim interval (cron format: */15 * * * * = every 15 minutes) batchClaimInterval = "*/15 * * * *" expiryTolerance = 120 # Minutes before expiry to trigger claim cltvDelta = 20 # CLTV delta for Lightning invoices (blocks) # sweepAmountTrigger = 1_000_000 # Scheduled sweep trigger (optional) # [swap.scheduleAmountTrigger] # interval = "0 */4 * * *" # Cron format # threshold = 500_000 # Multipliers applied to base fees to calculate minimum swap amounts (default: 6 for all) # Higher values ensure swaps are economically viable relative to on-chain costs # [swap.minSwapSizeMultipliers] # submarine = 6 # reverse = 6 # chain = 6 # [swap.overpayment] # exemptAmount = 10_000 # Amount below which overpayment is accepted # maxPercentage = 2 # paymentTimeoutMinutes = 60 # ============================================================================= # Routing Fee Configuration # ============================================================================= [routing] # Default routing fee ratio (0.0035 = 0.35%) default = 0.0035 # [routing.overrides] # "nodepubkey1" = 0.0050 # "nodepubkey2" = 0.0025 # ============================================================================= # Node Switching (optional) # ============================================================================= # [nodeSwitch] # swapNode = "" # Default node pubkey # # Amount threshold for switching to CLN: # Option A - Single threshold for all swap types: # clnAmountThreshold = 1_000_000 # # Option B - Per-swap-type thresholds (use instead of Option A): # [nodeSwitch.clnAmountThreshold] # submarine = 1_000_000 # reverse = 5_000_000 # # [nodeSwitch.referralsIds] # "referral-1" = "" # "referral-2" = "" # # [nodeSwitch.preferredForNode] # "destination-node-pubkey" = "" # ============================================================================= # Referral Defaults (optional) # ============================================================================= # # These defaults are merged with referral config from the database. # Database config overrides duplicate keys from this file. # # [referrals.pro.pairs."BTC/BTC"] # showHidden = true # maxRoutingFee = 0.001 # # [referrals.pro.pairs."L-BTC/BTC"] # hidePair = true # ============================================================================= # Exchange Rates # ============================================================================= [rates] # Update interval in minutes (default: 1) interval = 1 # ============================================================================= # Sidecar (Rust Service) Configuration # ============================================================================= [sidecar] # dataDir = "/var/lib/boltz/sidecar" # path = "/usr/local/bin/boltzr" # logFile = "/var/lib/boltz/sidecar/sidecar.log" [sidecar.grpc] host = "127.0.0.1" port = 9003 # disableSsl = false # certificates = "/var/lib/boltz/certificates" [sidecar.ws] host = "127.0.0.1" port = 9004 # [sidecar.ws.messageLimit] # messagesPerMinutePerConnection = 120 # maxSwapUpdateIdsPerMessage = 100 [sidecar.api] host = "127.0.0.1" port = 9005 # [sidecar.webhook] # retryInterval = 60 # requestTimeout = 15 # maxRetries = 5 # blockList = ["blocked.domain.com"] # [sidecar.metrics] # host = "127.0.0.1" # port = 9_093 ``` ## boltzr-cli The `boltzr-cli` tool allows you to interact with a running backend over gRPC. It can be useful for backend development and to perform maintenance tasks. It is available on the `bin` folder after compiling the backend. --- --- url: /api-v1.md description: Endpoint reference for the deprecated Boltz REST API v1. --- # 🤖 REST API v1 (deprecated) This page lists all available endpoints of the deprecated Boltz API v1. ::: warning API v1 is maintained for existing integrations only and does not include the latest features or swap pairs. For any new integrations, we strongly recommend using API v2. ::: ## Basics ### Response and request encoding All the responses to all calls are encoded as `JSON` objects. If endpoints require the client to provide any kind of arguments these also have to be encoded as `JSON` and sent in the body of a `POST` request. Make sure to set the `Content-Type` header of your `POST` requests to `application/json`. ### Error handling If a call fails for some reason, the returned [HTTP status code](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes) will indicate that. Additionally, an object will be returned that includes the reason why the call failed: ```json { "error": "message explaining why the call failed" } ``` ## Backend Version Returns the version of [Boltz Backend](https://github.com/BoltzExchange/boltz-backend) serving the API. A good call to get started and see if Boltz API requests and responses are working. | URL | Response | | -------------- | ------------- | | `GET /version` | `JSON` object | Status Codes: * `200 OK` Response object: * `version`: The deployed version of Boltz Backend. **Examples:** `GET /version` ```json { "version": "3.2.1-7c38088" } ``` ## Supported Pairs In order to create a swap, one first has to know which pairs are supported and what kind of rates, limits and fees are applied when creating a new swap. The following call returns this information. | URL | Response | | --------------- | ------------- | | `GET /getpairs` | `JSON` object | Status Codes: * `200 OK` Response object: * `info`: Contains information about special configuration parameters of the Boltz Backend deployment. Currently there is only one: * `prepay.minerfee`: If the array contains this value, Boltz requires a small invoice for the miner fee to be paid before the actual hold invoice of a Reverse Swap is revealed. * `warnings`: An array of strings that indicate that some feature of Boltz might be disabled or restricted. * `pairs`: An object containing the supported pairs. The keys of the values are the IDs of the pairs (`BTC/BTC` is a special case with mainchain bitcoin as *base asset* and Lightning bitcoin as *quote asset*) and the values itself contain information about the pair: * `hash`: SHA256 hash of the `JSON` encoded data in the pair object. * `rate`: The exchange rate of the pair. * `limits`: A `JSON` Object containing the minimal and maximal amount of the pair's swap. The numbers are denominated **10 \*\* -8** of the *quote asset.* * `maximalZeroConf`: The maximal amounts that will be accepted without chain confirmations by Boltz. 0 indicates that Boltz will not accept 0-conf. See [0-conf](0-conf.md) for more info. * `fees`: A `JSON` object that contains different kinds of fees: * `percentage`: The percentage of the "send amount" that is charged by Boltz as "Boltz Fee" for swaps from quote to base asset (e.g. Lightning -> Bitcoin). * `percentageSwapIn`: The percentage of the "send amount" that is charged by Boltz as "Boltz Fee" for swaps from base to quote asset (e.g. Bitcoin -> Lightning). * `minerFees`: The network fees charged for locking up and claiming funds onchain. These values are absolute, denominated in **10 \*\* -8** of the quote asset. **Examples:** `GET /getpairs` Response: ```json { "info": [], "warnings": [], "pairs": { "BTC/BTC": { "hash": "05d0f522ef08dd66fa0f87f167cc1380eaf7e5227e698941ecb44876e0736cb8", "rate": 1, "limits": { "maximal": 10000000, "minimal": 50000, "maximalZeroConf": { "baseAsset": 0, "quoteAsset": 0 } }, "fees": { "percentage": 0.5, "percentageSwapIn": 0.1, "minerFees": { "baseAsset": { "normal": 1360, "reverse": { "claim": 1104, "lockup": 1224 } }, "quoteAsset": { "normal": 1360, "reverse": { "claim": 1104, "lockup": 1224 } } } } }, "L-BTC/BTC": { "hash": "769215ae0f8cb14d250374a77de530ac2887c927dc08f8efce74f3634df03171", "rate": 1, "limits": { "maximal": 10000000, "minimal": 10000, "maximalZeroConf": { "baseAsset": 0, "quoteAsset": 0 } }, "fees": { "percentage": 0.25, "percentageSwapIn": 0.1, "minerFees": { "baseAsset": { "normal": 147, "reverse": { "claim": 152, "lockup": 276 } }, "quoteAsset": { "normal": 1360, "reverse": { "claim": 1104, "lockup": 1224 } } } } } } } ``` ## Creating Normal Submarine Swaps This section walks you through creating Normal Submarine Swaps (Chain -> Lightning). They differ slightly depending on the kind of bitcoin that are swapped, more information can be found below. **Please note that Boltz works with 10 \*\* -8 decimals internally** and all amounts in the API endpoints follow this denomination. All requests to create Normal Submarine Swaps have the following common values in the API request: * `type`: The type of swap to create. For Normal Submarine Swaps this is `submarine` . * `pairId`: The pair of which the swap should be created, query available pairs via [`/getpairs`](api-v1.md#supported-pairs). * `orderSide`: Possible values are `buy` & `sell`. Currently, we recommend using `sell` across all pairs of swap type `submarine`. The value `buy` for Normal Submarine Swaps of e.g. the `L-BTC/BTC` pair signifies a swap from Bitcoin mainchain to *Liquid* Lightning. Currently, this is not supported and the backend will return `"error": "L-BTC has no lightning support"`. If you already know the amount to be swapped, you should also set `invoice`. * `invoice`: The Lightning invoice of the user that should be paid. If the amount is **not** known yet, a **preimage hash has be specified**. The invoice that is provided later [during the lifecycle of the Submarine Swap](api-v1.md#set-invoice) has to have the *same preimage hash* as the one specified here. * `preimageHash`: Hash of a preimage that will be used for the invoice that is set later on. We recommend verifying that pair data fetched previously (like `minerFees`) is still valid by additionally passing the `pairHash` argument in this call. * `pairHash`: `hash` string of the pair object of [`/getpairs`](api-v1.md#supported-pairs). Members of our [partner program](api-v1.md#querying-referral-fees) may set this optional referral parameter to get a percentage of the fees earned from referred swaps as kickback. * `referralId`: Partner referral ID. | URL | Response | | ------------------ | ------------- | | `POST /createswap` | `JSON` object | Status Codes: * `201 Created` * `400 Bad Request`: The swap could not be created. Check the `error` string in the `JSON` object of the body of the response for more information. Response objects: * `id`: Id of the newly created swap. * `timeoutBlockHeight`: Base asset block height at which the swap will expire. * `address`: Address in which the bitcoin will be locked up. For the Bitcoin mainchain, this is a SegWit `P2SHP2WSH` address (`P2WSH` nested in a `P2SH`) for the sake of compatibility, for Liquid a `P2WSH` address and for EVM chains the address of the corresponding swap contract. If a Lightning invoice is set in this call, one will also find the following values in the response: * `acceptZeroConf`: Whether Boltz will accept 0-conf for this swap. * `expectedAmount`: The amount that Boltz expects to be locked on the chain. ### Normal Swaps: UTXO Chains For UTXO chains, `/createswap` requests have to contain one additional parameter: * `refundPublicKey`: public key of a keypair that will allow the user to refund the locked up bitcoin once the time lock is expired. This keypair has to be generated and stored by the client integrating Boltz API. Responses also contain one additional value: * `redeemScript`: redeem script from which the `address` is derived. The redeem script should be used, [to verify](dont-trust-verify.md#utxo-chain-address-verification) that Boltz did provide the correct address. In case the address is for the Liquid Network, it will be blinded by a key that is also in the response: * `blindingKey`: hex encoded private key with which the address was blinded If the invoice has been set, you will also get this value: * `bip21`: a [BIP21 payment request](https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki) for the `expectedAmount` and the `address` **Examples:** `POST /createswap` Request body: ```json { "type": "submarine", "pairId": "BTC/BTC", "orderSide": "sell", "refundPublicKey": "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGBkraVi05Eyn6slRQV5h+fX6rtpudOq0LqPEnbnbxRshMdhS56vKWawUNLkLZZ4hKsTdbJZvgTtO/rDc2WI/Gw", "invoice": "lntb1m1pjv0dt5pp5y9dl3z50c5g6p26a86g432zdzvdlx6a565hk55a2ellz3t9f84jsdqqcqzzsxqyz5vqsp5a4k0f59u62na3fngv24nv5xjuxyf6qjnnj806se373h4gt9fmejq9qyyssqpeh42yy72pqzfwdfehvuru9s735vrgg324lxdp9gg8w6m379w8ajd3sxyy6f0qqfqa6vhk5k4pqfz6nys3u5xf68wcjyjygykn7za6cqf6flce" } ``` Response: ```json { "id": "E63LC4", "bip21": "bitcoin:2NBBYeBZgY64nJKiibnGokwrBBPjoQeMzyx?amount=0.0010054&label=Send%20to%20BTC%20lightning", "address": "2NBBYeBZgY64nJKiibnGokwrBBPjoQeMzyx", "redeemScript": "a9148f8d01a3e1a794024fa78bd9c81d5ae9bb1c56d287632102bba4fbfe50ea8caf880cb367f7b0083c7e91c2bc2808c817823bd36385e3a376670319b125b17503aaaae268ac", "acceptZeroConf": false, "expectedAmount": 100540, "timeoutBlockHeight": 2470169 } ``` ### Normal Swaps: EVM Chains Swaps from account-based EVM chains like RSK do not require a new address for every swap. `/createswap` takes the details of the swap (like lightning invoice and pair) and Boltz waits until the user locked e.g. RBTC in the contract. The addresses of those contracts can be queried with [`/getcontracts`](api-v1.md#swap-contracts) and the address of the contract that needs to be used for the swap is also returned in the response of this request. For EVM chains, the request does not require any additional values, but the response returns one additional value: * `claimAddress`: The EVM chain destination address of Boltz. It is specified in the`lock` function of the swap contract. **Examples:** `POST /createswap` Request body: ```json { "type": "submarine", "pairId": "RBTC/BTC", "orderSide": "sell", "invoice": "lntb700u1pj5gft9pp5he32emhme8nrl6u7yyaa7s8svcl3kcwlwz99yzsck09rh4tr9fzqdqqcqzzsxqyz5vqsp5jztrjz3qx8tekwgwqlp7up2c0fvyvpdnd54flk6f9hgytv43ca7q9qyyssqk3l4zr9xs80x0ymg3jczd0t6crwpq5klv9uu6xzgn8fvsr9rraqhluaxstcnxg4ka0frerzrxh8ghgf2ey44hvgdhjke4zcrw8ukugspe9jrvy" } ``` Response: ```json { "id": "wXKtEu", "address": "0x165F8E654b3Fe310A854805323718D51977ad95F", "claimAddress": "0x4217BD283e9Dc9A2cE3d5D20fAE34AA0902C28db", "acceptZeroConf": false, "expectedAmount": 70513, "timeoutBlockHeight": 4455644 } ``` ### Swap Rates In case the amount to be swapped is not known when creating a Normal Submarine Swap, the invoice can be set afterwards; even if the chain bitcoin were sent already. In this case, you want to first use this endpoint to figure out what the exact amount of the invoice should be based on the already sent bitcoin. Send a `POST` request with a `JSON` encoded body with this value: * `id`: Id of the Submarine Swap. | URL | Response | | ----------------- | ------------- | | `POST /swaprates` | `JSON` object | Status Codes: * `200 OK` * `400 Bad Request`: The invoice amount could not be calculated. Check the `error` string in the `JSON` object of the body of the response for more information. A common case is where the user did not lock up chain bitcoin yet, which is a requirement in order to calculate an invoice amount: `"error": "no coins were locked up yet"`. Response object: * `invoiceAmount`: Amount of the invoice that should be set with [`/setinvoice`](api-v1.md#set-invoice). **Examples:** Request body: ```json { "id": "BY8asG" } ``` Response: ```json { "invoiceAmount": 15713393 } ``` ### Setting an Invoice In case the amount to be swapped is not known when creating a Normal Submarine Swap, the invoice can be set afterwards; even if the chain bitcoin were sent already. Please keep in mind that the invoice **has to have the same preimage hash** that was specified when creating the swap. Although the invoice can be changed after setting it initially, this endpoint will only work if Boltz did not try to pay the initial invoice yet. Requests to this endpoint have to be `POST` and should have the following values in its `JSON` encoded body: * `id`: Id of the swap for which the invoice should be set. * `invoice`: Invoice of the user that should be paid. | URL | Response | | ------------------ | ------------- | | `POST /setinvoice` | `JSON` object | Status Codes: * `200 OK` * `400 Bad Request`: The invoice could not be set. Check the `error` string in the `JSON` object of the body of the response for more information. Response objects: What is returned when the invoice is set depends on the status of the Normal Submarine Swap. If no funds were sent (status [`swap.created`](lifecycle.md#normal-submarine-swaps)) the endpoint will return a `JSON` object with these values: * `acceptZeroConf`: Whether Boltz will accept 0-conf for this swap. * `expectedAmount`: The amount that Boltz expects you to lock in the chain HTLC. * `bip21`: A [BIP21 payment request](https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki) for the `expectedAmount` of bitcoin and the `address` (only set when swapping from UTXO chains). If chain bitcoin were sent already (status [`transaction.mempool`](lifecycle.md#normal-submarine-swaps) or [`transaction.confirmed`](lifecycle.md#normal-submarine-swaps)) the endpoint will return an empty `JSON` object, **signifying success**. In case this endpoint is called again after an invoice was set and Boltz already tried to pay the invoice, the following response objects are returned: * `error`: Error message explaining that Boltz tried to pay the invoice already and that it cannot be changed anymore. * `invoice`: The invoice that was set and that will be used for the swap. **Examples:** If *no* bitcoin were sent yet: `POST /setinvoice` Request body: ```json { "id": "UwHIPg", "invoice": "lnbcrt1m1p08epqjpp5yvv222x7te9asyzuhmjym083lwqpp5vlem09ewfeufyrp6f76w2sdql2djkuepqw3hjqnz5gvsxzerywfjhxuccqzpgsp5u6kxgf9daf64ptvl2ht74m6duc2neywx3ecvwxs07vf7egw5dy5s9qy9qsqakms0e7ww46q9cq2fa2ymrcx6nucfknjalkm5w4ywvjpfxdp5ya82drvdvxhqzzt2ysysh5u7rellzjse37fng3vsqafuwwz3kv4ykcquy8k29" } ``` Response: ```json { "acceptZeroConf": false, "expectedAmount": 1359564, "bip21": "bitcoin:QNaGS7WM31xANXQCbmrhXfnxUjxiGFpFwM?amount=0.01359564&label=Submarine%20Swap%20to%20BTC" } ``` If bitcoin *were sent* already: `POST /setinvoice` Request body: ```json { "id": "UwHIPg", "invoice": "lnbcrt1m1p08epqjpp5yvv222x7te9asyzuhmjym083lwqpp5vlem09ewfeufyrp6f76w2sdql2djkuepqw3hjqnz5gvsxzerywfjhxuccqzpgsp5u6kxgf9daf64ptvl2ht74m6duc2neywx3ecvwxs07vf7egw5dy5s9qy9qsqakms0e7ww46q9cq2fa2ymrcx6nucfknjalkm5w4ywvjpfxdp5ya82drvdvxhqzzt2ysysh5u7rellzjse37fng3vsqafuwwz3kv4ykcquy8k29" } ``` Response: ```json {} ``` If the invoice was previously successfully set and Boltz tried to pay it already: `POST /setinvoice` Request body: ```json { "id": "UwHIPg", "invoice": "lnbcrt100u1p0gv8hjpp5j0vs0te6wykahrp3aammm46m73n2afzk6a87ezfp3p58qpcpu4wqdqqcqzpgsp5j6wue634lac577xnupy8auvq7n9062vshvvc6xszq4jt5q9phhzq9qy9qsqhs7zrs98tu669xz7w0gqy96g5pvs9p6lssmyseg7a92kpjlzramk8khyzkd8x4nl2zasekmwt45z6pe78rk032lkmshjdnesw2vukwgqtglt89" } ``` Response: ```json { "error": "lightning payment in progress already", "invoice": "lnbcrt1m1p08epqjpp5yvv222x7te9asyzuhmjym083lwqpp5vlem09ewfeufyrp6f76w2sdql2djkuepqw3hjqnz5gvsxzerywfjhxuccqzpgsp5u6kxgf9daf64ptvl2ht74m6duc2neywx3ecvwxs07vf7egw5dy5s9qy9qsqakms0e7ww46q9cq2fa2ymrcx6nucfknjalkm5w4ywvjpfxdp5ya82drvdvxhqzzt2ysysh5u7rellzjse37fng3vsqafuwwz3kv4ykcquy8k29" } ``` ## Creating Reverse Submarine Swaps This section walks you creating Reverse Submarine Swaps (Lightning -> Chain). Similar to Normal Submarine Swaps, requests and responses change slightly depending on the kind of bitcoin involved in the swap. Keep in mind, **Boltz uses 10 \*\* -8 as denomination** for responses on the API. All requests to create Reverse Submarine Swaps have the following common values in the API request: * `type`: Type of the swap to create. For Reverse Submarine Swaps this is `reversesubmarine`. * `pairId`: The pair of which the swap should be created, query available pairs via [`/getpairs`](api-v1.md#supported-pairs). * `orderSide`: Possible values are `buy` & `sell`. Currently, we recommend using `buy` across all pairs of swap type `reversesubmarine`. The value `sell` for reverse swaps of e.g. the `L-BTC/BTC` pair signifies a swap from Bitcoin mainchain to *Liquid* Lightning. Currently, this is not supported and the backend will return `"error": "L-BTC has no lightning support"` * `preimageHash`: The SHA256 hash of a preimage that was generated by the client. The size of that preimage *has* to be 32 bytes, otherwise claiming will fail. There are two options how to set the amount of a reverse swap. The first option is to specify the amount of the invoice that Boltz will generate: * `invoiceAmount`: amount of the invoice that will be generated by Boltz The second option is to specify the amount that will be locked in the chain HTLC. That amount is *not* what the user will finally receive because of transaction fees required to claim the HTLC. But those can be approximated easily in advance with third party tools like [mempool.space](https://mempool.space/). * `onchainAmount`: amount Boltz will lock in the chain HTLC We recommend verifying that pair data fetched previously (like `minerFees`) is still valid by additionally passing the `pairHash` argument in this call. * `pairHash`: `hash` string in the pair object of [`/getpairs`](api-v1.md#supported-pairs). Members of our [partner program](api-v1.md#querying-referral-fees) may set this optional referral parameter to get a percentage of the fees earned from referred swaps as kickback. * `referralId`: Partner referral ID. | URL | Response | | ------------------ | ------------- | | `POST /createswap` | `JSON` object | Status Codes: * `201 Created` * `400 Bad Request`: The swap could not be created. Check the `error` string in the `JSON` object of the body of the response for more information. Response objects: * `id`: Id of the newly created swap. * `lockupAddress`: Address derived from the `redeemScript` or contract in which Boltz will lock up bitcoin. * `invoice`: Hold invoice that needs to be paid before Boltz locks up bitcoin. * `timeoutBlockHeight`: Base asset block height at which the swap will expire. In case the invoice amount was specified, the amount that will be locked in the chain HTLC is also returned: * `onchainAmount`: Amount of chain bitcoin that will be locked by Boltz. Boltz Backend finally features the so-called Prepay Miner Fee protocol that requires an invoice for network fees to be paid before the actual hold `invoice` of a Reverse Submarine Swap. If this protocol is enabled, the response object will also contain a `minerFeeInvoice`. Once the `minerFeeInvoice` is paid, Boltz will send the event `minerfee.paid` and when the actual hold `invoice` is paid, the chain bitcoin will be sent. ::: info This protocol is a countermeasure against a specific attack vector and is currently \_not\_ enabled on Boltz Mainnet. ::: ### Reverse Swaps: UTXO Chains For UTXO chains, `/createswap` requests have to contain one additional parameter: * `claimPublicKey`: Public key of a keypair that will allow the user to claim the locked up bitcoin with the preimage. This keypair has to be generated and stored by the Boltz API client. Responses also contain one additional value: * `redeemScript`: Redeem script from which the lockup address is derived. The redeem script should be used, [to verify](dont-trust-verify.md#utxo-chain-address-verification) that Boltz didn't try to cheat by creating an address without a HTLC. In case the lockup address is on the Liquid Network, it will be blinded by a key that is returned in the response too: * `blindingKey`: Hex encoded private key with which the address was blinded. **Examples:** `POST /createswap` Request body: ```json { "type": "reversesubmarine", "pairId": "L-BTC/BTC", "orderSide": "buy", "invoiceAmount": 1000000, "preimageHash": "51a05b15e66ecd12bf6b1b62a678e63add0185bc5f41d2cd013611f7a4b6703f", "claimPublicKey": "03b76c1fe14bab50e52a026f35287fda75b9304bcf311ee85b4d32482400a436f5" } ``` Response: ```json { "id": "v3CfMa", "invoice": "lntb10m1pjvsy8ppp52xs9k90xdmx390mtrd32v78x8twsrpdutaqa9ngpxcgl0f9kwqlsdpz2djkuepqw3hjqnpdgf2yxgrpv3j8yetnwvcqz95xqrrsssp5hzcjq972f9cl8c6u3zechepm65hjceaqvlzye6kc23qkz5rhva6q9qyyssq4kkw9fwjq7n9cm4j3ajj2a92ka0zyeg3sxppfy932c62pnsqkw7nhvg9rrxztszw37wqtal4cchw2f4s09qe48pngsl9euv7wjlz93qqrfx60s", "blindingKey": "897034f717beb12a3c2b7ae8c08c5c4def7bc7cfb6efa3713c617f28d90d1419", "redeemScript": "8201208763a914be1abd8e8d7ef7e64a9c6e1e2f498f3a92e078a2882103b76c1fe14bab50e52a026f35287fda75b9304bcf311ee85b4d32482400a436f5677503dbf40eb175210330fd4cfd53b5c20886415c1b67d2daa87bce2761b9be009e9d1f9eec4419ba5968ac", "lockupAddress": "tlq1qqdd0v79wcqnpvujf5mfp88d5cz8rynk0awr6m84ca8pzn39kwagsh4z204s9d5sww3cxckd47wjxlqwl3u6tgdqfa877txqt9m8wgk22qwyp5yzxaf40", "timeoutBlockHeight": 980187, "onchainAmount": 995724 } ``` *In case the Prepay Miner Fee protocol is enabled:* Request body: ```json { "type": "reversesubmarine", "pairId": "BTC/BTC", "orderSide": "buy", "claimPublicKey": "0391fbaf549578fd7c2cb26b216441825bd780d85dba1f3d706e2f206587e96266", "invoiceAmount": 100000, "preimageHash": "2215034def003b63b2717fccd4ce8259f4807a39318c14e2bdd42639ca989a45" } ``` Response body: ```json { "id": "mVSKyF", "invoice": "lnbcrt996940n1p0dhjr3pp5yg2sxn00qqak8vn30lxdfn5zt86gq73exxxpfc4a6snrnj5cnfzsdql2djkuepqw3hjqsj5gvsxzerywfjhxuccqzy0sp5hjcvwl2glrq9n3vzm8072cdruz3hhz70edml8g0u76gryve6np4q9qy9qsqzw5w8ulxjgrg478hz4enjrw0a9tedl8s3n879xqh3mhn0pxrvajrz9qnnsr58twx4a30gk57d4fykm7x3v2vcamw7k4ny9fkpwl65vcpw8v5em", "redeemScript": "8201208763a914fb75ff5dc4272c2da33d744615905f54b62de41588210391fbaf549578fd7c2cb26b216441825bd780d85dba1f3d706e2f206587e962666775028e01b1752102c22801bd7dd3a6afb780671c1c983fcd91fa46826eadd82e325e7e13bb348a9768ac", "lockupAddress": "bcrt1qweryu6nk8gn5lj8ar5kjdy476wynheszg0lumu6jx83l2v6f435stlel03", "onchainAmount": 98694, "timeoutBlockHeight": 398, "minerFeeInvoice": "lnbcrt3060n1p0dhjr3pp5sk2u4rt0z8rrl6jj62d6szqvsdejj8kjcxa8tdt4dau5rtyskj6qdp4f45kuetjypnx2efqvehhygznwashqgr5dusyy4zrypskgerjv4ehxcqzpgsp5qtsm5vfy9yq8kjpthla67jagmcxnj529pm3edk94npf6fekq2sxq9qy9qsqmun0z8ed4kp9dhp7lthvzdrx3ngmjs32smx6l4hvyyktv92mf348aftgrwf44sl94ewywr3sw8dc4acy63yamxxpjtd4pkkr2uw2h5gpqc3d3y" } ``` ### Reverse Swaps: EVM Chains For EVM chains, `/createswap` requests have to contain one additional parameter: * `claimAddress`: address from which the bitcoin will be claimed The response also has one more property: * `refundAddress`: the address of Boltz which is specified as refund address when it is locking up funds Boltz features an optional "gasless" protocol that allows a user to pay an additional lightning invoice to pay for gas on EVM chains like RSK to claim a reverse swap. This is useful for users who do not not have e.g. RBTC on RSK yet. In the gasless protocol, using the example of RSK, Boltz sends just enough RBTC to the `claimAddress` in the swap process for the user to successfully claim the swap. To use this protocol, set the following property in the request body to `true`. * `gasless`: If the gasless protocol should be used for the reverse swap. When the gasless protocol is used, the response will contain two more values. One is the amount of RBTC that will be sent to the `claimAddress`. The other one is an invoice to pay for the sent `gasAssetAmount`. * `gasAssetAmount`: Amount of e.g. RBTC that will be sent to the `claimAddress` to be used as gas to claim the swap. * `gaslessInvoice`: Invoice that pays for `gasAssetAmount`. Only when both invoices (`gaslessInvoice` and `invoice)` are paid, Boltz will lock the chain bitcoin to proceed with the swap. **Examples:** `POST /createswap` Request body: ```json { "type": "reversesubmarine", "pairId": "RBTC/BTC", "orderSide": "buy", "claimAddress": "0x88532974EC20559608681A53F4Ac8C34dd5e2804", "invoiceAmount": 100000, "preimageHash": "295b93a766959d607861ab7b7a6bf9e178e7c69c3cc4ca715065dfe9d6eea352" } ``` Response body: ```json { "id": "NPT9VE", "invoice": "lntb1m1pj5g2sssp5p4nn4jk4w7p208c22eytkvclleghsfeug4cu2u9qpaqnflrtp3yqpp599de8fmxjkwkq7rp4dah56leu9uw035u8nzv5u2svh07n4hw5dfqdpq2djkuepqw3hjq5jz23pjqctyv3ex2umnxqyp2xqcqz959qxpqysgqca37dvrmpy294383zsszeqyny0fdqtkr8tllnvvj0g2w65lgcwaszah5kcs4hej9dsm9c3tj43tklnc0y6fqy5964h4xz0rylz4nqugpz0y90y", "refundAddress": "0x4217BD283e9Dc9A2cE3d5D20fAE34AA0902C28db", "lockupAddress": "0x165F8E654b3Fe310A854805323718D51977ad95F", "timeoutBlockHeight": 4454445, "onchainAmount": 99195 } ``` ## Swap Status ::: info Before handling status events of this method, we recommended to read: [Swap Types & States](lifecycle.md) ::: To query the status of a swap one can use this endpoint which returns a `JSON` object containing the status of the swap. Possible states and status events are documented in the section [Swap Types & States](lifecycle.md)*.* Requests querying the status of a swap have to be `POST` and contain a single value in its `JSON` encoded body: * `id`: Id of the swap of which the status is queried. | URL | Response | | ------------------ | ------------- | | `POST /swapstatus` | `JSON` object | Status Codes: * `200 OK` * `404 Not Found`: The swap with the provided id couldn't be found. * `400 Bad Request`: The `id` argument wasn't provided. Response object: * `status`: Status of the swap, e.g. `transaction.mempool` & `transaction.claimed` for successful Normal Submarine Swaps and `transaction.mempool` and `transaction.confirmed` for successful Reverse Submarine Swaps. * `transaction`: For Reverse Submarine Swaps, this field contains lockup transaction details in the states`transaction.mempool` and `transaction.confirmed:` * `id`: Id of the lockup transaction. * `hex`: Hex encoded lockup transaction (only set for transactions on UTXO chains). * `eta`: If the status is `transaction.mempool`, this value is the estimated time of arrival (ETA) in blocks of when the transaction will be confirmed. Only set for transactions on UTXO chains. * `zeroConfRejected`: Set to `true` for Swaps with the status `transaction.mempool` and a lockup transaction that is not eligible for [0-conf](0-conf.md). * `failureReason`: Set when it's necessary to further clarify the failure reason. **Examples:** `POST /swapstatus` Request body: ```json { "id": "Asnj2Y" } ``` Response: ```json { "status": "invoice.paid" } ``` `POST /swapstatus` Request body: ```json { "id": "ryUK9G" } ``` Response: ```json { "status": "transaction.mempool", "transaction": { "id": "31fcddf287d985eef85211b75976cd903dba3008a8e13b597e1b54941278c29f", "hex": "01000000000101618cd5c50221577a1b98ae4a73f652917f9d2e343b9bc6a978239da78dfcbc630000000000ffffffff02b878010000000000220020ddc8dd3bcb45660e421fc3129bfdcb317446c27ce909369d4c8cb17bbd6d4951c718393b00000000160014ea9e0fc9432fc8b6831e94ac3974d46d1ba1c62f024830450221008b14ecce2eebb2ec7e56c53de4796603fbd22cfd269f3a5249446b79987dec36022059b68568a57ebfbd50cc792af3d514c507af20e795ff5f0bf2a02d2fc44be223012103a1ad2a3891018e856700a20a4dea6bea9f4bba58ab3ca7cbaefaa06e805770d100000000", "eta": 2 } } ``` `POST /swapstatus` Request body: ```json { "id": "gnIthU" } ``` Response: ```json { "status": "transaction.lockupFailed", "failureReason": "locked 1396075383 is less than expected 1396075384" } ``` ## Swap Status Stream To avoid querying the [`/swapstatus`](api-v1.md#swap-status) endpoint regularly to get the latest swap status, this endpoint streams swap status updates via [Server-Side Events](https://www.w3schools.com/html/html5_serversentevents.asp). Requests to this endpoint have to provide the required swap `id` parameter via an URL parameter because all requests have to be of the method `GET`. Every event in the Server-Side stream has data that is encoded exactly like the `JSON` object of the `/swapstatus` endpoint. Please refer to the examples below for a reference implementation in JavaScript in how to handle the stream. | URL | Response | | ----------------------- | ------------------------ | | `GET /streamswapstatus` | Server-Side event stream | **Examples:** Server-Side event streams have to be handled differently from regular HTTP responses. Below is a sample implementation in JavaScript and also what a raw response of a Server-Side event stream looks like. Sample implementation in JavaScript: ```javascript var stream = new EventSource(boltzApi + '/streamswapstatus?id=' + swapId); source.onmessage = function (event) { const data = JSON.parse(event.data); console.log('Swap status update:' + data.status); }; ``` Raw response: ``` data: {"status":"transaction.mempool"} data: {"status":"invoice.paid"} ``` ## Swap Timeouts Boltz Swaps have different timeouts for each pair. This endpoint allows querying those timeouts denominated in blocks of the base and quote chain. | URL | Response | | --------------- | ------------- | | `GET /timeouts` | `JSON` object | Status Codes: * `200 OK` Response object: * `timeouts`: A `JSON` object with the pairs as keys and a `JSON` object with the timeouts as values. **Examples:** `GET /timeouts` Response: ```json { "timeouts": { "BTC/BTC": { "base": { "reverse": 144, "swapMinimal": 144, "swapMaximal": 288 }, "quote": { "reverse": 144, "swapMinimal": 144, "swapMaximal": 288 } }, "L-BTC/BTC": { "base": { "reverse": 1440, "swapMinimal": 1440, "swapMaximal": 2880 }, "quote": { "reverse": 144, "swapMinimal": 144, "swapMaximal": 288 } } } } ``` ## Swap Contracts To query the addresses of contracts used by Boltz for swaps on EVM chains like RSK, the following endpoint can be queried: | URL | Response | | ------------------- | ------------- | | `GET /getcontracts` | `JSON` object | Status Codes: * `200 OK` Response object: * `chain`: `JSON` object that contains all relevant contract addresses of this EVM chain. * `network`: `JSON` object that contains information about the network. * `chainId`: Id of the EVM chain. * `swapContracts`: `JSON` object containing swap contract addresses as values. * `tokens`: `JSON` object with the ticker symbol of the supported token as key and its address as value. **Examples:** `GET /getcontracts` Response: ```json { "rsk": { "network": { "chainId": 31 }, "swapContracts": { "EtherSwap": "0x165F8E654b3Fe310A854805323718D51977ad95F", "ERC20Swap": "0x5F51247606d29Df75Af11475A85F7072f6472345" }, "tokens": {} } } ``` ## Fee Estimations Boltz provides an API endpoint that returns fee estimations for all supported chains. These fee estimations are *not* enforced by Boltz and merely represent a recommendation. For UTXO chains like Bitcoin it is important to mention that if 0-conf is accepted by Boltz for a particular pair and to be used with Normal Submarine Swaps, the lockup transaction has to have at least 80% of the recommended `sat/vbyte` value. For more information refer to the [0-conf](0-conf.md) section. | URL | Response | | ----------------------- | ------------- | | `GET /getfeeestimation` | `JSON` object | Status Codes: * `200 OK` Response object: This endpoint returns a `JSON` object of which each key is the symbol of a chain and each value the estimated fee for that chain denominated in `sat/vbyte` for UTXO chains like Bitcoin or `GWEI` for EVM chains like RSK. **Examples:** `GET /getfeeestimation` Response: ```json { "BTC": 16, "L-BTC": 0.11 } ``` ## Raw Transactions Boltz API also allows for querying raw transactions of all supported UTXO chains, irrespective of whether the transactions are still in the mempool or already included in a block. Note, that Boltz does *not* provide any kind of cryptographic proof that the transaction was included in a block. Also this call is primarily kept for backward compatibility with older integrations, it is *not* needed to construct transactions as the response of [`/swapstatus`](api-v1.md#swap-status) provides all necessary info. Requests querying for transactions have to be `POST` and contain two arguments in its `JSON` encoded body: * `currency`: The chain to be queried for the transaction. * `transactionId`: The id of the transaction that should be queried. | URL | Response | | ---------------------- | ------------- | | `POST /gettransaction` | `JSON` object | Status Codes: * `200 OK` * `400 Bad Request`: An argument wasn't provided or the transaction couldn't be found. Response object: * `transactionHex`: The requested transaction encoded in hex. **Examples:** `POST /gettransaction` Request body: ```json { "currency": "BTC", "transactionId": "0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098" } ``` Response: ```json { "transactionHex": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000" } ``` ## Lockup Transactions The following endpoint can be used to query the user's lockup transaction of a Normal Submarine Swap on UTXO chains. The request has to be `POST` and contain the following argument in the `JSON` encoded body: * `id`: Id of the Submarine Swap. | URL | Response | | -------------------------- | ------------- | | `POST /getswaptransaction` | `JSON` object | Status Codes: * `200 OK` * `400 Bad Request`: An argument wasn't provided, or the swap couldn't be found. Response object: * `transactionHex`: The lockup transaction of the Normal Submarine Swap encoded in hex. * `timeoutBlockHeight`: The block height at which the HTLC in the lockup transaction will time out. If the HTLC has not timed out yet, there will be an additional value in the response: * `timeoutEta`: UNIX timestamp at which the HTLC is expected to time out. **Examples:** `POST /getswaptransaction` Request body: ```json { "id": "KR8XaB" } ``` Response: ```json { "transactionHex": "020000000001015bf3fe03071edae971276831963d45821ce6bc95c567fd7832bee3b1848254ba0000000000feffffff02b82a75e80000000017a914ca9b26af0865c2a59f0d6c41ede68f03264a52398700e1f5050000000017a914d2271732308baf018a1c2c751a3afb197f2a2e7e870247304402200206ae10cd66267caea1c6e0fca0275924a85fa377572f599304c1abb6a3a97302204335eed108151048de7ba56fd644352831e3d453b412547d8b762f4e96ee1e310121035b6440fe45a8bf7c1d9d238fa39a128ee78b58df73377bfeb1d6d752849714c800000000", "timeoutBlockHeight": 252, "timeoutEta": 1586353245 } ``` This call works for Normal Submarine Swaps only. If used for Reverse Submarine Swaps, the response will be: ```json { "error": "could not find swap with id: CR8XaB" } ``` ## Broadcasting Transactions This endpoint is used to broadcast transactions on UTXO chains. It is similar to [`/gettransaction`](api-v1.md#raw-transactions) but instead of getting the hex representation of existing transactions, this call broadcasts *new* transactions to the network. It is mainly intended to be used as an easy way to broadcast [claim & refund transactions](claiming-swaps.md) by Boltz API clients that don't have access to a full node. We encourage checking out alternatives like mempool.space's public API for [Bitcoin](https://mempool.space/docs/api/rest#post-transaction) or [Liquid](https://liquid.network/docs/api/rest#post-transaction) to reduce reliance on Boltz. The call returns the id of the broadcast transaction,which can be used to verify that the refund transaction was broadcast successfully using a third party service. Requests broadcasting transactions have to be `POST` and contain two arguments in the `JSON` encoded body: * `currency`: Which network the transaction should be broadcast on. * `transactionHex`: The HEX encoded transaction. | URL | Response | | ---------------------------- | ------------- | | `POST /broadcasttransaction` | `JSON` object | Status Codes: * `200 OK` * `400 Bad Request`: An argument wasn't provided or the node that was used to broadcast the transaction returned an error. Response object: * `transactionId`: The id of the transaction that was broadcast. **Example:** `POST /broadcasttransaction` Request body: ```json { "currency": "BTC", "transactionHex": "02000000000101f7ddc247aad2c1e91b2495cf6814aa183b46785cf21f44f43a3c3419c09d377201000000171600142a805e4cfaa6fb79360917a5b0b9c5fcb0dfe6e9ffffffff02938601000000000017a9141137c50104d0814e8663ded75b43ddaa7b9d192b87e8cc96000000000017a9148f6df517d00e650d8c5e6bfa0986b775b256609e870247304402202e8a8572ce3cb232a7b48483bdc280feba7f9cf8c163ac2df091e54dfcf90bb6022042ba2b3e4d89220b3b39b52a444dff2010b22144099aaa348d83301f2ae456be01210341720559e7375a409b03e814415a6c15fc142c5a9e78a83831ff6fe4706d352900000000" } ``` Response: ```json { "transactionId": "52ff6682b0bff109e6c6d97de6b6d075f7241c9ac364e02de6315281e423d816" } ``` There is one special case: when trying to broadcast a refund transaction for a swap that has not timed out yet, the backend will return some additional information in addition to the `error` in the `JSON` encoded response: * `error`: The reason why broadcasting failed. In this special case always: `non-mandatory-script-verify-flag (Locktime requirement not satisfied) (code 64)`. * `timeoutEta`: UNIX timestamp at which the HTLC is expected to time out. * `timeoutBlockHeight`: Block height at which the HTLC in the lockup transaction will time out. **Example:** `POST /broadcasttransaction` Request body: ```json { "currency": "BTC", "transactionHex": "0100000000010113a19f721bc15b17a63700a7bf0056b7640b2c80239577f7ee95618baa34958f0100000023220020e1088f54c6d46e861bf4c5590bdff35c6f277b10e4e2787e1a5df23f1e540a3dfdffffff01bcdff5050000000016001486590ca595782212ce4fe0a4be8855f55f7f288603483045022100c142b9616659ba1728c254d8e275a304dc31c9139f005f9a97938cb1606c370e0220686d4e6166a7adab916f9af823c2f173aa9bd7f47a581909bda95881e1c00e07010064a9146aad1375552e58e9d4281a331caf271d0d160e3c8763210396ed47336687c51bc7e2bd32d0fc7a377d33c888f02a0647a7f1156761614a0d6702c401b1752103ffb18860cbe08060bd93a17abe4b436c46d0ee5b43fd0c24ba5bd65d6f42beb568ac00000000" } ``` Response: ```json { "timeoutEta": 1586374147, "timeoutBlockHeight": 452, "error": "non-mandatory-script-verify-flag (Locktime requirement not satisfied) (code 64)" } ``` ## Authentication Boltz API does not require any sort of authentication to perform swaps. However, some API endpoints like [querying referral fees](api-v1.md#querying-referral-fees) for members of our partner program, do. To authenticate your API request, three request headers have to be set: * `TS`: current UNIX timestamp (can only deviate from server time by 1 minute at most) * `API-KEY`: your API key * `API-HMAC`: SHA256 HMAC encoded as HEX (lower case letters!) of the following values: * value of the `TS` header * method of the HTTP request (e.g. `GET` or `POST`) * request path, including the leading slash (e.g. `/referrals/query`) * if the request method is `POST`, the body of the request TypeScript Node.js example: ```typescript import axios from 'axios'; import { createHmac } from 'crypto'; const path = '/referrals/query'; const ts = Math.round(new Date().getTime() / 1000); const hmac = createHmac('sha256', argv.secret) .update(`${ts}GET${path}`) .digest('hex'); try { const res = await axios.get( `https://${argv.rest.host}:${argv.rest.port}${path}`, { headers: { TS: ts, 'API-KEY': argv.key, 'API-HMAC': hmac, }, }, ); console.log(JSON.stringify(res.data, undefined, 2)); } catch (e) { const error = e as any; console.log(`${error.message}: ${JSON.stringify(error.response.data)}`); } ``` ## Querying Referral Fees Members of the Boltz partner program can request a referral key () to get a percentage of the fees earned from referred swaps as kickback. To query for their referrals, they can send an [authenticated](api-v1.md#authentication) request to this endpoint. | URL | Response | | ---------------------- | ------------- | | `GET /referrals/query` | `JSON` object | Status Codes: * `200 OK` * `401 Unauthorized`: Missing or invalid request authentication. Response object: The response of a valid request is grouped by year, month and referral key. **Examples:** `GET /referrals/query` Response: ```json { "2021": { "9": { "cliTest": { "BTC": 60 } } } } ``` ## Lightning Node Info This endpoint allows you to query info like public keys and URIs of the lightning nodes operated by Boltz. | URL | Response | | --------------- | ------------- | | `GET /getnodes` | `JSON` object | Status Codes: * `200 OK` Response object: * `nodes`: `JSON` with the symbol of the chain on which the Lightning node is running as key and the following objects: * `nodeKey`: Public key of the Lightning node. * `uris`: Array of the URIs on which the Lightning node is reachable. **Examples:** `GET /getnodes` Response: ```json { "nodes": { "BTC": { "uris": [ "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2@45.86.229.190:9735", "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2@d7kak4gpnbamm3b4ufq54aatgm3alhx3jwmu6kyy2bgjaauinkipz3id.onion:9735" ], "nodeKey": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2" } } } ``` ## Lightning Node Statistics For display purposes on our website, basic statistics about our lightning nodes are exposed via the following endpoint: | URL | Response | | ---------------- | ------------- | | `GET /nodestats` | `JSON` object | Status Codes: * `200 OK` Response object: * `nodes`: `JSON` with the symbol of the chain on which the Lightning node is running as key, and the following objects: * `peers`: Number of peers. * `channels`: Number of public channels. * `oldestChannel`: UNIX timestamp of the block in which the opening transaction of the oldest channel was included. * `capacity`: Sum of the capacity of all public channels. **Examples:** `GET /nodestats` Response: ```json { "nodes": { "BTC": { "peers": 79, "channels": 103, "oldestChannel": 1590772669, "capacity": 369879555 } } } ```