curl --request POST \
--url https://api.satora.io/swap/{id}/claim-gasless \
--header 'Content-Type: application/json' \
--data '
{
"destination": "<string>",
"r": "<string>",
"s": "<string>",
"secret": "<string>",
"v": 1,
"bridge_recipient": "<string>",
"bridge_recipient_wallet": "<string>",
"dex_calldata": {
"data": "<string>",
"to": "<string>",
"value": "<string>"
}
}
'import requests
url = "https://api.satora.io/swap/{id}/claim-gasless"
payload = {
"destination": "<string>",
"r": "<string>",
"s": "<string>",
"secret": "<string>",
"v": 1,
"bridge_recipient": "<string>",
"bridge_recipient_wallet": "<string>",
"dex_calldata": {
"data": "<string>",
"to": "<string>",
"value": "<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({
destination: '<string>',
r: '<string>',
s: '<string>',
secret: '<string>',
v: 1,
bridge_recipient: '<string>',
bridge_recipient_wallet: '<string>',
dex_calldata: {data: '<string>', to: '<string>', value: '<string>'}
})
};
fetch('https://api.satora.io/swap/{id}/claim-gasless', 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/{id}/claim-gasless",
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([
'destination' => '<string>',
'r' => '<string>',
's' => '<string>',
'secret' => '<string>',
'v' => 1,
'bridge_recipient' => '<string>',
'bridge_recipient_wallet' => '<string>',
'dex_calldata' => [
'data' => '<string>',
'to' => '<string>',
'value' => '<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/{id}/claim-gasless"
payload := strings.NewReader("{\n \"destination\": \"<string>\",\n \"r\": \"<string>\",\n \"s\": \"<string>\",\n \"secret\": \"<string>\",\n \"v\": 1,\n \"bridge_recipient\": \"<string>\",\n \"bridge_recipient_wallet\": \"<string>\",\n \"dex_calldata\": {\n \"data\": \"<string>\",\n \"to\": \"<string>\",\n \"value\": \"<string>\"\n }\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/{id}/claim-gasless")
.header("Content-Type", "application/json")
.body("{\n \"destination\": \"<string>\",\n \"r\": \"<string>\",\n \"s\": \"<string>\",\n \"secret\": \"<string>\",\n \"v\": 1,\n \"bridge_recipient\": \"<string>\",\n \"bridge_recipient_wallet\": \"<string>\",\n \"dex_calldata\": {\n \"data\": \"<string>\",\n \"to\": \"<string>\",\n \"value\": \"<string>\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.satora.io/swap/{id}/claim-gasless")
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 \"destination\": \"<string>\",\n \"r\": \"<string>\",\n \"s\": \"<string>\",\n \"secret\": \"<string>\",\n \"v\": 1,\n \"bridge_recipient\": \"<string>\",\n \"bridge_recipient_wallet\": \"<string>\",\n \"dex_calldata\": {\n \"data\": \"<string>\",\n \"to\": \"<string>\",\n \"value\": \"<string>\"\n }\n}"
response = http.request(request)
puts response.read_body{
"id": "<string>",
"message": "<string>",
"status": "<string>",
"tx_hash": "<string>"
}{
"error": "<string>"
}{
"error": "<string>"
}Claims an EVM-targeted swap gaslessly via the HTLCCoordinator contract.
Supports Arkade-to-EVM, Lightning and Bitcoin-to-EVM swaps. The server submits
coordinator.redeemAndExecute on the client’s behalf, using the client’s
EIP-712 signature and secret.
Prerequisites — what the client must do before calling this endpoint
-
Create a swap via one of the swap endpoints (e.g.
POST /swap/arkade/evm). Wait for the swap status to reachServerFunded. -
Fetch calldata via
GET /swap/{id}/redeem-and-swap-calldata. This returns:dex_calldata— DEX swap calldata (only for non-WBTC targets)gasless_fee_sats— the fee deducted from the HTLC amountcalls_hash—keccak256(abi.encode(calls)), must be included in the signature
-
Construct the EIP-712 signature over the
Redeemstruct:Domain:
EIP712Domain(string name, string version, uint256 chainId, address verifyingContract) name = "HTLCErc20" version = "4" chainId = <target EVM chain ID> verifyingContract = <HTLCErc20 contract address (swap.evm_htlc_address)>Struct:
Redeem( bytes32 preimage, // the swap secret uint256 amount, // WBTC amount in the HTLC (in sats) address token, // WBTC token address address sender, // server's EVM address uint256 timelock, // HTLC refund locktime address caller, // HTLCCoordinator contract address address destination, // where output tokens go address sweepToken, // target token (or WBTC if direct) uint256 minAmountOut, // slippage protection from step 2 bytes32 callsHash // from step 2 )The final digest is:
keccak256(\x19\x01 || domainSeparator || structHash)Sign this digest with the client’s EVM private key (secp256k1) to obtain
v,r,s.JavaScript example (using ethers v6):
import { ethers } from "ethers"; // 1. Fetch calldata (returns calls_hash + optional dex_calldata) const { calls_hash, dex_calldata, gasless_fee_sats, min_amount_out } = await fetch( `${baseUrl}/swap/${swapId}/redeem-and-swap-calldata?destination=${destination}` ).then(r => r.json()); // 2. Determine sweepToken const needsDexSwap = targetTokenAddress.toLowerCase() !== wbtcAddress.toLowerCase(); const sweepToken = needsDexSwap ? targetTokenAddress : wbtcAddress; // 3. Build EIP-712 typed data and sign const wallet = new ethers.Wallet(privateKey); const domain = { name: "HTLCErc20", version: "4", chainId: swap.evm_chain_id, // e.g. 42161 for Arbitrum verifyingContract: swap.evm_htlc_address, // HTLCErc20 contract }; const types = { Redeem: [ { name: "preimage", type: "bytes32" }, { name: "amount", type: "uint256" }, { name: "token", type: "address" }, { name: "sender", type: "address" }, { name: "timelock", type: "uint256" }, { name: "caller", type: "address" }, { name: "destination", type: "address" }, { name: "sweepToken", type: "address" }, { name: "minAmountOut", type: "uint256" }, { name: "callsHash", type: "bytes32" }, ], }; const message = { preimage: secret, // 0x-prefixed 32-byte hex amount: swap.evm_expected_sats, // WBTC sats locked in HTLC token: wbtcAddress, // WBTC on the target chain sender: swap.server_evm_address, // server (HTLC creator) timelock: swap.evm_refund_locktime, // HTLC expiry timestamp caller: swap.evm_coordinator_address, // HTLCCoordinator contract destination: destination, // where output tokens go sweepToken: sweepToken, minAmountOut: min_amount_out, // from step 1 callsHash: calls_hash, // from step 1 }; const sig = await wallet.signTypedData(domain, types, message); const { v, r, s } = ethers.Signature.from(sig); // 4. POST to claim-gasless await fetch(`${baseUrl}/swap/${swapId}/claim-gasless`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ secret, destination, v, r, s, dex_calldata: needsDexSwap ? dex_calldata : undefined, }), }); -
POST the request to this endpoint with the secret, destination, signature, and optionally the
dex_calldatafrom step 2.
Idempotency
Repeating the request is safe. A swap whose claim has already been relayed
or observed (client_redeeming, client_redeemed, server_redeemed)
answers 200 with that status and the claim transaction hash, and a
request that arrives while the relay is being broadcast answers 200 with
client_redeeming and no hash yet. Nothing is relayed twice.
On a native-coin target (Rootstock) the response is sent as soon as the
node has accepted the claim transaction: the swap is client_redeeming
with the hash recorded, and the server keeps following the transaction
until it is mined. A relay that reverts or is dropped by the node returns
the swap to server_funded, so a later request claims again.
curl --request POST \
--url https://api.satora.io/swap/{id}/claim-gasless \
--header 'Content-Type: application/json' \
--data '
{
"destination": "<string>",
"r": "<string>",
"s": "<string>",
"secret": "<string>",
"v": 1,
"bridge_recipient": "<string>",
"bridge_recipient_wallet": "<string>",
"dex_calldata": {
"data": "<string>",
"to": "<string>",
"value": "<string>"
}
}
'import requests
url = "https://api.satora.io/swap/{id}/claim-gasless"
payload = {
"destination": "<string>",
"r": "<string>",
"s": "<string>",
"secret": "<string>",
"v": 1,
"bridge_recipient": "<string>",
"bridge_recipient_wallet": "<string>",
"dex_calldata": {
"data": "<string>",
"to": "<string>",
"value": "<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({
destination: '<string>',
r: '<string>',
s: '<string>',
secret: '<string>',
v: 1,
bridge_recipient: '<string>',
bridge_recipient_wallet: '<string>',
dex_calldata: {data: '<string>', to: '<string>', value: '<string>'}
})
};
fetch('https://api.satora.io/swap/{id}/claim-gasless', 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/{id}/claim-gasless",
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([
'destination' => '<string>',
'r' => '<string>',
's' => '<string>',
'secret' => '<string>',
'v' => 1,
'bridge_recipient' => '<string>',
'bridge_recipient_wallet' => '<string>',
'dex_calldata' => [
'data' => '<string>',
'to' => '<string>',
'value' => '<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/{id}/claim-gasless"
payload := strings.NewReader("{\n \"destination\": \"<string>\",\n \"r\": \"<string>\",\n \"s\": \"<string>\",\n \"secret\": \"<string>\",\n \"v\": 1,\n \"bridge_recipient\": \"<string>\",\n \"bridge_recipient_wallet\": \"<string>\",\n \"dex_calldata\": {\n \"data\": \"<string>\",\n \"to\": \"<string>\",\n \"value\": \"<string>\"\n }\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/{id}/claim-gasless")
.header("Content-Type", "application/json")
.body("{\n \"destination\": \"<string>\",\n \"r\": \"<string>\",\n \"s\": \"<string>\",\n \"secret\": \"<string>\",\n \"v\": 1,\n \"bridge_recipient\": \"<string>\",\n \"bridge_recipient_wallet\": \"<string>\",\n \"dex_calldata\": {\n \"data\": \"<string>\",\n \"to\": \"<string>\",\n \"value\": \"<string>\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.satora.io/swap/{id}/claim-gasless")
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 \"destination\": \"<string>\",\n \"r\": \"<string>\",\n \"s\": \"<string>\",\n \"secret\": \"<string>\",\n \"v\": 1,\n \"bridge_recipient\": \"<string>\",\n \"bridge_recipient_wallet\": \"<string>\",\n \"dex_calldata\": {\n \"data\": \"<string>\",\n \"to\": \"<string>\",\n \"value\": \"<string>\"\n }\n}"
response = http.request(request)
puts response.read_body{
"id": "<string>",
"message": "<string>",
"status": "<string>",
"tx_hash": "<string>"
}{
"error": "<string>"
}{
"error": "<string>"
}Path Parameters
Swap ID
Body
Request body for POST /swap/{id}/claim-gasless.
The client provides the HTLC preimage (secret) and an EIP-712 signature
authorising the server to call redeemAndExecute on the coordinator
contract on the client's behalf.
Examples
Minimal request (WBTC-direct target, no DEX swap):
let request = serde_json::json!({
"secret": "0x0101010101010101010101010101010101010101010101010101010101010101",
"destination": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"v": 27,
"r": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"s": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
});
Request with DEX calldata (non-WBTC target, e.g. USDC):
let request = serde_json::json!({
"secret": "0x0101010101010101010101010101010101010101010101010101010101010101",
"destination": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"v": 28,
"r": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"s": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"dex_calldata": {
"to": "0x1111111254EEB25477B68fb85Ed929f73A960582",
"data": "0xabcdef0123456789",
"value": "0"
}
});
EVM address where tokens should be sent
EIP-712 signature r (32-byte hex string, with or without 0x prefix)
EIP-712 signature s (32-byte hex string, with or without 0x prefix)
The secret/preimage (32-byte hex string, with or without 0x prefix)
EIP-712 signature v
x >= 0Optional non-EVM bridge recipient (e.g. a Solana base58 SPL pubkey).
Used as CCTP mintRecipient when bridge_target_chain is non-EVM;
must match the value passed to redeem-and-swap-calldata so the
rebuilt calls_hash matches the EIP-712 signature.
For Solana: this is the recipient's USDC ATA, not their wallet.
Optional Solana wallet pubkey, supplied alongside bridge_recipient
when the recipient's USDC ATA does not yet exist. Triggers the
extended 65-byte forwarding hookData. Must mirror what was passed to
redeem-and-swap-calldata so the rebuilt calls_hash matches.
Optional DEX calldata from the client. If provided, the server uses this
instead of fetching from 1inch. Client should fetch via
GET /swap/{id}/redeem-and-swap-calldata?destination=...
Show child attributes
Show child attributes
Response
Claim relayed, or already relayed / observed (idempotent): the status and the claim transaction hash
Swap ID
Success message
Current swap status
Hash of the claim transaction. Empty while the relay is still being broadcast (a concurrent request beat this one by seconds) or when the swap was claimed by a transaction the server did not relay.