Skip to main content

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 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)
The Swap API is stateless — there is no session token or transaction state to manage. Each call is independent.

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
{
  "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

FieldTypeDescription
from.addressstringWallet address sending the tokens
from.chainNamestringChain family: "EVM", "SOL", "BTC", "SUI"
from.chainIdstringNetwork ID (e.g., "1" for Ethereum, "137" for Polygon)
from.tokenAddressstringToken contract address. Use 0x0000000000000000000000000000000000000000 for native tokens on EVM
from.amountstringAmount in smallest unit (e.g., "10000000" = 10 USDC). Mutually exclusive with to.amount
to.addressstringWallet address receiving the tokens
to.chainNamestringDestination chain family
to.chainIdstringDestination network ID
to.tokenAddressstringDestination token contract address
to.amountstringDesired output amount. Mutually exclusive with from.amount
slippagenumberMax slippage as a decimal (e.g., 0.005 = 0.5%). Optional
orderstringRoute preference: "FASTEST" or "CHEAPEST". Optional
maxPriceImpactnumberHide routes above this price impact (e.g., 0.15 = 15%). Optional
Exactly one of from.amount or to.amount must be provided. Sending both or neither returns a 400 error.

Response

{
  "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. This example uses viem, but any signing library works.

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.
approve.ts
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

swap.ts
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;
}

Step 3: Poll for Status

For cross-chain swaps, use the status endpoint to track completion. For same-chain swaps, the transaction receipt alone is sufficient.
POST /sdk/{environmentId}/swap/status
Content-Type: application/json
{
  "txHash": "0xabc123...",
  "from": { "chainName": "EVM", "chainId": "1" },
  "to": { "chainName": "EVM", "chainId": "137" }
}

Response

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

Status Values

StatusDescription
PENDINGSwap in progress
COMPLETEDSwap finished successfully
FAILEDSwap failed

Substatus Values (When Pending)

SubstatusDescription
WAIT_SOURCE_CONFIRMATIONSWaiting for source chain confirmations
WAIT_DESTINATION_TRANSACTIONWaiting for destination chain transaction
BRIDGE_NOT_AVAILABLEBridge temporarily unavailable
REFUND_IN_PROGRESSRefund 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:
swap-example.ts
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:
cross-chain-swap.ts
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' },
});
Cross-chain swaps may take longer to complete. The steps array in the quote shows each bridge and swap hop along the route.

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

ChainchainNamechainIdNetworks
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:
ChainAccepted native token addresses
EVM0x0000000000000000000000000000000000000000 (zero address) or 0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
Solana11111111111111111111111111111111 (System Program) or So11111111111111111111111111111111111111112 (Wrapped SOL)
Bitcoin11111111111111111111111111111111 or bitcoin
Sui0x2::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

StatusCauseRecovery
400Missing or conflicting amount fieldsProvide exactly one of from.amount or to.amount
422Unsupported chainCheck from.chainName is supported
5xxProvider or infrastructure errorRetry 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.