curl --request POST \
--url https://api.satora.io/swap/evm/bitcoin \
--header 'Content-Type: application/json' \
--data '
{
"btc_htlc_script_version": 123,
"claim_pk": "<string>",
"evm_chain_id": 1,
"hash_lock": "<string>",
"token_address": "<string>",
"user_address": "<string>",
"user_id": "<string>",
"amount_in": "<string>",
"amount_out": 1,
"bridge_source_chain": "<string>",
"bridge_source_token_address": "<string>",
"extra_fees": 1,
"gasless": true,
"referral_code": "<string>",
"target_address": "<string>"
}
'import requests
url = "https://api.satora.io/swap/evm/bitcoin"
payload = {
"btc_htlc_script_version": 123,
"claim_pk": "<string>",
"evm_chain_id": 1,
"hash_lock": "<string>",
"token_address": "<string>",
"user_address": "<string>",
"user_id": "<string>",
"amount_in": "<string>",
"amount_out": 1,
"bridge_source_chain": "<string>",
"bridge_source_token_address": "<string>",
"extra_fees": 1,
"gasless": True,
"referral_code": "<string>",
"target_address": "<string>"
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
btc_htlc_script_version: 123,
claim_pk: '<string>',
evm_chain_id: 1,
hash_lock: '<string>',
token_address: '<string>',
user_address: '<string>',
user_id: '<string>',
amount_in: '<string>',
amount_out: 1,
bridge_source_chain: '<string>',
bridge_source_token_address: '<string>',
extra_fees: 1,
gasless: true,
referral_code: '<string>',
target_address: '<string>'
})
};
fetch('https://api.satora.io/swap/evm/bitcoin', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.satora.io/swap/evm/bitcoin",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'btc_htlc_script_version' => 123,
'claim_pk' => '<string>',
'evm_chain_id' => 1,
'hash_lock' => '<string>',
'token_address' => '<string>',
'user_address' => '<string>',
'user_id' => '<string>',
'amount_in' => '<string>',
'amount_out' => 1,
'bridge_source_chain' => '<string>',
'bridge_source_token_address' => '<string>',
'extra_fees' => 1,
'gasless' => true,
'referral_code' => '<string>',
'target_address' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.satora.io/swap/evm/bitcoin"
payload := strings.NewReader("{\n \"btc_htlc_script_version\": 123,\n \"claim_pk\": \"<string>\",\n \"evm_chain_id\": 1,\n \"hash_lock\": \"<string>\",\n \"token_address\": \"<string>\",\n \"user_address\": \"<string>\",\n \"user_id\": \"<string>\",\n \"amount_in\": \"<string>\",\n \"amount_out\": 1,\n \"bridge_source_chain\": \"<string>\",\n \"bridge_source_token_address\": \"<string>\",\n \"extra_fees\": 1,\n \"gasless\": true,\n \"referral_code\": \"<string>\",\n \"target_address\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.satora.io/swap/evm/bitcoin")
.header("Content-Type", "application/json")
.body("{\n \"btc_htlc_script_version\": 123,\n \"claim_pk\": \"<string>\",\n \"evm_chain_id\": 1,\n \"hash_lock\": \"<string>\",\n \"token_address\": \"<string>\",\n \"user_address\": \"<string>\",\n \"user_id\": \"<string>\",\n \"amount_in\": \"<string>\",\n \"amount_out\": 1,\n \"bridge_source_chain\": \"<string>\",\n \"bridge_source_token_address\": \"<string>\",\n \"extra_fees\": 1,\n \"gasless\": true,\n \"referral_code\": \"<string>\",\n \"target_address\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.satora.io/swap/evm/bitcoin")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"btc_htlc_script_version\": 123,\n \"claim_pk\": \"<string>\",\n \"evm_chain_id\": 1,\n \"hash_lock\": \"<string>\",\n \"token_address\": \"<string>\",\n \"user_address\": \"<string>\",\n \"user_id\": \"<string>\",\n \"amount_in\": \"<string>\",\n \"amount_out\": 1,\n \"bridge_source_chain\": \"<string>\",\n \"bridge_source_token_address\": \"<string>\",\n \"extra_fees\": 1,\n \"gasless\": true,\n \"referral_code\": \"<string>\",\n \"target_address\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"btc_hash_lock": "<string>",
"btc_htlc_address": "<string>",
"btc_htlc_script_version": 123,
"btc_refund_locktime": 123,
"btc_server_refund_pk": "<string>",
"btc_user_claim_pk": "<string>",
"chain": "<string>",
"client_evm_address": "<string>",
"created_at": "2023-11-07T05:31:56Z",
"evm_chain_id": 123,
"evm_coordinator_address": "<string>",
"evm_expected_sats": "<string>",
"evm_funding_confirmations": 1,
"evm_hash_lock": "<string>",
"evm_htlc_address": "<string>",
"evm_htlc_kind": "erc20",
"evm_htlc_version": 123,
"evm_refund_locktime": 123,
"fee_sats": 123,
"gasless": true,
"id": "<string>",
"network": "<string>",
"server_evm_address": "<string>",
"source_amount": "<string>",
"source_token": {
"chain": "Arkade",
"decimals": 1,
"name": "<string>",
"symbol": "<string>",
"token_id": "btc"
},
"status": "pending",
"target_amount": "<string>",
"target_token": {
"chain": "Arkade",
"decimals": 1,
"name": "<string>",
"symbol": "<string>",
"token_id": "btc"
},
"wbtc_address": "<string>",
"bridge_source_chain": "<string>",
"bridge_source_token_address": "<string>",
"btc_claim_txid": "<string>",
"btc_fund_txid": "<string>",
"btc_fund_vout": 123,
"evm_claim_txid": "<string>",
"evm_fund_txid": "<string>",
"target_btc_address": "<string>"
}{
"error": "<string>"
}{
"error": "<string>"
}{
"error": "<string>"
}Create a chain-agnostic EVM-to-Bitcoin swap.
curl --request POST \
--url https://api.satora.io/swap/evm/bitcoin \
--header 'Content-Type: application/json' \
--data '
{
"btc_htlc_script_version": 123,
"claim_pk": "<string>",
"evm_chain_id": 1,
"hash_lock": "<string>",
"token_address": "<string>",
"user_address": "<string>",
"user_id": "<string>",
"amount_in": "<string>",
"amount_out": 1,
"bridge_source_chain": "<string>",
"bridge_source_token_address": "<string>",
"extra_fees": 1,
"gasless": true,
"referral_code": "<string>",
"target_address": "<string>"
}
'import requests
url = "https://api.satora.io/swap/evm/bitcoin"
payload = {
"btc_htlc_script_version": 123,
"claim_pk": "<string>",
"evm_chain_id": 1,
"hash_lock": "<string>",
"token_address": "<string>",
"user_address": "<string>",
"user_id": "<string>",
"amount_in": "<string>",
"amount_out": 1,
"bridge_source_chain": "<string>",
"bridge_source_token_address": "<string>",
"extra_fees": 1,
"gasless": True,
"referral_code": "<string>",
"target_address": "<string>"
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
btc_htlc_script_version: 123,
claim_pk: '<string>',
evm_chain_id: 1,
hash_lock: '<string>',
token_address: '<string>',
user_address: '<string>',
user_id: '<string>',
amount_in: '<string>',
amount_out: 1,
bridge_source_chain: '<string>',
bridge_source_token_address: '<string>',
extra_fees: 1,
gasless: true,
referral_code: '<string>',
target_address: '<string>'
})
};
fetch('https://api.satora.io/swap/evm/bitcoin', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.satora.io/swap/evm/bitcoin",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'btc_htlc_script_version' => 123,
'claim_pk' => '<string>',
'evm_chain_id' => 1,
'hash_lock' => '<string>',
'token_address' => '<string>',
'user_address' => '<string>',
'user_id' => '<string>',
'amount_in' => '<string>',
'amount_out' => 1,
'bridge_source_chain' => '<string>',
'bridge_source_token_address' => '<string>',
'extra_fees' => 1,
'gasless' => true,
'referral_code' => '<string>',
'target_address' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.satora.io/swap/evm/bitcoin"
payload := strings.NewReader("{\n \"btc_htlc_script_version\": 123,\n \"claim_pk\": \"<string>\",\n \"evm_chain_id\": 1,\n \"hash_lock\": \"<string>\",\n \"token_address\": \"<string>\",\n \"user_address\": \"<string>\",\n \"user_id\": \"<string>\",\n \"amount_in\": \"<string>\",\n \"amount_out\": 1,\n \"bridge_source_chain\": \"<string>\",\n \"bridge_source_token_address\": \"<string>\",\n \"extra_fees\": 1,\n \"gasless\": true,\n \"referral_code\": \"<string>\",\n \"target_address\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.satora.io/swap/evm/bitcoin")
.header("Content-Type", "application/json")
.body("{\n \"btc_htlc_script_version\": 123,\n \"claim_pk\": \"<string>\",\n \"evm_chain_id\": 1,\n \"hash_lock\": \"<string>\",\n \"token_address\": \"<string>\",\n \"user_address\": \"<string>\",\n \"user_id\": \"<string>\",\n \"amount_in\": \"<string>\",\n \"amount_out\": 1,\n \"bridge_source_chain\": \"<string>\",\n \"bridge_source_token_address\": \"<string>\",\n \"extra_fees\": 1,\n \"gasless\": true,\n \"referral_code\": \"<string>\",\n \"target_address\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.satora.io/swap/evm/bitcoin")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"btc_htlc_script_version\": 123,\n \"claim_pk\": \"<string>\",\n \"evm_chain_id\": 1,\n \"hash_lock\": \"<string>\",\n \"token_address\": \"<string>\",\n \"user_address\": \"<string>\",\n \"user_id\": \"<string>\",\n \"amount_in\": \"<string>\",\n \"amount_out\": 1,\n \"bridge_source_chain\": \"<string>\",\n \"bridge_source_token_address\": \"<string>\",\n \"extra_fees\": 1,\n \"gasless\": true,\n \"referral_code\": \"<string>\",\n \"target_address\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"btc_hash_lock": "<string>",
"btc_htlc_address": "<string>",
"btc_htlc_script_version": 123,
"btc_refund_locktime": 123,
"btc_server_refund_pk": "<string>",
"btc_user_claim_pk": "<string>",
"chain": "<string>",
"client_evm_address": "<string>",
"created_at": "2023-11-07T05:31:56Z",
"evm_chain_id": 123,
"evm_coordinator_address": "<string>",
"evm_expected_sats": "<string>",
"evm_funding_confirmations": 1,
"evm_hash_lock": "<string>",
"evm_htlc_address": "<string>",
"evm_htlc_kind": "erc20",
"evm_htlc_version": 123,
"evm_refund_locktime": 123,
"fee_sats": 123,
"gasless": true,
"id": "<string>",
"network": "<string>",
"server_evm_address": "<string>",
"source_amount": "<string>",
"source_token": {
"chain": "Arkade",
"decimals": 1,
"name": "<string>",
"symbol": "<string>",
"token_id": "btc"
},
"status": "pending",
"target_amount": "<string>",
"target_token": {
"chain": "Arkade",
"decimals": 1,
"name": "<string>",
"symbol": "<string>",
"token_id": "btc"
},
"wbtc_address": "<string>",
"bridge_source_chain": "<string>",
"bridge_source_token_address": "<string>",
"btc_claim_txid": "<string>",
"btc_fund_txid": "<string>",
"btc_fund_vout": 123,
"evm_claim_txid": "<string>",
"evm_fund_txid": "<string>",
"target_btc_address": "<string>"
}{
"error": "<string>"
}{
"error": "<string>"
}{
"error": "<string>"
}Body
Request to create an EVM-to-Bitcoin swap.
User sends any ERC-20 token on EVM, receives on-chain BTC (Taproot HTLC).
The caller specifies the source chain via evm_chain_id and the token
via its ERC-20 contract token_address.
Bitcoin HTLC script version. New swaps must use version 1.
User's BTC public key for claiming BTC from the on-chain Taproot HTLC.
Numeric EVM chain ID: 1 (Ethereum), 137 (Polygon), 42161 (Arbitrum).
x >= 0Hash lock (0x-prefixed 32-byte hex). Client generates secret, computes SHA256(secret).
ERC-20 contract address of the source token on the EVM chain.
User's EVM address (sender of the ERC-20 token).
User ID derived from wallet for recovery purposes.
Source token amount in smallest units (mutually exclusive with amount_out).
Desired BTC output in sats (mutually exclusive with amount_in).
x >= 0Optional: CCTP bridge source chain (e.g., "Ethereum", "Optimism", "Solana"). When set,
the user's source USDC originates on this chain and hops through CCTPv2 to
Arbitrum before the HTLC is created. The backend pads deposit_amount by
the fast-transfer fee at UserOp-calldata time.
Optional: USDC address on the bridge source chain (0x address on EVM chains, base58 mint on Solana).
Optional per-swap fee surcharge in basis points (0..=max_extra_fee_bps configured on the matching developer key).
x >= 0Whether to use gasless relay for funding (server submits tx on behalf of user).
Optional referral code for tracking.
User's target BTC address to receive claimed funds.
Response
Swap created successfully
EVM → Bitcoin (on-chain) swap response
BTC hash lock (RIPEMD160 of EVM hash lock)
On-chain BTC Taproot HTLC address (server creates, user claims)
BTC HTLC refund locktime (unix timestamp)
Server's x-only public key for refunding BTC
User's x-only public key for claiming BTC
EVM chain name
User's EVM address (funds the EVM HTLC)
Numeric EVM chain ID
The coordinator this swap is pinned to (the HTLC refundAddress).
Expected WBTC amount (in sats) on EVM
Blocks the deposit must be buried under before the server acts on it (funding finality on this chain). With the funding receipt's block and the chain head a client can show the confirmation progress.
x >= 0EVM hash lock (SHA-256, 0x-prefixed 32-byte hex)
EVM HTLC contract address (user creates, server claims)
HTLC family of the lock: erc20 (HTLCErc20 + HTLCCoordinator) or
native (HTLCNative + HTLCNativeCoordinator).
erc20, native HTLCErc20 contract VERSION of the deployment this swap lives on — the EIP-712 domain version string for redeem/refund signatures.
EVM HTLC refund locktime (unix timestamp)
Whether this swap was created with gasless relay (Permit2)
Server's EVM address (claims the EVM HTLC)
Source token amount in smallest units
Show child attributes
Show child attributes
Atomic swap state machine for BTC --> Target Asset swaps using HTLCs.
Overview
This enum tracks the state of an atomic swap between Bitcoin (via Lightning/Arkade) and Target Asset (on Polygon, Ethereum, etc). The swap uses Hash Time-Locked Contracts (HTLCs) to ensure atomicity without requiring trust.
Normal Flow
Pending → ClientFunded → ServerFunded → ClientRedeemed → ServerRedeemed
- Client creates swap request →
Pending - Client sends BTC →
ClientFunded - Server locks WBTC in Polygon HTLC →
ServerFunded - Client reveals secret to claim Target Asset →
ClientRedeemed - Server uses secret to claim BTC →
ServerRedeemed(terminal)
Refund Flows
Pending → Expired (no funding)
ClientFunded → ClientRefunded (client refunds before server funds)
ServerFunded → ClientFundedServerRefunded (HTLC timeout)
State Diagram
See swap_states.md for a complete state diagram and detailed documentation.
Security
- Bitcoin HTLC timeout must be longer than Polygon HTLC timeout
- Secret becomes public when client claims TargetAsset on Polygon
ClientRefundedServerFundedis an error state that should never occur
pending, clientfundingseen, clientfunded, clientrefunded, serverfunded, clientredeeming, clientredeemed, serverredeemed, clientfundedserverrefunded, clientrefundedserverfunded, clientrefundedserverrefunded, expired, clientinvalidfunded, clientfundedtoolate, serverwontfund, clientredeemedandclientrefunded Expected BTC amount (in sats) user will receive
Show child attributes
Show child attributes
WBTC token contract address on the EVM chain
CCTP bridge source chain. Set when the source USDC originated on
another CCTP chain and hopped to Arbitrum via CCTPv2 before the
HTLC was created. source_token still reports the post-hop
Arbitrum USDC; this field tells the SDK what chain the user is
expected to burn from.
Native USDC address on the bridge source chain (base58 mint on Solana).
User's target BTC address for receiving claimed funds