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
| 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 |
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. 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 — sign and send as-is. No approvals. |
The EVM example below uses viem; the Solana example uses @solana/web3.js. 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.
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
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.
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;
}
Using Dynamic server wallets (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 does internally.
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
{
"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:
{
"txHash": "5f3Hq...signature",
"from": { "chainName": "SOL", "chainId": "101" },
"to": { "chainName": "EVM", "chainId": "8453" }
}
Response
{
"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:
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:
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.
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.
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);
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.
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.