# Execute Swap

Run a quoted swap from a wallet — sent immediately for custodial wallets, prepared for client signing for non-custodial wallets.

## Overview

Send the `quoteId` and the `transactionData` object from [Get Quote](https://docs.aureahub.com/docs/swap-quote.md). The API does not fetch the quote again: the transaction it signs or prepares is built from the `transactionData` you send, and `quoteId` is stored with the transaction record. Pass `transactionData` unchanged.

If the quote returned `needsApproval: true`, the approval must be in place first (see [Approve Token](https://docs.aureahub.com/docs/swap-approve.md)). Pass `chain` with the chain you quoted on; otherwise the wallet's own chain is used. The optional token and amount fields (`fromAmount`, `toTokenSymbol`, …) are saved in the transaction metadata for transaction history — copy them from the quote. They do not change what is executed.

> ⚠️ Requests are not de-duplicated: every call signs and sends (or prepares) a new transaction. After a timeout, check [List Transactions](https://docs.aureahub.com/docs/tx-list.md) before retrying.

## Execution Paths

The path depends on the chain (`chain`, or the wallet's chain) and on who holds the wallet key.

- **EVM, custodial wallet** — the API checks that the wallet's native balance covers `transactionData.value` plus `gasLimit` × the current gas price, then signs and sends the transaction. It returns `transactionId`, `txHash` and `status: "pending"`; in this response `fromToken`, `toToken`, `fromAmount` and `expectedToAmount` are empty strings. Poll [Get Tx Status](https://docs.aureahub.com/docs/tx-status.md) with `transactionId`. A background job that runs every 60 seconds also resolves pending EVM transactions.
- **EVM, non-custodial wallet** (key management scheme `client_side`, `client_side_pending` or `mpc_tss`) — `transactionData.to`, `value` and `gasLimit` are required. If `value` is greater than zero, the API checks that the balance covers it plus `gasLimit` × `maxFeePerGas`. It then prepares an unsigned EIP-1559 transaction (fee caps 1.5× current fee data) and creates a `pending` transaction record. The response has `requiresClientSigning: true` with `transactionId`, `preparedTxId`, `txHashToSign`, `nonce`, `maxFeePerGas`, `maxPriorityFeePerGas`, `chainId` and `expiresAt`; `txHash` is an empty string. Sign `txHashToSign` and call [Broadcast Swap Tx](https://docs.aureahub.com/docs/swap-broadcast.md).
- **Solana** — see [Execute Solana Swap](https://docs.aureahub.com/docs/swap-execute-solana.md). Custodial Solana swaps return `status: "confirmed"`; non-custodial Solana wallets are rejected with `400`.

> ⚠️ **Broadcast window (non-custodial).** Broadcast Swap Tx is refused with `QUOTE_EXPIRED` after `quoteExpiry`. If you did not send `quoteExpiry`, the limit is 60 seconds after this call. It is refused with `410` once the prepared transaction expires (`expiresAt`, 5 minutes). If the `quoteExpiry` you send is already in the past, this call itself fails with `400`.

## Endpoint

### `POST /v1/swap/execute`

Authentication: bearer token required.

Executes the transaction data of a LI.FI quote from a wallet.

**Request body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `walletId` | string (uuid) | yes | Wallet that executes the swap. Must belong to the authenticated user. |
| `quoteId` | string | yes | `quoteId` from Get Quote; stored with the transaction. |
| `transactionData` | object | yes | `transactionData` from Get Quote: `data` (required), `to` (string or `null`), `value`, `gasLimit`. EVM swaps need `to`, `value` and `gasLimit`. |
| `chain` | string | no | Chain to execute on. Defaults to the wallet's chain. |
| `isTestnet` | boolean | no | Use the testnet of the chain. Defaults to `false`. |
| `quoteExpiry` | string | no | ISO 8601 time after which a non-custodial broadcast is refused. Only used for non-custodial EVM wallets. |
| `fromAmount` | string | no | Stored in transaction metadata and used as the record's amount. |
| `toAmount` | string | no | Stored in transaction metadata. |
| `fromTokenSymbol` | string | no | Stored in transaction metadata. |
| `toTokenSymbol` | string | no | Stored in transaction metadata. |
| `fromTokenDecimals` | integer | no | Stored in transaction metadata (0 or greater). |
| `toTokenDecimals` | integer | no | Stored in transaction metadata (0 or greater). |
| `fromTokenAddress` | string | no | Stored in transaction metadata. |
| `toTokenAddress` | string | no | Stored in transaction metadata. |
| `gasless` | boolean | no | Accepted; not used by this endpoint. Gasless swaps use `/v1/swap/execute-gasless`. |

**Responses**

`200` Custodial EVM

```json
{
  "transactionId": "…",
  "txHash": "0x…",
  "status": "pending",
  "fromToken": "",
  "toToken": "",
  "fromAmount": "",
  "expectedToAmount": ""
}
```

`200` Non-custodial EVM

```json
{
  "transactionId": "…",
  "txHash": "",
  "status": "pending",
  "fromToken": "…",
  "toToken": "…",
  "fromAmount": "…",
  "expectedToAmount": "…",
  "requiresClientSigning": true,
  "preparedTxId": "…",
  "txHashToSign": "0x…",
  "nonce": "…",
  "maxFeePerGas": "…",
  "maxPriorityFeePerGas": "…",
  "chainId": 137,
  "expiresAt": "…"
}
```

`400` Bad Request

```json
{
  "statusCode": 400,
  "error": "BadRequestError",
  "message": "Insufficient MATIC balance for swap. Required: … MATIC (… MATIC swap value + … MATIC estimated gas), Available: … MATIC, Shortfall: … MATIC"
}
```

`404` Not Found

```json
{
  "statusCode": 404,
  "error": "NotFoundError",
  "message": "Wallet not found"
}
```

In the non-custodial response, `fromToken`, `toToken`, `fromAmount` and `expectedToAmount` echo the optional fields you sent (empty strings otherwise). Other `400` responses include `Request validation failed`, `Unsupported chain: …`, `Network mismatch: …`, `Cannot execute swap from watch-only wallet`, `Failed to execute swap: …` (custodial), `transactionData.to is required for non-custodial EVM swaps` (likewise for `value` and `gasLimit`), `Insufficient balance: …` and `QUOTE_EXPIRED: The LiFi quote has already expired. Please request a new quote.`

## Complete Swap Flow

A same-chain EVM swap covering custodial and non-custodial wallets. `signDigest` must return `{ v, r, s }` with `v` = 0 or 1.

```javascript
async function executeFullSwap(token, { walletId, chain, fromToken, toToken, fromAmount }, signDigest) {
  const post = async (path, body) => {
    const res = await fetch(`https://api.aureahub.com${path}`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
      body: JSON.stringify(body)
    });
    const json = await res.json();
    if (!res.ok) throw new Error(`${res.status} ${json.code ?? ''}: ${json.message}`);
    return json;
  };

  // 1. Quote
  const quote = await post('/v1/swap/quote', {
    walletId, fromChain: chain, toChain: chain, fromToken, toToken, fromAmount, slippage: 0.5
  });

  // 2. Approval (only when the current allowance is zero)
  if (quote.needsApproval) {
    const approval = await post('/v1/swap/approve', {
      walletId, chain, tokenAddress: fromToken, spenderAddress: quote.approvalAddress
    });
    if (approval.requiresClientSigning) {
      const sig = await signDigest(approval.txHashToSign);
      await post('/v1/swap/broadcast-approve', { preparedTxId: approval.preparedTxId, ...sig });
      // Approvals have no transaction record: poll /v1/swap/check-allowance until it is in place
    } else if (approval.status !== 'confirmed') {
      throw new Error('Approval transaction failed');
    }
  }

  // 3. Execute — transactionData exactly as quoted
  const exec = await post('/v1/swap/execute', {
    walletId,
    chain,
    quoteId: quote.quoteId,
    transactionData: quote.transactionData,
    fromAmount: quote.fromAmount,
    toAmount: quote.toAmount,
    fromTokenSymbol: quote.fromToken.symbol,
    toTokenSymbol: quote.toToken.symbol,
    fromTokenDecimals: quote.fromToken.decimals,
    toTokenDecimals: quote.toToken.decimals,
    fromTokenAddress: quote.fromToken.address,
    toTokenAddress: quote.toToken.address
  });

  if (!exec.requiresClientSigning) return exec; // custodial: status 'pending'

  // 4. Non-custodial: sign and broadcast (60 s window when quoteExpiry is not sent)
  const sig = await signDigest(exec.txHashToSign);
  return post('/v1/swap/broadcast', {
    transactionId: exec.transactionId,
    preparedTxId: exec.preparedTxId,
    ...sig
  });
  // Then poll GET /v1/transactions/{transactionId}/status until it is no longer 'pending'
}
```

---

Web version: https://docs.aureahub.com/#swap-execute
