DEV Community

Moon Soon
Moon Soon

Posted on • Originally published at swapapi.dev

How to Integrate Token Swaps in a Flutter App

Flutter now powers apps used by over 500 million people worldwide, with 2.8 million monthly active developers building across mobile, web, and desktop (GoodFirms, 2025). At the same time, the DeFi market is projected to reach $256.4 billion by 2030, growing at a 43.3% CAGR (CoinLaw, 2025). That intersection creates a clear opportunity: adding token swap functionality to Flutter apps.

This guide walks through integrating swapapi.dev into a Flutter application. The API is free, requires no API keys, and supports 46 EVM chains. By the end, you will have a working swap feature that fetches quotes, displays pricing, and submits transactions.

What You'll Need

  • Flutter 3.22+ with Dart 3.4+
  • The http package for API calls
  • The provider package for state management
  • A wallet integration library (e.g., web3dart) for transaction signing
  • Basic understanding of ERC-20 tokens and blockchain transactions

Install the dependencies:

dependencies:
  http: ^1.2.0
  provider: ^6.1.0
  web3dart: ^2.7.0
  json_annotation: ^4.9.0
Enter fullscreen mode Exit fullscreen mode

Step 1: Define Response Models

The API returns a consistent envelope format: { success, data, error, timestamp }. Model this in Dart so every response is type-safe.

Start with the token model:

class SwapToken {
  final String address;
  final String symbol;
  final String name;
  final int decimals;

  SwapToken({
    required this.address,
    required this.symbol,
    required this.name,
    required this.decimals,
  });

  factory SwapToken.fromJson(Map<String, dynamic> json) {
    return SwapToken(
      address: json["address"] as String,
      symbol: json["symbol"] as String,
      name: json["name"] as String,
      decimals: json["decimals"] as int,
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Next, model the transaction object returned by the API:

class SwapTransaction {
  final String from;
  final String to;
  final String data;
  final String value;
  final int gasPrice;

  SwapTransaction({
    required this.from,
    required this.to,
    required this.data,
    required this.value,
    required this.gasPrice,
  });

  factory SwapTransaction.fromJson(Map<String, dynamic> json) {
    return SwapTransaction(
      from: json["from"] as String,
      to: json["to"] as String,
      data: json["data"] as String,
      value: json["value"] as String,
      gasPrice: json["gasPrice"] as int,
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Now the main swap result. The status field distinguishes between Successful, Partial, and NoRoute responses, all returned with HTTP 200:

class SwapResult {
  final String status;
  final SwapToken? tokenFrom;
  final SwapToken? tokenTo;
  final double? swapPrice;
  final double? priceImpact;
  final String? amountIn;
  final String? expectedAmountOut;
  final String? minAmountOut;
  final SwapTransaction? tx;

  SwapResult({
    required this.status,
    this.tokenFrom,
    this.tokenTo,
    this.swapPrice,
    this.priceImpact,
    this.amountIn,
    this.expectedAmountOut,
    this.minAmountOut,
    this.tx,
  });

  factory SwapResult.fromJson(Map<String, dynamic> json) {
    return SwapResult(
      status: json["status"] as String,
      tokenFrom: json["tokenFrom"] != null
          ? SwapToken.fromJson(json["tokenFrom"])
          : null,
      tokenTo: json["tokenTo"] != null
          ? SwapToken.fromJson(json["tokenTo"])
          : null,
      swapPrice: (json["swapPrice"] as num?)?.toDouble(),
      priceImpact: (json["priceImpact"] as num?)?.toDouble(),
      amountIn: json["amountIn"] as String?,
      expectedAmountOut: json["expectedAmountOut"] as String?,
      minAmountOut: json["minAmountOut"] as String?,
      tx: json["tx"] != null
          ? SwapTransaction.fromJson(json["tx"])
          : null,
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Flutter holds 46% of the cross-platform framework market share compared to React Native's 35% (Statista, 2025). That means nearly half of all cross-platform developers can use this approach directly.

Step 2: Build the Swap Service

Create a service class that handles API communication. The API base URL is https://api.swapapi.dev and the swap endpoint follows the pattern /v1/swap/{chainId}.

import "dart:convert";
import "package:http/http.dart" as http;

class SwapService {
  static const _baseUrl = "https://api.swapapi.dev";
  final http.Client _client;

  SwapService({http.Client? client})
      : _client = client ?? http.Client();
Enter fullscreen mode Exit fullscreen mode

Add the method that fetches a swap quote. The API recommends a 15-second timeout since response times range from 1 to 5 seconds:

  Future<SwapResult> getSwapQuote({
    required int chainId,
    required String tokenIn,
    required String tokenOut,
    required String amount,
    required String sender,
    double maxSlippage = 0.005,
  }) async {
    final uri = Uri.parse(
      "$_baseUrl/v1/swap/$chainId"
      "?tokenIn=$tokenIn"
      "&tokenOut=$tokenOut"
      "&amount=$amount"
      "&sender=$sender"
      "&maxSlippage=$maxSlippage",
    );

    final response = await _client
        .get(uri)
        .timeout(const Duration(seconds: 15));

    final body = jsonDecode(response.body)
        as Map<String, dynamic>;

    if (body["success"] != true) {
      final error = body["error"] as Map<String, dynamic>;
      throw SwapApiException(
        code: error["code"] as String,
        message: error["message"] as String,
      );
    }

    return SwapResult.fromJson(
      body["data"] as Map<String, dynamic>,
    );
  }

  void dispose() => _client.close();
}
Enter fullscreen mode Exit fullscreen mode

Define a custom exception for API errors:

class SwapApiException implements Exception {
  final String code;
  final String message;

  SwapApiException({
    required this.code,
    required this.message,
  });

  @override
  String toString() => "SwapApiException: $code - $message";
}
Enter fullscreen mode Exit fullscreen mode

The API supports 46 chains including Ethereum, Polygon, Base, Arbitrum, BSC, Optimism, and Avalanche. Financial services adoption of Flutter grew 217% since 2021 (Crustlab, 2026), making it the fastest-growing sector for the framework.

Step 3: Handle Token Amounts Correctly

Token decimals vary across chains and tokens. ETH uses 18 decimals, USDC uses 6 on most chains, but USDC on BSC uses 18. Getting this wrong produces incorrect swap amounts.

Build a utility for BigInt conversion:

BigInt toSmallestUnit(double amount, int decimals) {
  final factor = BigInt.from(10).pow(decimals);
  final scaled = (amount * 1e8).round();
  return BigInt.from(scaled) * factor ~/ BigInt.from(1e8.round());
}
Enter fullscreen mode Exit fullscreen mode

Convert the output amount back to a human-readable value:

double fromSmallestUnit(String raw, int decimals) {
  final value = BigInt.parse(raw);
  final factor = BigInt.from(10).pow(decimals);
  return value / factor;
}
Enter fullscreen mode Exit fullscreen mode

Define chain constants with the correct token addresses:

class ChainTokens {
  static const ethereum = 1;
  static const polygon = 137;
  static const base = 8453;

  static const nativeToken =
      "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";

  static const ethUsdc =
      "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";

  static const polygonUsdc =
      "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359";

  static const baseUsdc =
      "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
}
Enter fullscreen mode Exit fullscreen mode

The global mobile wallet market is valued at $12.85 billion in 2025 and projected to reach $104.69 billion by 2034 (Precedence Research, 2025). Accurate decimal handling prevents costly miscalculations in that growing market.

Step 4: Create the State Management Layer

Use ChangeNotifier with Provider to manage swap state. This keeps the UI reactive and separates business logic from presentation.

import "package:flutter/foundation.dart";

enum SwapState { idle, loading, success, error }
Enter fullscreen mode Exit fullscreen mode

Build the view model:

class SwapViewModel extends ChangeNotifier {
  final SwapService _service;

  SwapState _state = SwapState.idle;
  SwapResult? _result;
  String? _errorMessage;

  SwapViewModel(this._service);

  SwapState get state => _state;
  SwapResult? get result => _result;
  String? get errorMessage => _errorMessage;
Enter fullscreen mode Exit fullscreen mode

Add the method that triggers a swap quote. It validates the response status and handles partial fills:

  Future<void> fetchQuote({
    required int chainId,
    required String tokenIn,
    required String tokenOut,
    required double amount,
    required int decimals,
    required String sender,
  }) async {
    _state = SwapState.loading;
    _errorMessage = null;
    notifyListeners();

    try {
      final rawAmount = toSmallestUnit(amount, decimals);

      _result = await _service.getSwapQuote(
        chainId: chainId,
        tokenIn: tokenIn,
        tokenOut: tokenOut,
        amount: rawAmount.toString(),
        sender: sender,
      );

      if (_result!.status == "NoRoute") {
        _state = SwapState.error;
        _errorMessage = "No swap route found";
      } else {
        _state = SwapState.success;
      }
    } on SwapApiException catch (e) {
      _state = SwapState.error;
      _errorMessage = e.message;
    } catch (e) {
      _state = SwapState.error;
      _errorMessage = "Connection error";
    }

    notifyListeners();
  }
}
Enter fullscreen mode Exit fullscreen mode

Over 26,800 companies worldwide use Flutter in production (ScaleUpAlly, 2025). Provider is the recommended state management approach from the Flutter team, making this pattern portable across those codebases.

Step 5: Build the Swap UI

Create a swap screen that lets users select tokens, enter amounts, and view quotes. The UI reacts to state changes from the view model.

class SwapScreen extends StatelessWidget {
  const SwapScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => SwapViewModel(SwapService()),
      child: const _SwapForm(),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Build the form widget with amount input and a swap button:

class _SwapForm extends StatefulWidget {
  const _SwapForm();

  @override
  State<_SwapForm> createState() => _SwapFormState();
}

class _SwapFormState extends State<_SwapForm> {
  final _amountController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    final vm = context.watch<SwapViewModel>();

    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        children: [
          TextField(
            controller: _amountController,
            keyboardType: TextInputType.number,
            decoration: const InputDecoration(
              labelText: "Amount (ETH)",
            ),
          ),
          const SizedBox(height: 16),
          _buildSwapButton(vm),
          const SizedBox(height: 16),
          _buildResult(vm),
        ],
      ),
    );
  }
Enter fullscreen mode Exit fullscreen mode

Add the swap button that triggers the quote:

  Widget _buildSwapButton(SwapViewModel vm) {
    return ElevatedButton(
      onPressed: vm.state == SwapState.loading
          ? null
          : () => _onSwap(vm),
      child: vm.state == SwapState.loading
          ? const CircularProgressIndicator()
          : const Text("Get Swap Quote"),
    );
  }

  void _onSwap(SwapViewModel vm) {
    final amount = double.tryParse(
      _amountController.text,
    );
    if (amount == null || amount <= 0) return;

    vm.fetchQuote(
      chainId: ChainTokens.ethereum,
      tokenIn: ChainTokens.nativeToken,
      tokenOut: ChainTokens.ethUsdc,
      amount: amount,
      decimals: 18,
      sender: "YOUR_WALLET_ADDRESS",
    );
  }
Enter fullscreen mode Exit fullscreen mode

Display the quote result, including price impact as a safety check:

  Widget _buildResult(SwapViewModel vm) {
    if (vm.state == SwapState.error) {
      return Text(
        vm.errorMessage ?? "Unknown error",
        style: const TextStyle(color: Colors.red),
      );
    }

    if (vm.result == null) return const SizedBox();

    final out = fromSmallestUnit(
      vm.result!.expectedAmountOut!,
      vm.result!.tokenTo!.decimals,
    );

    final impact = vm.result!.priceImpact ?? 0;

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Column(
          children: [
            Text("You receive: ${out.toStringAsFixed(2)} USDC"),
            Text("Price impact: ${(impact * 100).toStringAsFixed(2)}%"),
            if (vm.result!.status == "Partial")
              const Text("Partial fill only"),
          ],
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Add Multi-Chain Support

The API uses a path parameter for chain selection, so supporting multiple chains requires only a chain ID and the correct token addresses. Here are the main networks:

Chain ID Native Token USDC Address
Ethereum 1 ETH 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
Polygon 137 POL 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359
Base 8453 ETH 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
Arbitrum 42161 ETH 0xaf88d065e77c8cC2239327C5EDb3A432268e5831
BSC 56 BNB 0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d

Add a chain selector to the UI:

final chains = [
  {"name": "Ethereum", "id": 1},
  {"name": "Polygon", "id": 137},
  {"name": "Base", "id": 8453},
  {"name": "Arbitrum", "id": 42161},
  {"name": "BSC", "id": 56},
];
Enter fullscreen mode Exit fullscreen mode

Flutter's ability to share a single codebase across iOS, Android, web, and desktop means this multi-chain swap feature deploys everywhere from one Dart implementation. With 68% of Flutter developers targeting multiple platforms (GoodFirms, 2025), that reach matters.

Step 7: Handle Errors and Edge Cases

The API returns four error codes you need to handle: INVALID_PARAMS (400), UNSUPPORTED_CHAIN (400), RATE_LIMITED (429), and UPSTREAM_ERROR (502). Add retry logic for transient errors:

Future<SwapResult> getSwapWithRetry({
  required int chainId,
  required String tokenIn,
  required String tokenOut,
  required String amount,
  required String sender,
  int maxRetries = 3,
}) async {
  for (var i = 0; i < maxRetries; i++) {
    try {
      return await getSwapQuote(
        chainId: chainId,
        tokenIn: tokenIn,
        tokenOut: tokenOut,
        amount: amount,
        sender: sender,
      );
    } on SwapApiException catch (e) {
      if (e.code == "RATE_LIMITED") {
        await Future.delayed(
          Duration(seconds: (i + 1) * 5),
        );
        continue;
      }
      if (e.code == "UPSTREAM_ERROR" && i < maxRetries - 1) {
        await Future.delayed(
          Duration(seconds: (i + 1) * 2),
        );
        continue;
      }
      rethrow;
    }
  }
  throw SwapApiException(
    code: "MAX_RETRIES",
    message: "Failed after $maxRetries attempts",
  );
}
Enter fullscreen mode Exit fullscreen mode

Always check priceImpact before letting users execute. Reject swaps with impact worse than -5%:

bool isSafeToExecute(SwapResult result) {
  if (result.status != "Successful") return false;
  if (result.priceImpact != null && result.priceImpact! < -0.05) {
    return false;
  }
  return true;
}
Enter fullscreen mode Exit fullscreen mode

Flutter vs React Native for Crypto Apps

Choosing the right framework for a DeFi app involves performance, ecosystem, and developer availability tradeoffs:

Factor Flutter React Native
Market share 46% 35%
Language Dart JavaScript
Rendering Own engine (Skia) Native components
Web3 libraries web3dart, walletconnect ethers.js, wagmi
Hot reload Sub-second Sub-second
Desktop support Stable Community-driven
GitHub stars 170k+ 121k+
Job postings (US) ~1,000 ~6,400

Flutter's custom rendering engine provides pixel-perfect control over swap UIs across platforms, which matters when displaying precise token amounts and price charts. React Native has a larger JavaScript ecosystem for Web3, but Flutter's Dart-native approach avoids the JavaScript bridge overhead that can impact transaction-heavy apps.

With Flutter seeing 47% year-over-year growth (Black Kite Technologies, 2026), the Dart Web3 ecosystem is expanding rapidly. For teams building new crypto apps, Flutter offers a strong foundation.

FAQ

Is token swap in Flutter secure for production use?

Yes. The API returns raw transaction calldata that gets signed locally in the user's wallet. Private keys never leave the device. Combine this with web3dart for local signing, and the security model matches native wallet apps.

How many chains does the token swap API support?

The API supports 46 EVM-compatible chains including Ethereum, Polygon, Base, Arbitrum, BSC, Optimism, Avalanche, and newer networks like Monad, Berachain, and MegaETH. Every chain uses the same endpoint pattern with only the chain ID changing.

Do I need an API key to integrate token swap in Flutter?

No. The API is free with no API keys, no authentication, and no account registration required. Rate limiting is approximately 30 requests per minute per IP, which is sufficient for most consumer wallet apps.

How do I handle partial fills in a Flutter swap app?

Check data.status in the response. If it returns Partial, the amountIn and expectedAmountOut reflect the partial fill amount, not your original request. Display the actual fill amount to the user and let them decide whether to execute, adjust their amount, or try a different pair.

What token decimals should I use for BSC swaps in Flutter?

USDC and USDT on BSC use 18 decimals, not the 6 decimals used on Ethereum and most other chains. Always reference the API response's tokenTo.decimals field rather than hardcoding values to avoid miscalculating amounts.

Get Started

swapapi.dev provides free token swap quotes and executable calldata across 46 EVM chains. No API keys, no registration.

Add the http package to your Flutter project, copy the SwapService class from this guide, and start fetching quotes. The API returns everything you need to build a complete swap experience in under an hour.

Top comments (0)