> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dynamic.xyz/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Token swaps via API

> Swap tokens across chains or within the same chain using Dynamic's Swap API — no SDK or frontend required

## What We're Building

A server-side flow that swaps tokens using Dynamic's Swap API directly. By the end of this guide you will be able to:

* Get a swap quote with route, fees, and a ready-to-sign payload
* Handle ERC-20 token approvals when required
* Sign and broadcast the swap transaction on-chain
* Poll for cross-chain completion

This recipe uses raw HTTP calls so it works in any language or runtime — Node.js scripts, backend services, AI agents, or cron jobs.

## Prerequisites

* A [Dynamic](https://app.dynamic.xyz) environment
* A wallet with private key access (for signing)
* Node.js 18+ (or any runtime with `fetch`)

## Base URL

All swap endpoints live under:

```
https://app.dynamicauth.com/api/v0/sdk/{environmentId}
```

## Overview

The swap flow is two API calls plus one on-chain transaction:

```
1. Get quote     →  POST /swap/quote     (returns route + signing payload)
2. Sign & send   →  use the signing payload with your wallet
3. Poll status   →  POST /swap/status    (check completion by tx hash)
```

<Note>
  The Swap API is stateless — there is no session token or transaction state to manage. Each call is independent.
</Note>

***

## Step 1: Get a Swap Quote

Request a quote by specifying the source and destination tokens. The API finds the best route and returns a `signingPayload` you can send directly on-chain.

```
POST /sdk/{environmentId}/swap/quote
Content-Type: application/json
```

```json theme={"system"}
{
  "from": {
    "address": "0xYourWalletAddress",
    "chainName": "EVM",
    "chainId": "1",
    "tokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
    "amount": "10000000"
  },
  "to": {
    "address": "0xYourWalletAddress",
    "chainName": "EVM",
    "chainId": "1",
    "tokenAddress": "0x0000000000000000000000000000000000000000"
  },
  "slippage": 0.005,
  "order": "CHEAPEST"
}
```

### Request Fields

| Field               | Type   | Description                                                                                       |
| :------------------ | :----- | :------------------------------------------------------------------------------------------------ |
| `from.address`      | string | Wallet address sending the tokens                                                                 |
| `from.chainName`    | string | Chain family: `"EVM"`, `"SOL"`, `"BTC"`, `"SUI"`                                                  |
| `from.chainId`      | string | Network ID (e.g., `"1"` for Ethereum, `"137"` for Polygon)                                        |
| `from.tokenAddress` | string | Token contract address. Use `0x0000000000000000000000000000000000000000` for native tokens on EVM |
| `from.amount`       | string | Amount in smallest unit (e.g., `"10000000"` = 10 USDC). Mutually exclusive with `to.amount`       |
| `to.address`        | string | Wallet address receiving the tokens                                                               |
| `to.chainName`      | string | Destination chain family                                                                          |
| `to.chainId`        | string | Destination network ID                                                                            |
| `to.tokenAddress`   | string | Destination token contract address                                                                |
| `to.amount`         | string | Desired output amount. Mutually exclusive with `from.amount`                                      |
| `slippage`          | number | Max slippage as a decimal (e.g., `0.005` = 0.5%). Optional                                        |
| `order`             | string | Route preference: `"FASTEST"` or `"CHEAPEST"`. Optional                                           |
| `maxPriceImpact`    | number | Hide routes above this price impact (e.g., `0.15` = 15%). Optional                                |

<Warning>
  Exactly one of `from.amount` or `to.amount` must be provided. Sending both or neither returns a `400` error.
</Warning>

### Response

```json theme={"system"}
{
  "id": "quote-uuid",
  "from": {
    "address": "0xYourWalletAddress",
    "amount": "10000000",
    "amountUSD": "10.00",
    "token": { "address": "0xA0b8...", "symbol": "USDC" }
  },
  "to": {
    "address": "0xYourWalletAddress",
    "amount": "3200000000000000",
    "amountUSD": "9.85",
    "token": { "address": "0x0000...0000", "symbol": "ETH" }
  },
  "gasCostUSD": "0.12",
  "feeCosts": [],
  "approvalAddress": "0xRouterAddress",
  "signingPayload": {
    "to": "0xRouterAddress",
    "data": "0xCalldata...",
    "value": "0x0"
  },
  "steps": [
    {
      "id": "step-1",
      "type": "swap",
      "tool": "1inch",
      "from": { "amount": "10000000", "amountUSD": "10.00", "token": { "symbol": "USDC" } },
      "to": { "amount": "3200000000000000", "amountMin": "3184000000000000", "amountUSD": "9.85", "token": { "symbol": "ETH" } },
      "feeCosts": [],
      "gasCosts": []
    }
  ]
}
```

The `signingPayload` contains the `to`, `data`, and `value` fields needed to submit the transaction on-chain.

For **cross-chain swaps**, the `steps` array shows each hop (bridge + swap), so you can display the full route to users.

***

## Step 2: Sign and Broadcast

Use the `signingPayload` from the quote to submit an on-chain transaction. Its shape depends on the source chain:

| Chain  | `signingPayload` shape      | Notes                                                                                                                           |
| :----- | :-------------------------- | :------------------------------------------------------------------------------------------------------------------------------ |
| EVM    | `{ to, data, value }`       | Raw transaction request. ERC-20 swaps may also need an approval transaction first.                                              |
| Solana | `{ serializedTransaction }` | Base64-encoded [versioned transaction](https://solana.com/docs/core/transactions/versions) — sign and send as-is. No approvals. |

The EVM example below uses [viem](https://viem.sh); the Solana example uses [`@solana/web3.js`](https://solana.com/docs/clients/javascript). Any signing library works.

### EVM

#### Handle ERC-20 Approval

If you're swapping an ERC-20 token (not a native token), you may need to approve the router to spend your tokens first. Check whether the router already has sufficient allowance — if not, send an approval transaction before the swap.

```typescript title="approve.ts" theme={"system"}
import { createWalletClient, http, publicActions, parseAbi } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { mainnet } from 'viem/chains';

const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const client = createWalletClient({
  account,
  chain: mainnet,
  transport: http(),
}).extend(publicActions);

const ERC20_ABI = parseAbi([
  'function allowance(address owner, address spender) view returns (uint256)',
  'function approve(address spender, uint256 amount) returns (bool)',
]);

async function ensureApproval(
  tokenAddress: `0x${string}`,
  spender: `0x${string}`,
  amount: bigint
) {
  const allowance = await client.readContract({
    address: tokenAddress,
    abi: ERC20_ABI,
    functionName: 'allowance',
    args: [account.address, spender],
  });

  if (allowance < amount) {
    const hash = await client.writeContract({
      address: tokenAddress,
      abi: ERC20_ABI,
      functionName: 'approve',
      args: [spender, amount],
    });
    console.log('Approval tx:', hash);
  }
}
```

#### Send the Swap Transaction

```typescript title="swap.ts" theme={"system"}
async function sendSwap(signingPayload: {
  to: string;
  data: string;
  value: string;
}) {
  const txHash = await client.sendTransaction({
    to: signingPayload.to as `0x${string}`,
    data: signingPayload.data as `0x${string}`,
    value: BigInt(signingPayload.value),
  });

  console.log('Swap broadcasted:', txHash);
  return txHash;
}
```

### Solana

For Solana the quote returns a base64 `serializedTransaction` instead of EVM calldata — a ready-to-sign transaction with the blockhash and any associated token account (ATA) creation already bundled in. There are no token approvals. Deserialize it, sign with your keypair, and broadcast as-is. Sign promptly: the quote's blockhash has a limited lifetime, so request a fresh quote rather than reusing a stale one.

```typescript title="swap-solana.ts" theme={"system"}
import {
  Connection,
  Keypair,
  VersionedTransaction,
} from '@solana/web3.js';

const connection = new Connection(
  process.env.SOLANA_RPC_URL ?? 'https://api.mainnet-beta.solana.com',
  'confirmed'
);
const signer = Keypair.fromSecretKey(
  Uint8Array.from(JSON.parse(process.env.SOLANA_SECRET_KEY as string))
);

async function sendSolanaSwap(serializedTransaction: string) {
  // Deserialize the base64 versioned transaction from the quote and sign it
  // as-is — the quote already set the fee payer, blockhash, and instructions.
  const tx = VersionedTransaction.deserialize(
    Buffer.from(serializedTransaction, 'base64')
  );
  tx.sign([signer]);

  const signature = await connection.sendRawTransaction(tx.serialize());
  await connection.confirmTransaction(signature, 'confirmed');

  console.log('Swap broadcasted:', signature);
  return signature;
}
```

<Note>
  Using Dynamic [server wallets](/overview/wallets/overview) (MPC) instead of a local keypair? Sign the same `VersionedTransaction` with `DynamicSvmWalletClient.signTransaction`, attach the returned signature to the transaction, then broadcast — no raw private key required. This mirrors what the SDK's [`executeSwapTransaction`](/javascript/reference/client/execute-swap-transaction) does internally.
</Note>

***

## Step 3: Poll for Status

For cross-chain swaps, use the status endpoint to track completion. For same-chain swaps (including Solana), the transaction confirmation alone is sufficient.

```
POST /sdk/{environmentId}/swap/status
Content-Type: application/json
```

```json theme={"system"}
{
  "txHash": "0xabc123...",
  "from": { "chainName": "EVM", "chainId": "1" },
  "to": { "chainName": "EVM", "chainId": "137" }
}
```

For a Solana source or destination, set `chainName` to `"SOL"` and `chainId` to `"101"`, and pass the transaction signature as `txHash`:

```json theme={"system"}
{
  "txHash": "5f3Hq...signature",
  "from": { "chainName": "SOL", "chainId": "101" },
  "to": { "chainName": "EVM", "chainId": "8453" }
}
```

### Response

```json theme={"system"}
{
  "status": "PENDING",
  "substatus": "WAIT_DESTINATION_TRANSACTION",
  "sendingTxLink": "https://etherscan.io/tx/0xabc123...",
  "receivingTxLink": null
}
```

### Status Values

| Status      | Description                |
| :---------- | :------------------------- |
| `PENDING`   | Swap in progress           |
| `COMPLETED` | Swap finished successfully |
| `FAILED`    | Swap failed                |

### Substatus Values (When Pending)

| Substatus                      | Description                               |
| :----------------------------- | :---------------------------------------- |
| `WAIT_SOURCE_CONFIRMATIONS`    | Waiting for source chain confirmations    |
| `WAIT_DESTINATION_TRANSACTION` | Waiting for destination chain transaction |
| `BRIDGE_NOT_AVAILABLE`         | Bridge temporarily unavailable            |
| `REFUND_IN_PROGRESS`           | Refund is being processed                 |

Poll every 3–5 seconds until `status` is `COMPLETED` or `FAILED`.

***

## Complete Example

A self-contained TypeScript script that gets a quote, signs, broadcasts, and polls for completion:

```typescript title="swap-example.ts" theme={"system"}
import { createWalletClient, http, publicActions, parseAbi } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { mainnet } from 'viem/chains';

// --- Config ---
const API = 'https://app.dynamicauth.com/api/v0';
const ENV_ID = 'your-environment-id';

const account = privateKeyToAccount(
  process.env.PRIVATE_KEY as `0x${string}`
);
const client = createWalletClient({
  account,
  chain: mainnet,
  transport: http(),
}).extend(publicActions);

// --- Helpers ---
async function api(path: string, body: object) {
  const res = await fetch(`${API}${path}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });
  if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
  return res.json();
}

const ERC20_ABI = parseAbi([
  'function allowance(address owner, address spender) view returns (uint256)',
  'function approve(address spender, uint256 amount) returns (bool)',
]);

// --- Swap flow ---
async function swap() {
  // 1. Get quote: swap 10 USDC → ETH on Ethereum
  const quote = await api(`/sdk/${ENV_ID}/swap/quote`, {
    from: {
      address: account.address,
      chainName: 'EVM',
      chainId: '1',
      tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
      amount: '10000000', // 10 USDC (6 decimals)
    },
    to: {
      address: account.address,
      chainName: 'EVM',
      chainId: '1',
      tokenAddress: '0x0000000000000000000000000000000000000000', // ETH
    },
    slippage: 0.005,
    order: 'CHEAPEST',
  });

  console.log(
    `Quote: ${quote.from.amountUSD} USDC → ${quote.to.amountUSD} ETH`
  );
  console.log(`Gas: $${quote.gasCostUSD}`);

  // 2. Approve token spend (ERC-20 only)
  if (quote.approvalAddress) {
    const tokenAddress =
      quote.from.token.address as `0x${string}`;
    const spender = quote.approvalAddress as `0x${string}`;

    const allowance = await client.readContract({
      address: tokenAddress,
      abi: ERC20_ABI,
      functionName: 'allowance',
      args: [account.address, spender],
    });

    if (allowance < BigInt(quote.from.amount)) {
      const approvalHash = await client.writeContract({
        address: tokenAddress,
        abi: ERC20_ABI,
        functionName: 'approve',
        args: [spender, BigInt(quote.from.amount)],
      });
      console.log('Approval tx:', approvalHash);
      // Wait for approval confirmation
      await client.waitForTransactionReceipt({ hash: approvalHash });
    }
  }

  // 3. Send the swap transaction
  const txHash = await client.sendTransaction({
    to: quote.signingPayload.to as `0x${string}`,
    data: quote.signingPayload.data as `0x${string}`,
    value: BigInt(quote.signingPayload.value),
  });
  console.log('Swap tx:', txHash);

  // 4. Poll for completion (useful for cross-chain swaps)
  while (true) {
    const status = await api(`/sdk/${ENV_ID}/swap/status`, {
      txHash,
      from: { chainName: 'EVM', chainId: '1' },
      to: { chainName: 'EVM', chainId: '1' },
    });

    console.log(`Status: ${status.status} (${status.substatus ?? ''})`);

    if (status.status === 'COMPLETED') {
      console.log('Swap complete!');
      if (status.explorerLink) console.log('Explorer:', status.explorerLink);
      return;
    }

    if (status.status === 'FAILED') {
      throw new Error(`Swap failed: ${status.message}`);
    }

    await new Promise((r) => setTimeout(r, 3000));
  }
}

swap().catch(console.error);
```

***

## Cross-Chain Example

Swap USDC on Ethereum to MATIC on Polygon — the API handles bridging automatically:

```typescript title="cross-chain-swap.ts" theme={"system"}
const quote = await api(`/sdk/${ENV_ID}/swap/quote`, {
  from: {
    address: account.address,
    chainName: 'EVM',
    chainId: '1',      // Ethereum
    tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
    amount: '50000000', // 50 USDC
  },
  to: {
    address: account.address,
    chainName: 'EVM',
    chainId: '137',    // Polygon
    tokenAddress: '0x0000000000000000000000000000000000000000', // MATIC
  },
  slippage: 0.01,
  order: 'FASTEST',
});

// Inspect the multi-step route
for (const step of quote.steps) {
  console.log(`${step.type} via ${step.tool}: ${step.from.token.symbol} → ${step.to.token.symbol}`);
}

// Sign and send (same as above)
// Then poll status with the correct source/destination chains
const status = await api(`/sdk/${ENV_ID}/swap/status`, {
  txHash,
  from: { chainName: 'EVM', chainId: '1' },
  to: { chainName: 'EVM', chainId: '137' },
});
```

<Note>
  Cross-chain swaps may take longer to complete. The `steps` array in the quote shows each bridge and swap hop along the route.
</Note>

***

## Solana Example

Swap native SOL to USDC on Solana. The quote returns a base64 `serializedTransaction`; sign and broadcast it with the `sendSolanaSwap` helper from [Step 2](#solana).

```typescript title="solana-swap.ts" theme={"system"}
const quote = await api(`/sdk/${ENV_ID}/swap/quote`, {
  from: {
    address: signer.publicKey.toBase58(),
    chainName: 'SOL',
    chainId: '101',
    tokenAddress: '11111111111111111111111111111111', // native SOL
  },
  to: {
    address: signer.publicKey.toBase58(),
    chainName: 'SOL',
    chainId: '101',
    tokenAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC
    amount: '5000000', // exact-out: 5 USDC (6 decimals)
  },
  slippage: 0.1,
  order: 'CHEAPEST',
});

console.log(`Quote: ${quote.from.amountUSD} SOL → ${quote.to.amountUSD} USDC`);
console.log(`Gas: $${quote.gasCostUSD}`);

// Sign + broadcast the base64 versioned transaction (see Step 2 → Solana)
const signature = await sendSolanaSwap(
  quote.signingPayload.serializedTransaction
);
console.log('Swap signature:', signature);
```

<Note>
  This is an **exact-output** swap (`to.amount` is set), so the route pins the minimum USDC out at that amount. A too-tight `slippage` can trip the aggregator's minimum-out guard on thin routes — `0.1` (10%) reliably clears small swaps; tighten it for larger amounts. To swap an exact amount of SOL instead, set `from.amount` and omit `to.amount`.
</Note>

***

## Supported Chains and Native Tokens

The Swap API supports the following chains (mainnet only). Use these values for `chainName`, `chainId`, and native token addresses in your requests.

### Chain Reference

| Chain   | `chainName` | `chainId`                                                                                                                             | Networks               |
| :------ | :---------- | :------------------------------------------------------------------------------------------------------------------------------------ | :--------------------- |
| EVM     | `"EVM"`     | Standard EVM chain ID (e.g., `"1"` for Ethereum, `"137"` for Polygon, `"8453"` for Base, `"42161"` for Arbitrum, `"10"` for Optimism) | All major EVM networks |
| Solana  | `"SOL"`     | `"101"`                                                                                                                               | Solana mainnet         |
| Bitcoin | `"BTC"`     | `"1"`                                                                                                                                 | Bitcoin mainnet        |
| Sui     | `"SUI"`     | `"501"`                                                                                                                               | Sui mainnet            |

### Native Token Addresses

For native tokens (ETH, SOL, BTC, SUI), use any of the accepted addresses below in the `tokenAddress` field:

| Chain   | Accepted native token addresses                                                                                    |
| :------ | :----------------------------------------------------------------------------------------------------------------- |
| EVM     | `0x0000000000000000000000000000000000000000` (zero address) or `0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee`        |
| Solana  | `11111111111111111111111111111111` (System Program) or `So11111111111111111111111111111111111111112` (Wrapped SOL) |
| Bitcoin | `11111111111111111111111111111111` or `bitcoin`                                                                    |
| Sui     | `0x2::sui::SUI` or `0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI`                  |

For non-native tokens, use the token's contract address on that chain (e.g., `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` for USDC on Ethereum).

***

## Error Handling

| Status | Cause                                  | Recovery                                            |
| :----- | :------------------------------------- | :-------------------------------------------------- |
| `400`  | Missing or conflicting `amount` fields | Provide exactly one of `from.amount` or `to.amount` |
| `422`  | Unsupported chain                      | Check `from.chainName` is supported                 |
| `5xx`  | Provider or infrastructure error       | Retry with exponential backoff                      |

***

## Tips

* **Quote freshness:** Quotes are snapshots — prices and gas can shift. Sign promptly after receiving a quote.
* **Slippage:** Set slippage based on token liquidity. Stablecoins work well at `0.005` (0.5%), volatile pairs may need `0.01` or more.
* **Order preference:** Use `"CHEAPEST"` to minimize fees or `"FASTEST"` to reduce execution time. Cross-chain routes benefit the most from this.
* **Price impact:** Set `maxPriceImpact` to filter out routes that would move the market too much for your trade size.

## Related

* [Fireblocks Flow API guide](/overview/fireblocks-flow-api) — Accept payments with automatic cross-chain settlement
* [Cross-chain swaps with LI.FI](/recipes/integrations/swaps/lifi) — Frontend integration with LI.FI widget
* [`getSwapQuote`](/javascript/reference/client/get-swap-quote) — JavaScript SDK swap quote reference
* [`executeSwapTransaction`](/javascript/reference/client/execute-swap-transaction) — JavaScript SDK swap execution reference
