curl --request POST \
--url https://api.satora.io/swap/bitcoin/evm \
--header 'Content-Type: application/json' \
--data '
{
"btc_htlc_script_version": 123,
"claiming_address": "<string>",
"evm_chain_id": 1,
"hash_lock": "<string>",
"refund_pk": "<string>",
"target_address": "<string>",
"token_address": "<string>",
"user_id": "<string>",
"amount_in": 1,
"amount_out": "<string>",
"bridge_recipient_setup": true,
"bridge_target_chain": "<string>",
"bridge_target_token_address": "<string>",
"extra_fees": 1,
"gasless": true,
"referral_code": "<string>"
}
'import requests
url = "https://api.satora.io/swap/bitcoin/evm"
payload = {
"btc_htlc_script_version": 123,
"claiming_address": "<string>",
"evm_chain_id": 1,
"hash_lock": "<string>",
"refund_pk": "<string>",
"target_address": "<string>",
"token_address": "<string>",
"user_id": "<string>",
"amount_in": 1,
"amount_out": "<string>",
"bridge_recipient_setup": True,
"bridge_target_chain": "<string>",
"bridge_target_token_address": "<string>",
"extra_fees": 1,
"gasless": True,
"referral_code": "<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,
claiming_address: '<string>',
evm_chain_id: 1,
hash_lock: '<string>',
refund_pk: '<string>',
target_address: '<string>',
token_address: '<string>',
user_id: '<string>',
amount_in: 1,
amount_out: '<string>',
bridge_recipient_setup: true,
bridge_target_chain: '<string>',
bridge_target_token_address: '<string>',
extra_fees: 1,
gasless: true,
referral_code: '<string>'
})
};
fetch('https://api.satora.io/swap/bitcoin/evm', 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/bitcoin/evm",
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,
'claiming_address' => '<string>',
'evm_chain_id' => 1,
'hash_lock' => '<string>',
'refund_pk' => '<string>',
'target_address' => '<string>',
'token_address' => '<string>',
'user_id' => '<string>',
'amount_in' => 1,
'amount_out' => '<string>',
'bridge_recipient_setup' => true,
'bridge_target_chain' => '<string>',
'bridge_target_token_address' => '<string>',
'extra_fees' => 1,
'gasless' => true,
'referral_code' => '<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/bitcoin/evm"
payload := strings.NewReader("{\n \"btc_htlc_script_version\": 123,\n \"claiming_address\": \"<string>\",\n \"evm_chain_id\": 1,\n \"hash_lock\": \"<string>\",\n \"refund_pk\": \"<string>\",\n \"target_address\": \"<string>\",\n \"token_address\": \"<string>\",\n \"user_id\": \"<string>\",\n \"amount_in\": 1,\n \"amount_out\": \"<string>\",\n \"bridge_recipient_setup\": true,\n \"bridge_target_chain\": \"<string>\",\n \"bridge_target_token_address\": \"<string>\",\n \"extra_fees\": 1,\n \"gasless\": true,\n \"referral_code\": \"<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/bitcoin/evm")
.header("Content-Type", "application/json")
.body("{\n \"btc_htlc_script_version\": 123,\n \"claiming_address\": \"<string>\",\n \"evm_chain_id\": 1,\n \"hash_lock\": \"<string>\",\n \"refund_pk\": \"<string>\",\n \"target_address\": \"<string>\",\n \"token_address\": \"<string>\",\n \"user_id\": \"<string>\",\n \"amount_in\": 1,\n \"amount_out\": \"<string>\",\n \"bridge_recipient_setup\": true,\n \"bridge_target_chain\": \"<string>\",\n \"bridge_target_token_address\": \"<string>\",\n \"extra_fees\": 1,\n \"gasless\": true,\n \"referral_code\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.satora.io/swap/bitcoin/evm")
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 \"claiming_address\": \"<string>\",\n \"evm_chain_id\": 1,\n \"hash_lock\": \"<string>\",\n \"refund_pk\": \"<string>\",\n \"target_address\": \"<string>\",\n \"token_address\": \"<string>\",\n \"user_id\": \"<string>\",\n \"amount_in\": 1,\n \"amount_out\": \"<string>\",\n \"bridge_recipient_setup\": true,\n \"bridge_target_chain\": \"<string>\",\n \"bridge_target_token_address\": \"<string>\",\n \"extra_fees\": 1,\n \"gasless\": true,\n \"referral_code\": \"<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_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_hash_lock": "<string>",
"evm_htlc_address": "<string>",
"evm_htlc_kind": "erc20",
"evm_htlc_version": 123,
"evm_refund_locktime": 123,
"fee_sats": 123,
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"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_target_chain": "<string>",
"bridge_target_token_address": "<string>",
"btc_claim_txid": "<string>",
"btc_fund_txid": "<string>",
"evm_claim_txid": "<string>",
"evm_fund_txid": "<string>",
"target_evm_address": "<string>"
}{
"error": "<string>"
}{
"error": "<string>"
}{
"error": "<string>"
}Create a chain-agnostic Bitcoin-to-EVM swap.
curl --request POST \
--url https://api.satora.io/swap/bitcoin/evm \
--header 'Content-Type: application/json' \
--data '
{
"btc_htlc_script_version": 123,
"claiming_address": "<string>",
"evm_chain_id": 1,
"hash_lock": "<string>",
"refund_pk": "<string>",
"target_address": "<string>",
"token_address": "<string>",
"user_id": "<string>",
"amount_in": 1,
"amount_out": "<string>",
"bridge_recipient_setup": true,
"bridge_target_chain": "<string>",
"bridge_target_token_address": "<string>",
"extra_fees": 1,
"gasless": true,
"referral_code": "<string>"
}
'import requests
url = "https://api.satora.io/swap/bitcoin/evm"
payload = {
"btc_htlc_script_version": 123,
"claiming_address": "<string>",
"evm_chain_id": 1,
"hash_lock": "<string>",
"refund_pk": "<string>",
"target_address": "<string>",
"token_address": "<string>",
"user_id": "<string>",
"amount_in": 1,
"amount_out": "<string>",
"bridge_recipient_setup": True,
"bridge_target_chain": "<string>",
"bridge_target_token_address": "<string>",
"extra_fees": 1,
"gasless": True,
"referral_code": "<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,
claiming_address: '<string>',
evm_chain_id: 1,
hash_lock: '<string>',
refund_pk: '<string>',
target_address: '<string>',
token_address: '<string>',
user_id: '<string>',
amount_in: 1,
amount_out: '<string>',
bridge_recipient_setup: true,
bridge_target_chain: '<string>',
bridge_target_token_address: '<string>',
extra_fees: 1,
gasless: true,
referral_code: '<string>'
})
};
fetch('https://api.satora.io/swap/bitcoin/evm', 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/bitcoin/evm",
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,
'claiming_address' => '<string>',
'evm_chain_id' => 1,
'hash_lock' => '<string>',
'refund_pk' => '<string>',
'target_address' => '<string>',
'token_address' => '<string>',
'user_id' => '<string>',
'amount_in' => 1,
'amount_out' => '<string>',
'bridge_recipient_setup' => true,
'bridge_target_chain' => '<string>',
'bridge_target_token_address' => '<string>',
'extra_fees' => 1,
'gasless' => true,
'referral_code' => '<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/bitcoin/evm"
payload := strings.NewReader("{\n \"btc_htlc_script_version\": 123,\n \"claiming_address\": \"<string>\",\n \"evm_chain_id\": 1,\n \"hash_lock\": \"<string>\",\n \"refund_pk\": \"<string>\",\n \"target_address\": \"<string>\",\n \"token_address\": \"<string>\",\n \"user_id\": \"<string>\",\n \"amount_in\": 1,\n \"amount_out\": \"<string>\",\n \"bridge_recipient_setup\": true,\n \"bridge_target_chain\": \"<string>\",\n \"bridge_target_token_address\": \"<string>\",\n \"extra_fees\": 1,\n \"gasless\": true,\n \"referral_code\": \"<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/bitcoin/evm")
.header("Content-Type", "application/json")
.body("{\n \"btc_htlc_script_version\": 123,\n \"claiming_address\": \"<string>\",\n \"evm_chain_id\": 1,\n \"hash_lock\": \"<string>\",\n \"refund_pk\": \"<string>\",\n \"target_address\": \"<string>\",\n \"token_address\": \"<string>\",\n \"user_id\": \"<string>\",\n \"amount_in\": 1,\n \"amount_out\": \"<string>\",\n \"bridge_recipient_setup\": true,\n \"bridge_target_chain\": \"<string>\",\n \"bridge_target_token_address\": \"<string>\",\n \"extra_fees\": 1,\n \"gasless\": true,\n \"referral_code\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.satora.io/swap/bitcoin/evm")
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 \"claiming_address\": \"<string>\",\n \"evm_chain_id\": 1,\n \"hash_lock\": \"<string>\",\n \"refund_pk\": \"<string>\",\n \"target_address\": \"<string>\",\n \"token_address\": \"<string>\",\n \"user_id\": \"<string>\",\n \"amount_in\": 1,\n \"amount_out\": \"<string>\",\n \"bridge_recipient_setup\": true,\n \"bridge_target_chain\": \"<string>\",\n \"bridge_target_token_address\": \"<string>\",\n \"extra_fees\": 1,\n \"gasless\": true,\n \"referral_code\": \"<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_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_hash_lock": "<string>",
"evm_htlc_address": "<string>",
"evm_htlc_kind": "erc20",
"evm_htlc_version": 123,
"evm_refund_locktime": 123,
"fee_sats": 123,
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"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_target_chain": "<string>",
"bridge_target_token_address": "<string>",
"btc_claim_txid": "<string>",
"btc_fund_txid": "<string>",
"evm_claim_txid": "<string>",
"evm_fund_txid": "<string>",
"target_evm_address": "<string>"
}{
"error": "<string>"
}{
"error": "<string>"
}{
"error": "<string>"
}Body
Chain-agnostic request for Bitcoin-to-EVM swaps.
The caller specifies the target chain via evm_chain_id and the token
via its ERC-20 contract token_address. This endpoint supports any token
reachable through 1inch aggregation (which covers Uniswap, Curve, etc.).
Bitcoin HTLC script version. New swaps must use version 1.
EVM address that will sign the HTLC claim (SDK-derived for gasless claims). Typed: garbage is rejected at deserialization, before an invoice/HTLC exists — a background funder can never be handed an unparseable claim address. Always an EVM address, even for Solana bridge targets (the claim happens on the hub chain).
Numeric EVM chain ID: 1 (Ethereum), 137 (Polygon), 42161 (Arbitrum).
x >= 0Hash lock provided by the client (32-byte hex string with 0x prefix).
Refund public key used to generate the Bitcoin HTLC.
Recipient where tokens land after the claim (user's final destination). An EVM hex address, or a base58 Solana pubkey for a Solana bridge target.
ERC-20 contract address of the desired token on the target chain.
User ID derived from wallet for recovery purposes.
How many sats the user wants to send (mutually exclusive with amount_out).
Value is in satoshis (smallest BTC unit).
x >= 0How much target token the user wants to receive (mutually exclusive with amount_in).
Value is in the target token's smallest unit (e.g. for USDC with 6 decimals, 1000000 = 1
USDC).
ATA-existence flag for non-EVM CCTP destinations (Solana).
true = recipient has no USDC ATA yet (Circle's forwarder
creates one, paid out of the burn). false (default) = recipient
already holds USDC, or destination is EVM. Drives the bridge-fee
variant used when inflating the DEX target and must match the
value later passed to redeem-and-swap-calldata so the burn
doesn't underflow. Solana callers are responsible for probing
the destination chain and setting this correctly.
Optional: CCTP bridge destination chain (e.g., "Ethereum", "Arbitrum"). When set, USDC will be bridged to this chain after the DEX swap.
Optional: USDC address on the bridge destination chain.
Optional per-swap fee surcharge in basis points (0..=max_extra_fee_bps configured on the matching developer key).
x >= 0Whether the server should execute the DEX swap on behalf of the user (gasless claim). When true, the gasless network fee is added to the total fee charged. Defaults to true.
Optional referral code for tracking.
Response
Swap created successfully
Response for Bitcoin-to-EVM swaps.
Hash lock (20-byte hex)
To be used for bitcoin htlc. This is the [ripemd160(evm_hash_lock)]
On-chain Bitcoin HTLC P2WSH address (user sends BTC here)
On-chain BTC refund locktime (unix timestamp)
The server's pk inside the htlc
EVM chain name (e.g. "Polygon", "Ethereum", "Arbitrum")
EVM address that will sign the HTLC claim (SDK-derived for gasless claims)
Timestamp of when the swap was created
Numeric EVM chain ID (e.g. 137, 1, 42161)
HTLCCoordinator contract address (for redeemAndExecute)
Amount of WBTC sats locked in the EVM HTLC
Hash lock (32-byte hex)
To be used for EVM htlc
EVM HTLC contract address
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)
Protocol fee in satoshis
Bitcoin network
Server's EVM address
Amount user must send in satoshis
Source token info
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 How much the user will receive in the target token's smallest unit
Target token info
Show child attributes
Show child attributes
WBTC token contract address on the target EVM chain (the token locked in the HTLC)
CCTP bridge destination chain. When set, USDC is bridged cross-chain after the swap.
USDC address on the bridge destination chain.
On-chain claim transaction ID (server claim)
On-chain funding transaction ID
EVM HTLC claim transaction ID (user claim)
EVM HTLC fund transaction ID
EVM address where tokens are swept after the claim (user's final destination)