# Creating & Executing a Swap

This page covers the core swap lifecycle that every direction follows: **create → poll → sign → submit → repeat** until finished. Bitcoin- and Lightning-specific details (PSBT building, LNURL, preimage reveal) are split into [Bitcoin & Lightning Specifics](https://docs.atomiq.exchange/rest-api-guide/bitcoin-and-lightning.md).

tip

This page covers the minimum needed to create and execute a swap. If you're building a complete swap form (e.g. token pickers, "Max" button, amount limit validation, recipient address parsing), see [Quoting Swaps](https://docs.atomiq.exchange/rest-api-guide/quoting.md) first to populate the form inputs.

## Create the swap

Use [`POST /createSwap`](https://docs.atomiq.exchange/rest-api-reference/create-swap) to create a new swap:

```
curl -X POST "https://mainnet.swaps-api.atomiq.exchange/createSwap" \

  -H "Content-Type: application/json" \

  -d '{

    "srcToken":   "BITCOIN-BTC",

    "dstToken":   "STARKNET-STRK",

    "amount":     "150000",

    "amountType": "EXACT_IN",

    "srcAddress": "bc1q…",

    "dstAddress": "0x0123…"

  }'
```

For **Bitcoin / Lightning → Smart chain** swaps, you can also request a destination-chain gas drop by passing `gasAmount`; see [Gas-drop](#gas-drop).

This creates a swap of 0.0015 BTC (150,000 sats) to STRK token. Note that `amount` is in base units (sats for Bitcoin, wei for EVM, etc.), and the amount type can be either `EXACT_IN` or `EXACT_OUT`, based on whether the `amount` field specifies an input amount or output amount. For some swap types (e.g. Lightning → Smart chain and Bitcoin → Smart chain) the `srcAddress` can be left empty.

Lightning network swaps

For **Lightning → Smart chain** swaps, you also need to create a swap secret and paymentHash pair.

First generate a cryptographically secure random 32-byte buffer, this is your swap secret, then hash this secret using sha256 hash function to derive a payment hash. Store the swap secret securely for later reveal (i.e. in the browser local storage).

```
const secret: Buffer = crypto.randomBytes(32);

const paymentHash: string = crypto.createHash("sha256").update(secret).digest("hex");

// Store the preimage, i.e. in browser storage
```

Then pass the hex-encoded payment hash to the [`POST /createSwap`](https://docs.atomiq.exchange/rest-api-reference/create-swap) API as a `paymentHash` parameter.

The response is a **swap record** — the same shape every subsequent [`GET /getSwapStatus`](https://docs.atomiq.exchange/rest-api-reference/get-swap-status) call will return (see the REST API Reference for the full response schema):

```
{

  "swapId":   "54cbe34e2ee1bf...",

  "swapType": "SPV_VAULT_FROM_BTC",

  "state":    { "number": 1, "name": "CREATED", "description": "..." },

  "quote":    {

    "inputAmount":  { "amount": "0.0015",      "rawAmount": "150000", ... },

    "outputAmount": { "amount": "4.21",        "rawAmount": "4210000000000000000", ... },

    "fees":         { "swap": { ... }, "networkOutput": { ... } },

    "expiry":       1713360000000,

    "outputAddress": "0x0123..."

  },

  "createdAt": 1713359700000,

  "steps":    [ ... ]

}
```

### Gas-drop

For **Bitcoin / Lightning → Smart chain** swaps you can optionally request a small portion of the swap amount to be swapped into destination chain's native gas token (e.g. STRK on Starknet, CBTC on Citrea, ...). This is useful for providing new wallets with some native token balance to cover transaction fees, such that they can start interacting with other dApps on the network immediately.

To request a gas drop set the `gasAmount` parameter (base-unit string, same convention as `amount`) when creating the swap with [`POST /createSwap`](https://docs.atomiq.exchange/rest-api-reference/create-swap), the gas drop is usually not available when already swapping to the native token of the chain (it is anyway only useful when you swap to a non-native token):

```
curl -X POST "https://mainnet.swaps-api.atomiq.exchange/createSwap" \

  -H "Content-Type: application/json" \

  -d '{

    "srcToken":   "BITCOIN-BTC",

    "dstToken":   "STARKNET-WBTC",

    "amount":     "150000",

    "amountType": "EXACT_IN",

    "srcAddress": "bc1q…",

    "dstAddress": "0x0123…",

    "gasAmount": "10000000000000000000"

  }'
```

This creates a swap of 150,000 sats (0.0015 BTC) from Bitcoin into WBTC and also adds a 10 STRK gas drop on Starknet. The 10 STRK gas drop value is subtracted from the output WBTC amount (as part of the swap value was converted into the STRK).

info

Requesting gas-drop also affects spendable-balance math; see [Quoting Swaps → Spendable balance](https://docs.atomiq.exchange/rest-api-guide/quoting.md#estimate-spendable-balance).

## Poll for the current action

tip

The full reference implementation of the polling loop, handling all the actions in TypeScript can be found at [`scripts/process-swap.ts`](https://github.com/atomiqlabs/atomiq-api-docker/blob/main/scripts/process-swap.ts) in the `atomiq-api-docker` repo.

The swap process might include multiple actions that must be executed sequentially, you should therefore call [`GET /getSwapStatus`](https://docs.atomiq.exchange/rest-api-reference/get-swap-status) in a polling loop, which returns the current action that the user should execute.

```
curl "https://mainnet.swaps-api.atomiq.exchange/getSwapStatus?swapId=<id>"
```

Bitcoin swaps

For **Bitcoin → Smart chain** swaps, pass `bitcoinAddress` + `bitcoinPublicKey` on **every** call as the API needs them to build funded PSBTs.

```
curl "https://mainnet.swaps-api.atomiq.exchange/getSwapStatus?swapId=<id>&bitcoinAddress=<addr>&bitcoinPublicKey=<hex>"
```

The response extends the swap record with four boolean flags and a `currentAction`, which indicates the action, that has to be executed by the user.

```
{

  /* ... all swap-record fields ... */

  "isFinished":       false,

  "isSuccess":        false,

  "isFailed":         false,

  "isExpired":        false,

  "currentAction":    { "type": "SignPSBT", /* ... */ },

  "requiresSecretReveal": false

}
```

When `isFinished === true`, stop polling. See [Terminal states](#terminal-states) below for what the other three flags mean.

Lightning secret reveal

For **Lightning → Smart chain** swaps, the `requiresSecretReveal` parameter indicates when the swap secret should be revealed. To reveal the swap secret, include the `secret` parameter in the subsequent [`GET /getSwapStatus`](https://docs.atomiq.exchange/rest-api-reference/get-swap-status) calls:

```
curl "https://mainnet.swaps-api.atomiq.exchange/getSwapStatus?swapId=<id>&secret=<hex-encoded swap secret>"
```

The returned `currentAction`, uses the [`SerializedAction`](https://docs.atomiq.exchange/rest-api-reference/schemas/serializedaction) schema, and is one of the following four shapes, which correspond to types that the client should branch on:

### `SignPSBT`

Reference: [SerializedActionSignPSBT](https://docs.atomiq.exchange/rest-api-reference/schemas/serializedactionsignpsbt)

Here, the API has built a Bitcoin PSBT for the user's deposit and needs the client's wallet to sign the PSBT, then return the signed PSBT back to the API.

```
{

  "type": "SignPSBT",

  "chain": "BITCOIN",

  "name": "Deposit on Bitcoin",

  "description": "Sign the funding transaction to commit the deposit.",

  "pollTimeSeconds": 5,

  "txs": [

    {

      "type": "FUNDED_PSBT",

      "psbtHex":    "70736274ff0100…ac0000000000",

      "psbtBase64": "cHNidP8BAH0CAAA…rAAAAAAA=",

      "signInputs": [0, 2],

      "feeRate": 3.5

    }

  ]

}
```

For each PSBT in `action.txs`, sign the inputs listed in `signInputs` with the user's Bitcoin wallet and post the signed PSBTs back to [`POST /submitTransaction`](https://docs.atomiq.exchange/rest-api-reference/submit-transaction), for the API to broadcast the transaction. I.e. in this example the client should parse the PSBT from its `psbtHex` or `psbtBase64`, sign inputs with indexes 0 and 2.

info

In case you didn't supply `bitcoinAddress` + `bitcoinPublicKey` on `GET /getSwapStatus` the endpoint, returns the `RAW_PSBT` objects in `action.txs` instead of `FUNDED_PSBT`, and the client must add its own inputs (coin-selection) before signing — see [Bitcoin & Lightning → PSBT signing](https://docs.atomiq.exchange/rest-api-guide/bitcoin-and-lightning.md#bitcoin-psbts).

Submit each signed PSBT as a **hex or base64 encoded** string. Order of the PSBTs in `signedTxs` must match the order in `action.txs`:

```
curl -X POST "https://mainnet.swaps-api.atomiq.exchange/submitTransaction" \

  -H "Content-Type: application/json" \

  -d '{

    "swapId": "0x9f3c…",

    "signedTxs": ["70736274ff01..."]

  }'
```

The response contains the broadcasted Bitcoin transaction IDs:

```
{ "txHashes": ["a1b2c3d4e5f6…"] }
```

### `SignSmartChainTransaction`

Reference: [SerializedActionSignSmartChainTransaction](https://docs.atomiq.exchange/rest-api-reference/schemas/serializedactionsignsmartchaintransaction)

Here, the API has pre-built smart chain (i.e. non-Bitcoin: EVM, Solana, Starknet, etc.) transactions and needs the user's smart chain wallet to sign them.

```
{

  "type": "SignSmartChainTransaction",

  "chain": "SOLANA",

  "name": "Commit on Solana",

  "description": "Sign and submit the escrow-funding transaction.",

  "pollTimeSeconds": 3,

  "txs": [

    "{\"tx\":\"01a4f1b2...\",\"signers\":[\"7c983c019e...\"],\"lastValidBlockheight\":345678901}"

  ]

}
```

Each entry of `action.txs` is a serialized unsigned transaction whose shape depends on `action.chain`:

#### Solana

Uses a **JSON-stringified envelope**, with the shape `{ tx, signers, lastValidBlockheight }` where `tx` is a hex-encoded legacy `Transaction`, `signers` is an array of hex-encoded secret keys for any ephemeral co-signers that also need to sign the transaction, and `lastValidBlockheight` is the transaction expiration block height. Use the `partialSign()`, as the LP may already have co-signed the transaction (see the code example below).

#### Starknet

Uses a **JSON-stringified envelope**, with the shape `{ type, tx, details, ... }`. `type` is either `INVOKE` (regular call) or `DEPLOY_ACCOUNT` (first-time account deployment). For `INVOKE`, build the signed invocation from `tx` + `details`. For `DEPLOY_ACCOUNT`, you need to build the deployment payload yourself (the API can't know your account class) using the supplied `details` (fees, nonce). Convert every `resourceBounds.*.*` value to `BigInt` before signing. Populate `parsed.signed` and re-stringify (use a BigInt → string replacer).

#### EVM (Citrea / Alpen / Goat)

Uses a hex-serialized unsigned EVM transaction, with pre-populated nonce and fees, ready to be signed by the EVM wallet.

***

Code example handling the smart chain signing action:

```
import {Transaction as SolanaTransaction} from "@solana/web3.js";

import {Transaction as EthersTransaction} from "ethers";



const signedTxs: string[] = [];

for (const tx of action.txs) {

  switch (action.chain) {

    case "SOLANA": {

      const parsed = JSON.parse(tx);

      const extraSigners = parsed.signers.map((k: string) =>

        Keypair.fromSecretKey(Buffer.from(k, "hex")),

      );

      const solTx = SolanaTransaction.from(Buffer.from(parsed.tx, "hex"));

      solTx.lastValidBlockHeight = parsed.lastValidBlockheight;

      solTx.partialSign(solanaWallet, ...extraSigners);   // preserves LP sig

      signedTxs.push(solTx.serialize().toString("hex"));

      break;

    }

    case "STARKNET": {

      const parsed = JSON.parse(tx);

      // Coerce resourceBounds back to BigInt

      for (const t in parsed.details.resourceBounds)

        for (const p in parsed.details.resourceBounds[t])

          parsed.details.resourceBounds[t][p] = BigInt(parsed.details.resourceBounds[t][p]);



      if (parsed.type === "INVOKE") {

        parsed.signed = await starknetWallet.buildInvocation(parsed.tx, parsed.details);

      } else if (parsed.type === "DEPLOY_ACCOUNT") {

        // API can't know your account class — build the deployment payload yourself.

        parsed.signed = await starknetWallet.buildAccountDeployPayload(

          starknetWalletDeploymentPayload,

          parsed.details,

        );

      }

      signedTxs.push(JSON.stringify(parsed, (_, v) => typeof v === "bigint" ? v.toString() : v));

      break;

    }

    default: { // EVM chains

      const parsedTx = EthersTransaction.from(tx);

      signedTxs.push(await evmWallet.signTransaction(parsedTx));

      break;

    }

  }

}
```

After signing, submit the signed payloads. Encoding per chain: hex-encoded signed transaction for Solana and EVM; JSON-stringified envelope (with `signed` populated) for Starknet.

```
curl -X POST "https://mainnet.swaps-api.atomiq.exchange/submitTransaction" \

  -H "Content-Type: application/json" \

  -d '{

    "swapId": "9f3c...",

    "signedTxs": ["21b12eea..."]

  }'
```

info

Smart chain transaction submission endpoint usually waits for the smart chain transaction to confirm before responding, ensure you call the [`POST /submitTransaction`](https://docs.atomiq.exchange/rest-api-reference/submit-transaction) with a long enough timeout, to accommodate transaction confirmation delays.

The response contains transaction IDs of the submitted and broadcasted transactions:

```
{ "txHashes": ["3KZv8Fq..."] }
```

### `SendToAddress`

Reference: [SerializedActionSendToAddress](https://docs.atomiq.exchange/rest-api-reference/schemas/serializedactionsendtoaddress)

Here, the user has to pay to either an on-chain Bitcoin address or a BOLT11 Lightning invoice.

```
{

  "type": "SendToAddress",

  "name": "Pay Lightning invoice",

  "description": "Pay the BOLT11 invoice from any Lightning wallet.",

  "pollTimeSeconds": 5,

  "expectedTimeSeconds": 60,

  "txs": [

    {

      "name":    "Lightning payment",

      "address": "lnbc1500n1p3z7...",

      "hyperlink": "lightning:lnbc1500n1p3z7...",

      "amount":  { "amount": "0.00015", "symbol": "BTC" }

    }

  ]

}
```

Show the `address` and `amount` from each entry of `action.txs` to the user (or use a `hyperlink`, which can be displayed as a QR code, or used as clickable deeplink) and let them pay from whichever wallet they like. Keep polling [`GET /getSwapStatus`](https://docs.atomiq.exchange/rest-api-reference/get-swap-status); the server detects the incoming payment and advances the state automatically.

info

A Lightning → Smart chain swap can also be settled via LNURL-withdraw instead of asking the user to pay an invoice — use the [LNURL-withdraw settlement](https://docs.atomiq.exchange/rest-api-guide/bitcoin-and-lightning.md#settle-the-swap-with-lnurl-withdraw-link) instead.

### `Wait`

Reference: [SerializedActionWait](https://docs.atomiq.exchange/rest-api-reference/schemas/serializedactionwait)

Here, there's nothing actionable right now — the API is waiting for confirmations, a counterparty action, or a timer to elapse.

```
{

  "type": "Wait",

  "name": "Waiting for confirmations",

  "description": "1/3 confirmations seen. ETA ~30 min.",

  "expectedTimeSeconds": 1800,

  "pollTimeSeconds": 30

}
```

Wait `pollTimeSeconds` and call [`GET /getSwapStatus`](https://docs.atomiq.exchange/rest-api-reference/get-swap-status) again. Use `expectedTimeSeconds` to render an ETA in the UI.

## Terminal states

A swap is **finished** when `isFinished === true`. The terminal state is described by three additional flags:

| Flag        | Meaning                                                                          |
| ----------- | -------------------------------------------------------------------------------- |
| `isSuccess` | The swap completed successfully and the user received the destination token.     |
| `isFailed`  | The swap failed after funds were committed — typically results in a refund flow. |
| `isExpired` | The quote expired before the user paid. No on-chain state was created.           |

Once `isFinished` is true, stop polling. These flags are mutually exclusive.

## Lifecycle overview

![REST API swap lifecycle — create, poll, sign, submit until finished](/assets/images/swap-lifecycle-3b0df6a4c3f3935fc1ffb425a86fcc31.svg)

## Swap execution steps

Every swap record carries a `steps` array alongside the action-level state, which is a **UX hint** describing the swap as a linear sequence of stages — good for rendering the swap progress. The *actionable* state still lives in `currentAction`. Individual steps in the array use the [`SwapExecutionStep`](https://docs.atomiq.exchange/rest-api-reference/schemas/swapexecutionstep) schema, which always contains a `status` field, plus additional fields based on the type:

| `type`                                                                                              | Meaning                                                                 | Statuses                                                                                                                | Additional fields                                     |
| --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| [`Setup`](https://docs.atomiq.exchange/rest-api-reference/schemas/swapexecutionstepsetup)           | Destination-side setup (e.g. creating the destination HTLC / escrow).   | `awaiting`, `completed`, `soft_expired`, `expired`                                                                      | `setupTxId`                                           |
| [`Payment`](https://docs.atomiq.exchange/rest-api-reference/schemas/swapexecutionsteppayment)       | The user's payment that initiates or funds the swap on the source side. | `inactive`, `awaiting`, `received`, `confirmed`, `soft_expired`, `expired`                                              | `initTxId`, `settleTxId`, `confirmations?` (BTC-only) |
| [`Settlement`](https://docs.atomiq.exchange/rest-api-reference/schemas/swapexecutionstepsettlement) | Payout / settlement on the destination side.                            | `inactive`, `waiting_lp`, `awaiting_automatic`, `awaiting_manual`, `soft_settled`, `soft_expired`, `settled`, `expired` | `initTxId`, `settleTxId`                              |
| [`Refund`](https://docs.atomiq.exchange/rest-api-reference/schemas/swapexecutionsteprefund)         | Source-side refund path after a failed swap.                            | `inactive`, `awaiting`, `refunded`                                                                                      | `refundTxId`                                          |

```
// Example step — Bitcoin payment being confirmed

{

  "type":        "Payment",

  "side":        "source",

  "chain":       "BITCOIN",

  "title":       "Bitcoin deposit",

  "description": "Waiting for 3 block confirmations.",

  "status":      "received",

  "confirmations": { "current": 1, "target": 3, "etaSeconds": 1200 },

  "initTxId":    "a1b2…"

}
```

## Next Steps

### Bitcoin & Lightning

Handling Bitcoin-specific bits (PSBT inputs, fee rates), Lightning invoices, and LNURL settlement.

**[Bitcoin & Lightning Specifics →](https://docs.atomiq.exchange/rest-api-guide/bitcoin-and-lightning.md)**

***

### Listing Swaps

Listing swap history, pending swaps that need user attention, resuming mid-flight swaps after restart, and handling refunds.

**[Listing Swaps →](https://docs.atomiq.exchange/rest-api-guide/listing-swaps.md)**

***
