# 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.

> ⚠️ Without an `Idempotency-Key`, requests are not de-duplicated: every call signs and sends (or prepares) a new transaction. Send a key and retry with it, or, without one, check [List Transactions](https://docs.aureahub.com/docs/tx-list.md) before retrying ([Idempotency](https://docs.aureahub.com/docs/idempotency.md)).

## 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`, and a wallet that is not a Solana wallet with `404` `Wallet not found`.

> ⚠️ **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`.

> ℹ️ **Institutional tenants only.** For a tenant on the institutional security profile, this route needs an `Idempotency-Key`, and the tenant's policy decides before anything is signed or handed out to sign. A rule that asks for an approval holds the operation: the route answers `202` with the operation to follow (the **202** tab below), and nothing is signed or sent until the rule's approvers approve it — [Approvals](https://docs.aureahub.com/docs/guide-approvals.md). The policy's other refusals: [Policy Sets](https://docs.aureahub.com/docs/guide-policy-sets.md). Every other tenant gets the answers below, unchanged.

> ℹ️ **Swaps built on the server, where Aurea enables them for your tenant:** an EVM swap is signed from the route Aurea kept with its quote, not from the transaction data you send — execute a quote you asked for, for this wallet, while it is valid (`409` `SWAP_QUOTE_UNKNOWN`), whose router is on Aurea's allowlist (`403` `SWAP_ROUTER_NOT_ALLOWED`). A custodial swap's unused allowance is revoked once it is mined, with exact approvals on. [Signing Rules](https://docs.aureahub.com/docs/guide-signing-rules.md).

## Endpoint

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

Authentication: bearer token required.

Executes the transaction data of a quote from a wallet.

**Headers**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | string | no | Optional — required for a tenant on the institutional security profile (`400` `IDEMPOTENCY_KEY_REQUIRED` without it). Up to 255 letters, digits, - or _. The same key with the same request answers with the first answer (`idempotency-replayed: true`) instead of running it again; see [Idempotency](https://docs.aureahub.com/docs/idempotency.md) |

**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": "…"
}
```

`202` Held for approval

```json
{
  "operationId": "1d7a3c52-8e4b-4f19-a0c6-2b9e7d5f3a18",
  "state": "PENDING_AUTHORIZATION",
  "subState": "awaiting_approval",
  "approval": {
    "id": "c3e1a9f4-6b2d-4e8a-9f17-5d0c2b8e4a63",
    "ruleId": "e2b6d9f1-7a3c-4c58-8b0e-5f1a4d2c9b73",
    "ruleName": "Large transfers need two approvers",
    "threshold": 2,
    "expiresAt": null
  }
}
```

`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 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
