Skip to navigation

Cross Chain Swaps with Bitcoin

View as Markdown

Steps to Cross Chain Swap Native BTC

This guide will walk you through using the /quotes and /status endpoints on 0x’s Cross Chain API to:

  • Buy BTC: sell a token on an EVM chain and receive native BTC at a Bitcoin address.
  • Sell BTC: sell native BTC from a Bitcoin address and receive a token on another chain.

The examples use USDC on Base as the other side of the swap.

Terms

TermMeaning
UTXOAn unspent output of an earlier Bitcoin transaction. The sell amount and the miner fee are paid from the UTXOs at your address. See Transaction data.
PSBTA Partially Signed Bitcoin Transaction. The sell quote returns one as hex. See BIP 174.
WIFA text form of a Bitcoin private key. The sign step reads it from BITCOIN_WIF.

0. Prerequisites

Make sure you have:

  • 0x API key
  • A mainnet Bitcoin address. Use a P2WPKH address that starts with bc1q when you can.
  • To buy BTC: a funded EVM wallet, set up as in Get started.
  • To sell BTC: BTC at your Bitcoin address in a confirmed UTXO. The amount must cover the sell amount and the miner fee.
  • To sell BTC: a WIF in BITCOIN_WIF. The example uses bitcoinjs-lib, ecpair, and tiny-secp256k1.

Bitcoin values

Use these values in every Bitcoin request:

ItemValue
Chain namebitcoin
Chain ID999999999994
Native BTC tokenbc1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqmql8k8
Decimals8

Write BTC amounts as integers in base units, the same as EVM token amounts. The base unit of BTC is the satoshi. BTC has 8 decimals, so 1 BTC = 100,000,000 satoshis.

The API accepts mainnet addresses of these types:

TypeStarts with
P2PKH1
P2SH3
P2WPKHbc1q, 42 characters
P2WSHbc1q, 62 characters
P2TRbc1p

Buy BTC with a token on an EVM chain

1. Fetch a quote

Set destinationChain to bitcoin and buyToken to the native BTC token.

const quotesParams = new URLSearchParams({
originChain: 'base', // Base mainnet
destinationChain: 'bitcoin', // Bitcoin mainnet
sellToken: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base
buyToken: 'bc1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqmql8k8', // native BTC
sellAmount: '20000000', // 20 USDC (6 decimals)
originAddress: '$USER_EVM_ADDRESS', // Base address that sells the USDC
destinationAddress: '$USER_BITCOIN_ADDRESS', // Bitcoin address that receives the BTC
sortQuotesBy: 'price',
maxNumQuotes: '3',
});
const headers = {
'0x-api-key': '[api-key]', // Get your live API key from the 0x Dashboard (https://dashboard.0x.org/apps)
};
const response = await fetch('https://api.0x.org/cross-chain/quotes?' + quotesParams.toString(), { headers });
const quoteResponse = await response.json();

destinationAddress is required. Without it, the API returns HTTP 400 INPUT_INVALID with the reason Bitcoin destinationAddress is required.

2. Set an allowance and send the transaction

import { createWalletClient, http, erc20Abi } from 'viem';
import { base } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';
const account = privateKeyToAccount(process.env.EVM_PRIVATE_KEY as `0x${string}`);
const walletClient = createWalletClient({
account,
chain: base,
transport: http('https://mainnet.base.org'),
});
const quote = quoteResponse.quotes[0];
if (quote.issues.allowance) {
const approveTxHash = await walletClient.writeContract({
address: quoteResponse.sellToken as `0x${string}`,
abi: erc20Abi,
functionName: 'approve',
args: [quote.issues.allowance.spender as `0x${string}`, BigInt(quote.sellAmount)],
});
await walletClient.waitForTransactionReceipt({ hash: approveTxHash });
}
const txHash = await walletClient.sendTransaction({
to: quote.transaction.details.to as `0x${string}`,
data: quote.transaction.details.data as `0x${string}`,
value: BigInt(quote.transaction.details.value),
gas: quote.transaction.details.gas ? BigInt(quote.transaction.details.gas) : undefined,
});

3. Monitor the cross chain execution

Poll /status with the Base transaction hash and the quoteId.

const statusParams = new URLSearchParams({
originChain: 'base',
originTxHash: txHash, // hash of the Base transaction from step 2
quoteId: quoteResponse.quotes[0].quoteId,
});
const statusResponse = await fetch('https://api.0x.org/cross-chain/status?' + statusParams.toString(), { headers });
console.log(await statusResponse.json());

The BTC is at destinationAddress when status is bridge_filled.

Sell BTC for a token on another chain

1. Fetch a quote

Set originChain to bitcoin and sellToken to the native BTC token.

const quotesParams = new URLSearchParams({
originChain: 'bitcoin', // Bitcoin mainnet
destinationChain: 'base', // Base mainnet
sellToken: 'bc1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqmql8k8', // native BTC
buyToken: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base
sellAmount: '1000000', // 0.01 BTC (8 decimals)
originAddress: '$USER_BITCOIN_ADDRESS', // Bitcoin address that holds the BTC
destinationAddress: '$USER_EVM_ADDRESS', // Base address that receives the USDC
sortQuotesBy: 'price',
maxNumQuotes: '3',
});
const headers = {
'0x-api-key': '[api-key]', // Get your live API key from the 0x Dashboard (https://dashboard.0x.org/apps)
};
const response = await fetch('https://api.0x.org/cross-chain/quotes?' + quotesParams.toString(), { headers });
const quoteResponse = await response.json();

Check these points before you use the quote:

  • Do not send feeBps, feeRecipient, or feeToken. The API returns HTTP 400 with the reason Fee collection is not supported when origin chain is Bitcoin.
  • allowanceTarget is null. Bitcoin has no approval step and no wrap step.
{
"liquidityAvailable": true,
"allowanceTarget": null,
"originChainId": 999999999994,
"originChain": "bitcoin",
"destinationChainId": 8453,
"destinationChain": "base",
"sellToken": "bc1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqmql8k8",
"buyToken": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"issues": {
"allowance": null,
"balance": null
},
"quotes": [
{
"sellAmount": "1000000",
"buyAmount": "<USDC base units>",
"minBuyAmount": "<USDC base units>",
"quoteId": "<quote id>",
"gasCosts": {
"chainType": "btc",
"total": "<satoshis>"
},
"steps": [
{
"type": "bridge",
"originChainId": 999999999994,
"destinationChainId": 8453,
"sellToken": "bc1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqmql8k8",
"buyToken": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"sellAmount": "1000000",
"buyAmount": "<USDC base units>",
"minBuyAmount": "<USDC base units>",
"provider": "near_intents",
"estimatedTimeSeconds": "<seconds>"
}
],
"transaction": {
"chainType": "btc",
"details": {
"psbt": "70736274ff..."
}
},
"estimatedTimeSeconds": "<seconds>"
}
]
}

A Bitcoin confirmation takes about 10 minutes.

2. Sign the PSBT

import * as bitcoin from 'bitcoinjs-lib';
import * as ecc from 'tiny-secp256k1';
import { ECPairFactory } from 'ecpair';
bitcoin.initEccLib(ecc);
const ECPair = ECPairFactory(ecc);
const keyPair = ECPair.fromWIF(process.env.BITCOIN_WIF);
const quote = quoteResponse.quotes[0];
const psbt = bitcoin.Psbt.fromHex(quote.transaction.details.psbt.replace(/^0x/, ''));
psbt.signAllInputs(keyPair);
psbt.finalizeAllInputs();
const rawTx = psbt.extractTransaction().toHex();

3. Broadcast the transaction

mempool.space returns the txid.

const broadcastResponse = await fetch('https://mempool.space/api/tx', {
method: 'POST',
headers: { 'Content-Type': 'text/plain' },
body: rawTx,
});
const txid = (await broadcastResponse.text()).trim();
if (!broadcastResponse.ok) {
throw new Error(txid);
}

4. Monitor the cross chain execution

Poll /status with originChain set to bitcoin, originTxHash set to the txid, and the quoteId.

const statusParams = new URLSearchParams({
originChain: 'bitcoin',
originTxHash: txid, // 64 hex characters, with or without a 0x prefix
quoteId: quoteResponse.quotes[0].quoteId,
});
const statusResponse = await fetch('https://api.0x.org/cross-chain/status?' + statusParams.toString(), { headers });
console.log(await statusResponse.json());

For the status lifecycle and failure handling, see Tracking Transaction Status.