curl --request POST \
--url https://api.satora.io/swap/arkade/evm \
--header 'Content-Type: application/json' \
--data '
{
"arkade_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/arkade/evm"
payload = {
"arkade_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({
arkade_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/arkade/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/arkade/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([
'arkade_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/arkade/evm"
payload := strings.NewReader("{\n \"arkade_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/arkade/evm")
.header("Content-Type", "application/json")
.body("{\n \"arkade_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/arkade/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 \"arkade_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{
"arkade_htlc_script_version": 123,
"arkade_server_pk": "<string>",
"btc_vhtlc_address": "<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_htlc_address": "<string>",
"evm_htlc_kind": "erc20",
"evm_htlc_version": 123,
"evm_refund_locktime": 123,
"fee_sats": 123,
"hash_lock": "<string>",
"id": "<string>",
"network": "<string>",
"receiver_pk": "<string>",
"sender_pk": "<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"
},
"unilateral_claim_delay": 123,
"unilateral_refund_delay": 123,
"unilateral_refund_without_receiver_delay": 123,
"vhtlc_refund_locktime": 123,
"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 Arkade-to-EVM swap.
curl --request POST \
--url https://api.satora.io/swap/arkade/evm \
--header 'Content-Type: application/json' \
--data '
{
"arkade_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/arkade/evm"
payload = {
"arkade_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({
arkade_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/arkade/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/arkade/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([
'arkade_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/arkade/evm"
payload := strings.NewReader("{\n \"arkade_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/arkade/evm")
.header("Content-Type", "application/json")
.body("{\n \"arkade_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/arkade/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 \"arkade_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{
"arkade_htlc_script_version": 123,
"arkade_server_pk": "<string>",
"btc_vhtlc_address": "<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_htlc_address": "<string>",
"evm_htlc_kind": "erc20",
"evm_htlc_version": 123,
"evm_refund_locktime": 123,
"fee_sats": 123,
"hash_lock": "<string>",
"id": "<string>",
"network": "<string>",
"receiver_pk": "<string>",
"sender_pk": "<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"
},
"unilateral_claim_delay": 123,
"unilateral_refund_delay": 123,
"unilateral_refund_without_receiver_delay": 123,
"vhtlc_refund_locktime": 123,
"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 Arkade-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.).
Arkade 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 Arkade VHTLC.
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).
See BitcoinToEvmSwapRequest::bridge_recipient_setup.
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
Arkade → EVM swap response
EVM address that will sign the HTLC claim (SDK-derived for gasless 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.
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 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.
EVM address where tokens are swept after the claim (user's final destination).