curl --request POST \
--url https://api.satora.io/swap/lightning/evm \
--header 'Content-Type: application/json' \
--data '
{
"claiming_address": "<string>",
"evm_chain_id": 1,
"hash_lock": "<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,
"invoice_description": "<string>",
"referral_code": "<string>"
}
'import requests
url = "https://api.satora.io/swap/lightning/evm"
payload = {
"claiming_address": "<string>",
"evm_chain_id": 1,
"hash_lock": "<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,
"invoice_description": "<string>",
"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({
claiming_address: '<string>',
evm_chain_id: 1,
hash_lock: '<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,
invoice_description: '<string>',
referral_code: '<string>'
})
};
fetch('https://api.satora.io/swap/lightning/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/lightning/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([
'claiming_address' => '<string>',
'evm_chain_id' => 1,
'hash_lock' => '<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,
'invoice_description' => '<string>',
'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/lightning/evm"
payload := strings.NewReader("{\n \"claiming_address\": \"<string>\",\n \"evm_chain_id\": 1,\n \"hash_lock\": \"<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 \"invoice_description\": \"<string>\",\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/lightning/evm")
.header("Content-Type", "application/json")
.body("{\n \"claiming_address\": \"<string>\",\n \"evm_chain_id\": 1,\n \"hash_lock\": \"<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 \"invoice_description\": \"<string>\",\n \"referral_code\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.satora.io/swap/lightning/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 \"claiming_address\": \"<string>\",\n \"evm_chain_id\": 1,\n \"hash_lock\": \"<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 \"invoice_description\": \"<string>\",\n \"referral_code\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"bolt11_invoice": "<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>",
"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>",
"evm_claim_txid": "<string>",
"evm_fund_txid": "<string>",
"invoice_expires_at": 123,
"target_evm_address": "<string>"
}{
"error": "<string>"
}{
"error": "<string>"
}{
"error": "<string>"
}{
"error": "<string>"
}Create a chain-agnostic Lightning-to-EVM swap.
Flow:
- User pays the returned hold invoice; the payment is held.
- Server funds an HTLCErc20 locked to the same hash.
- User claims the HTLC (gasless), revealing the preimage.
- Server settles the held payment with the revealed preimage.
If the server never funds (or the user never claims) the held payment is automatically returned to the payer when the hold expires.
curl --request POST \
--url https://api.satora.io/swap/lightning/evm \
--header 'Content-Type: application/json' \
--data '
{
"claiming_address": "<string>",
"evm_chain_id": 1,
"hash_lock": "<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,
"invoice_description": "<string>",
"referral_code": "<string>"
}
'import requests
url = "https://api.satora.io/swap/lightning/evm"
payload = {
"claiming_address": "<string>",
"evm_chain_id": 1,
"hash_lock": "<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,
"invoice_description": "<string>",
"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({
claiming_address: '<string>',
evm_chain_id: 1,
hash_lock: '<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,
invoice_description: '<string>',
referral_code: '<string>'
})
};
fetch('https://api.satora.io/swap/lightning/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/lightning/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([
'claiming_address' => '<string>',
'evm_chain_id' => 1,
'hash_lock' => '<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,
'invoice_description' => '<string>',
'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/lightning/evm"
payload := strings.NewReader("{\n \"claiming_address\": \"<string>\",\n \"evm_chain_id\": 1,\n \"hash_lock\": \"<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 \"invoice_description\": \"<string>\",\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/lightning/evm")
.header("Content-Type", "application/json")
.body("{\n \"claiming_address\": \"<string>\",\n \"evm_chain_id\": 1,\n \"hash_lock\": \"<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 \"invoice_description\": \"<string>\",\n \"referral_code\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.satora.io/swap/lightning/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 \"claiming_address\": \"<string>\",\n \"evm_chain_id\": 1,\n \"hash_lock\": \"<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 \"invoice_description\": \"<string>\",\n \"referral_code\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"bolt11_invoice": "<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>",
"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>",
"evm_claim_txid": "<string>",
"evm_fund_txid": "<string>",
"invoice_expires_at": 123,
"target_evm_address": "<string>"
}{
"error": "<string>"
}{
"error": "<string>"
}{
"error": "<string>"
}{
"error": "<string>"
}Body
Chain-agnostic request for Lightning-to-EVM swaps.
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): sha256(secret). The client keeps the secret — it is the
invoice's payment hash and the EVM HTLC's lock.
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 over Lightning (mutually
exclusive with amount_out).
x >= 0How much target token the user wants to receive (mutually exclusive
with amount_in), in the target token's smallest unit.
ATA-existence flag for non-EVM CCTP destinations (Solana). See the Bitcoin→EVM request for semantics.
Optional: bridge destination chain (e.g., "Base", "Optimism").
When set, the DEX output is bridged there after the claim — CCTP
for USDC, LayerZero OFT for USDT0, chosen from token_address.
Optional: token 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 >= 0true to use gasless funding via sdk derived wallet (Permit2 relay).
Optional description shown in the payer's wallet when they open the hold invoice.
Optional referral code for tracking.
Response
Swap created successfully
Lightning → EVM swap response (Spark era).
BOLT11 hold invoice the user pays. The payment is held until the user's EVM claim reveals the preimage.
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 locked in the EVM HTLC, in token smallest units (WBTC sats, or 18-decimal units for tBTC — hence U256, not i64)
EVM HTLC contract address
HTLC family of the lock: erc20 (HTLCErc20 + HTLCCoordinator) or
native (HTLCNative + HTLCNativeCoordinator). Selects the EIP-712
domain name and the claim call a client uses.
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 + network fee in satoshis
Hash lock (32-byte hex, 0x-prefixed) — the hold invoice's payment hash and the EVM HTLC's lock. The client holds the preimage.
Bitcoin network
Server's EVM address
Amount the user pays over Lightning, 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.
EVM HTLC claim transaction ID (user claim)
EVM HTLC fund transaction ID
Unix timestamp after which the invoice can no longer be paid
EVM address where tokens are swept after the claim (user's final destination)