curl --request POST \
--url https://api.satora.io/swap/evm/lightning \
--header 'Content-Type: application/json' \
--data '
{
"evm_chain_id": 1,
"token_address": "<string>",
"user_address": "<string>",
"user_id": "<string>",
"amount_in": "<string>",
"bridge_source_chain": "<string>",
"bridge_source_token_address": "<string>",
"extra_fees": 1,
"gasless": true,
"lightning_address": "<string>",
"lightning_invoice": "<string>",
"lnurl": "<string>",
"referral_code": "<string>",
"target_amount_sats": 123
}
'import requests
url = "https://api.satora.io/swap/evm/lightning"
payload = {
"evm_chain_id": 1,
"token_address": "<string>",
"user_address": "<string>",
"user_id": "<string>",
"amount_in": "<string>",
"bridge_source_chain": "<string>",
"bridge_source_token_address": "<string>",
"extra_fees": 1,
"gasless": True,
"lightning_address": "<string>",
"lightning_invoice": "<string>",
"lnurl": "<string>",
"referral_code": "<string>",
"target_amount_sats": 123
}
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({
evm_chain_id: 1,
token_address: '<string>',
user_address: '<string>',
user_id: '<string>',
amount_in: '<string>',
bridge_source_chain: '<string>',
bridge_source_token_address: '<string>',
extra_fees: 1,
gasless: true,
lightning_address: '<string>',
lightning_invoice: '<string>',
lnurl: '<string>',
referral_code: '<string>',
target_amount_sats: 123
})
};
fetch('https://api.satora.io/swap/evm/lightning', 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/lightning",
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([
'evm_chain_id' => 1,
'token_address' => '<string>',
'user_address' => '<string>',
'user_id' => '<string>',
'amount_in' => '<string>',
'bridge_source_chain' => '<string>',
'bridge_source_token_address' => '<string>',
'extra_fees' => 1,
'gasless' => true,
'lightning_address' => '<string>',
'lightning_invoice' => '<string>',
'lnurl' => '<string>',
'referral_code' => '<string>',
'target_amount_sats' => 123
]),
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/lightning"
payload := strings.NewReader("{\n \"evm_chain_id\": 1,\n \"token_address\": \"<string>\",\n \"user_address\": \"<string>\",\n \"user_id\": \"<string>\",\n \"amount_in\": \"<string>\",\n \"bridge_source_chain\": \"<string>\",\n \"bridge_source_token_address\": \"<string>\",\n \"extra_fees\": 1,\n \"gasless\": true,\n \"lightning_address\": \"<string>\",\n \"lightning_invoice\": \"<string>\",\n \"lnurl\": \"<string>\",\n \"referral_code\": \"<string>\",\n \"target_amount_sats\": 123\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/lightning")
.header("Content-Type", "application/json")
.body("{\n \"evm_chain_id\": 1,\n \"token_address\": \"<string>\",\n \"user_address\": \"<string>\",\n \"user_id\": \"<string>\",\n \"amount_in\": \"<string>\",\n \"bridge_source_chain\": \"<string>\",\n \"bridge_source_token_address\": \"<string>\",\n \"extra_fees\": 1,\n \"gasless\": true,\n \"lightning_address\": \"<string>\",\n \"lightning_invoice\": \"<string>\",\n \"lnurl\": \"<string>\",\n \"referral_code\": \"<string>\",\n \"target_amount_sats\": 123\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.satora.io/swap/evm/lightning")
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 \"evm_chain_id\": 1,\n \"token_address\": \"<string>\",\n \"user_address\": \"<string>\",\n \"user_id\": \"<string>\",\n \"amount_in\": \"<string>\",\n \"bridge_source_chain\": \"<string>\",\n \"bridge_source_token_address\": \"<string>\",\n \"extra_fees\": 1,\n \"gasless\": true,\n \"lightning_address\": \"<string>\",\n \"lightning_invoice\": \"<string>\",\n \"lnurl\": \"<string>\",\n \"referral_code\": \"<string>\",\n \"target_amount_sats\": 123\n}"
response = http.request(request)
puts response.read_body{
"chain": "<string>",
"client_evm_address": "<string>",
"client_lightning_invoice": "<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_htlc_address": "<string>",
"evm_htlc_kind": "erc20",
"evm_htlc_version": 123,
"evm_refund_locktime": 123,
"fee_sats": 123,
"gasless": true,
"hash_lock": "<string>",
"id": "<string>",
"invoice_expires_at": 123,
"lightning_payment_settled": true,
"network": "<string>",
"network_fee_sats": 123,
"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>",
"evm_claim_txid": "<string>",
"evm_fund_txid": "<string>"
}{
"error": "<string>"
}{
"error": "<string>"
}{
"error": "<string>"
}{
"error": "<string>"
}Create a chain-agnostic EVM-to-Lightning swap.
Flow:
- User funds the HTLCErc20 (via the coordinator; locked to the invoice’s payment hash)
- Once the funding is final, the server pays the invoice; the settled payment reveals the preimage
- Server claims the HTLC with the preimage
If the payment fails the swap becomes ServerWontFund and the user
reclaims their tokens via the collaborative EVM refund endpoint (or
unilaterally after the refund locktime).
curl --request POST \
--url https://api.satora.io/swap/evm/lightning \
--header 'Content-Type: application/json' \
--data '
{
"evm_chain_id": 1,
"token_address": "<string>",
"user_address": "<string>",
"user_id": "<string>",
"amount_in": "<string>",
"bridge_source_chain": "<string>",
"bridge_source_token_address": "<string>",
"extra_fees": 1,
"gasless": true,
"lightning_address": "<string>",
"lightning_invoice": "<string>",
"lnurl": "<string>",
"referral_code": "<string>",
"target_amount_sats": 123
}
'import requests
url = "https://api.satora.io/swap/evm/lightning"
payload = {
"evm_chain_id": 1,
"token_address": "<string>",
"user_address": "<string>",
"user_id": "<string>",
"amount_in": "<string>",
"bridge_source_chain": "<string>",
"bridge_source_token_address": "<string>",
"extra_fees": 1,
"gasless": True,
"lightning_address": "<string>",
"lightning_invoice": "<string>",
"lnurl": "<string>",
"referral_code": "<string>",
"target_amount_sats": 123
}
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({
evm_chain_id: 1,
token_address: '<string>',
user_address: '<string>',
user_id: '<string>',
amount_in: '<string>',
bridge_source_chain: '<string>',
bridge_source_token_address: '<string>',
extra_fees: 1,
gasless: true,
lightning_address: '<string>',
lightning_invoice: '<string>',
lnurl: '<string>',
referral_code: '<string>',
target_amount_sats: 123
})
};
fetch('https://api.satora.io/swap/evm/lightning', 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/lightning",
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([
'evm_chain_id' => 1,
'token_address' => '<string>',
'user_address' => '<string>',
'user_id' => '<string>',
'amount_in' => '<string>',
'bridge_source_chain' => '<string>',
'bridge_source_token_address' => '<string>',
'extra_fees' => 1,
'gasless' => true,
'lightning_address' => '<string>',
'lightning_invoice' => '<string>',
'lnurl' => '<string>',
'referral_code' => '<string>',
'target_amount_sats' => 123
]),
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/lightning"
payload := strings.NewReader("{\n \"evm_chain_id\": 1,\n \"token_address\": \"<string>\",\n \"user_address\": \"<string>\",\n \"user_id\": \"<string>\",\n \"amount_in\": \"<string>\",\n \"bridge_source_chain\": \"<string>\",\n \"bridge_source_token_address\": \"<string>\",\n \"extra_fees\": 1,\n \"gasless\": true,\n \"lightning_address\": \"<string>\",\n \"lightning_invoice\": \"<string>\",\n \"lnurl\": \"<string>\",\n \"referral_code\": \"<string>\",\n \"target_amount_sats\": 123\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/lightning")
.header("Content-Type", "application/json")
.body("{\n \"evm_chain_id\": 1,\n \"token_address\": \"<string>\",\n \"user_address\": \"<string>\",\n \"user_id\": \"<string>\",\n \"amount_in\": \"<string>\",\n \"bridge_source_chain\": \"<string>\",\n \"bridge_source_token_address\": \"<string>\",\n \"extra_fees\": 1,\n \"gasless\": true,\n \"lightning_address\": \"<string>\",\n \"lightning_invoice\": \"<string>\",\n \"lnurl\": \"<string>\",\n \"referral_code\": \"<string>\",\n \"target_amount_sats\": 123\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.satora.io/swap/evm/lightning")
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 \"evm_chain_id\": 1,\n \"token_address\": \"<string>\",\n \"user_address\": \"<string>\",\n \"user_id\": \"<string>\",\n \"amount_in\": \"<string>\",\n \"bridge_source_chain\": \"<string>\",\n \"bridge_source_token_address\": \"<string>\",\n \"extra_fees\": 1,\n \"gasless\": true,\n \"lightning_address\": \"<string>\",\n \"lightning_invoice\": \"<string>\",\n \"lnurl\": \"<string>\",\n \"referral_code\": \"<string>\",\n \"target_amount_sats\": 123\n}"
response = http.request(request)
puts response.read_body{
"chain": "<string>",
"client_evm_address": "<string>",
"client_lightning_invoice": "<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_htlc_address": "<string>",
"evm_htlc_kind": "erc20",
"evm_htlc_version": 123,
"evm_refund_locktime": 123,
"fee_sats": 123,
"gasless": true,
"hash_lock": "<string>",
"id": "<string>",
"invoice_expires_at": 123,
"lightning_payment_settled": true,
"network": "<string>",
"network_fee_sats": 123,
"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>",
"evm_claim_txid": "<string>",
"evm_fund_txid": "<string>"
}{
"error": "<string>"
}{
"error": "<string>"
}{
"error": "<string>"
}{
"error": "<string>"
}Body
Request to create an EVM-to-Lightning swap.
User sends any ERC-20 token on EVM, receives BTC on a Lightning invoice.
Numeric EVM chain ID: 1 (Ethereum), 137 (Polygon), 42161 (Arbitrum).
x >= 0ERC-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 (send-max: fees are deducted
from the Lightning payout). Only valid with
lightning_address/lnurl; mutually exclusive with
target_amount_sats.
Optional: 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.
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).
Lightning address (user@domain) to resolve into an invoice over
the payout amount. Mutually exclusive with the other destination
fields.
BOLT11 invoice the user wants paid. Must carry an amount (which
pins the payout — amount_in cannot be combined with it).
Mutually exclusive with lightning_address and lnurl.
LNURL-pay string to resolve into an invoice over the payout amount. Mutually exclusive with the other destination fields.
Optional referral code for fee exemption.
Amount the recipient should receive over Lightning in satoshis
(fees are added on top). Required with lightning_address/lnurl
unless amount_in is given; optional with lightning_invoice,
where it must match the invoice amount.
Response
Swap created successfully
EVM → Lightning swap response.
BOLT11 invoice the server will pay once the HTLC funding is final
The coordinator this swap is pinned to (the HTLC refundAddress).
WBTC/tBTC the coordinator locks in the HTLC, in token smallest units
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 >= 0HTLC family of the lock: erc20 (HTLCErc20 + HTLCCoordinator) or
native (HTLCNative + HTLCNativeCoordinator). Selects how the
client funds (Permit2 vs a payable lock) and refunds the swap.
erc20, native HTLC contract VERSION of the deployment this swap lives on — the EIP-712 domain version string for redeem/refund signatures.
Protocol fee in satoshis
Whether this swap was created with gasless relay (Permit2)
Hash lock (0x-prefixed 32-byte hex; the invoice's payment hash)
Unix timestamp after which the invoice can no longer be paid
Whether the invoice has settled. serverfunded only says the payment
was initiated; the preimage the recipient revealed is what proves it
landed, and the server claims the HTLC with it next.
Network fee in satoshis: the provider's quoted Lightning send fee
plus the server's HTLC claim gas. The actual send fee is paid out
of fee_sats + network_fee_sats, never on top.
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 Amount paid out on the user's Lightning invoice in satoshis
Show child attributes
Show child attributes
WBTC/tBTC token contract address on the EVM chain (the token locked in the HTLC).
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.
Native USDC address on the bridge source chain (base58 mint on Solana).