DEV Community

Moon Soon
Moon Soon

Posted on • Originally published at swapapi.dev

How to Add Cross-Chain Swaps to a Go Microservice

Go powers the backends of most major blockchain infrastructure -- from consensus clients to validator tooling to DEX routing engines. If you need to add token swaps across multiple EVM chains to a Go service, you can do it with a single GET request and zero API keys. This guide walks through building a production-ready swap client in Go using swapapi.dev, a free DEX aggregator API that covers 46 chains. Cross-chain swap volume hit $56.1 billion in a single month in mid-2025, and the DeFi market is projected to grow at a 43.3% CAGR through 2030. If your Go service touches crypto, swap functionality is table stakes.

What You'll Need

Before writing any code, make sure you have:

  • Go 1.21+ installed (go version)
  • A wallet address to use as the sender parameter (the address that will execute the swap on-chain)
  • Basic familiarity with EVM token standards (ERC-20 addresses, decimals, raw amounts)
  • No API key -- swapapi.dev requires zero authentication

The API is a single endpoint:

GET /v1/swap/{chainId}?tokenIn={addr}&tokenOut={addr}&amount={raw}&sender={addr}
Enter fullscreen mode Exit fullscreen mode

It returns executable calldata you can broadcast directly to the chain. Let's build it.

Step 1: Define the Swap Response Types

Start by modeling the API response in Go structs. The API returns an envelope format with success, data, and timestamp fields. According to the JetBrains Go ecosystem report, over 1.9 million developers build web services with Go -- and struct-based JSON handling is one reason why. Go's encoding/json maps cleanly to API contracts with no extra dependencies.

Create a file called swap.go:

package swap

type SwapResponse struct {
    Success   bool      `json:"success"`
    Data      *SwapData `json:"data,omitempty"`
    Error     *APIError `json:"error,omitempty"`
    Timestamp string    `json:"timestamp"`
}

type SwapData struct {
    Status            string     `json:"status"`
    TokenFrom         *TokenInfo `json:"tokenFrom,omitempty"`
    TokenTo           *TokenInfo `json:"tokenTo,omitempty"`
    SwapPrice         float64    `json:"swapPrice,omitempty"`
    PriceImpact       float64    `json:"priceImpact,omitempty"`
    AmountIn          string     `json:"amountIn,omitempty"`
    ExpectedAmountOut string     `json:"expectedAmountOut,omitempty"`
    MinAmountOut      string     `json:"minAmountOut,omitempty"`
    Tx                *TxData    `json:"tx,omitempty"`
    RpcURL            string     `json:"rpcUrl,omitempty"`
    RpcURLs           []string   `json:"rpcUrls,omitempty"`
}

type TokenInfo struct {
    Address  string `json:"address"`
    Symbol   string `json:"symbol"`
    Name     string `json:"name"`
    Decimals int    `json:"decimals"`
}

type TxData struct {
    From     string `json:"from"`
    To       string `json:"to"`
    Data     string `json:"data"`
    Value    string `json:"value"`
    GasPrice int64  `json:"gasPrice"`
}

type APIError struct {
    Code    string `json:"code"`
    Message string `json:"message"`
}
Enter fullscreen mode Exit fullscreen mode

These structs cover all three response statuses: Successful, Partial, and NoRoute. Using pointer types for optional fields lets you distinguish between "not present" and "zero value" -- critical when handling NoRoute responses where most fields are absent.

Step 2: Build the HTTP Client with Retries

The API recommends a 15-second HTTP timeout and suggests retrying UPSTREAM_ERROR (502) responses up to 3 times with 2-5 second backoff. Go's standard net/http handles this cleanly -- no third-party libraries needed. The microservices architecture market reached $7.45 billion in 2025, and retry logic is one of those microservice patterns every production service needs.

package swap

import (
    "encoding/json"
    "fmt"
    "net/http"
    "time"
)

const baseURL = "https://api.swapapi.dev"

type Client struct {
    http       *http.Client
    maxRetries int
}

func NewClient() *Client {
    return &Client{
        http:       &http.Client{Timeout: 15 * time.Second},
        maxRetries: 3,
    }
}
Enter fullscreen mode Exit fullscreen mode

The 15-second timeout matches the API's typical 1-5 second response time with headroom for complex multi-hop routes.

Step 3: Implement the Swap Quote Method

Now add the method that calls the swap endpoint. This is the core of your cross-chain swap Go integration. The method constructs the URL, fires the GET request, and parses the response into your typed structs.

func (c *Client) GetSwapQuote(
    chainID int,
    tokenIn string,
    tokenOut string,
    amount string,
    sender string,
    maxSlippage float64,
) (*SwapResponse, error) {
    url := fmt.Sprintf(
        "%s/v1/swap/%d?tokenIn=%s&tokenOut=%s&amount=%s&sender=%s&maxSlippage=%f",
        baseURL, chainID, tokenIn, tokenOut, amount, sender, maxSlippage,
    )

    var lastErr error
    for attempt := 0; attempt <= c.maxRetries; attempt++ {
        if attempt > 0 {
            time.Sleep(time.Duration(attempt*2) * time.Second)
        }

        resp, err := c.http.Get(url)
        if err != nil {
            lastErr = err
            continue
        }
        defer resp.Body.Close()

        var result SwapResponse
        if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
            return nil, fmt.Errorf("decode error: %w", err)
        }

        if resp.StatusCode == 502 {
            lastErr = fmt.Errorf("upstream error: %s", result.Error.Message)
            continue
        }

        return &result, nil
    }

    return nil, fmt.Errorf("max retries exceeded: %w", lastErr)
}
Enter fullscreen mode Exit fullscreen mode

Key details: the retry loop only triggers on 502 (UPSTREAM_ERROR). Client errors like 400 (INVALID_PARAMS) return immediately since retrying won't help. The exponential backoff (2s, 4s, 6s) stays within the API's recommended 2-5 second window.

Step 4: Handle All Response Statuses

A production swap service must handle three distinct statuses. DEX trading volume rose ~37% in 2025 with monthly averages around $412 billion, so your service will encounter all three statuses regularly under real load.

func ProcessSwapResult(resp *SwapResponse) error {
    if !resp.Success {
        return fmt.Errorf("API error [%s]: %s",
            resp.Error.Code, resp.Error.Message)
    }

    switch resp.Data.Status {
    case "Successful":
        fmt.Printf("Swap ready: %s %s -> %s %s\n",
            resp.Data.AmountIn, resp.Data.TokenFrom.Symbol,
            resp.Data.ExpectedAmountOut, resp.Data.TokenTo.Symbol)
        fmt.Printf("Price impact: %.2f%%\n",
            resp.Data.PriceImpact*100)

        if resp.Data.PriceImpact < -0.05 {
            return fmt.Errorf("price impact too high: %.2f%%",
                resp.Data.PriceImpact*100)
        }

        return nil

    case "Partial":
        fmt.Printf("Partial fill: only %s of requested amount\n",
            resp.Data.AmountIn)
        return nil

    case "NoRoute":
        return fmt.Errorf("no route found for this pair")

    default:
        return fmt.Errorf("unknown status: %s", resp.Data.Status)
    }
}
Enter fullscreen mode Exit fullscreen mode

The price impact check at -5% is a safety guard the API docs recommend. Anything beyond that threshold usually indicates low liquidity, and the transaction could result in significant losses.

Step 5: Query Multiple Chains in Parallel

This is where Go shines. Goroutines make it trivial to query swap quotes across multiple chains simultaneously. With 46 supported EVM chains, you can find the best rate for a token pair by checking several networks at once.

Here's a comparison of supported chains relevant to most cross-chain swap scenarios:

Chain Chain ID Native Token USDC Decimals
Ethereum 1 ETH 6
Arbitrum 42161 ETH 6
Base 8453 ETH 6
Polygon 137 POL 6
BSC 56 BNB 18
Optimism 10 ETH 6

Watch out for BSC. USDC and USDT use 18 decimals on BSC, not 6 like on every other major chain. Getting this wrong means your amount will be off by 12 orders of magnitude.

type ChainQuote struct {
    ChainID int
    Resp    *SwapResponse
    Err     error
}

func (c *Client) GetBestQuote(
    chainIDs []int,
    tokenIns map[int]string,
    tokenOuts map[int]string,
    amount string,
    sender string,
) []ChainQuote {
    results := make(chan ChainQuote, len(chainIDs))

    for _, id := range chainIDs {
        go func(chainID int) {
            resp, err := c.GetSwapQuote(
                chainID,
                tokenIns[chainID],
                tokenOuts[chainID],
                amount,
                sender,
                0.005,
            )
            results <- ChainQuote{chainID, resp, err}
        }(id)
    }

    quotes := make([]ChainQuote, 0, len(chainIDs))
    for range chainIDs {
        quotes = append(quotes, <-results)
    }
    return quotes
}
Enter fullscreen mode Exit fullscreen mode

Go's goroutines and channels map perfectly to this fan-out pattern. Go is used by 5.8 million developers globally, and concurrent API calls are one of the most common patterns in Go microservices. Each chain query runs in its own goroutine, and the channel collects results as they arrive.

Step 6: Wire It Into an HTTP Handler

If you're exposing this as an internal microservice endpoint, wrap it in an http.HandlerFunc. This gives other services in your stack a single place to request swap quotes.

func SwapHandler(client *Client) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        chainID := 42161
        tokenIn := "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
        tokenOut := "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"
        amount := "1000000000000000000"
        sender := r.URL.Query().Get("sender")

        resp, err := client.GetSwapQuote(
            chainID, tokenIn, tokenOut, amount, sender, 0.005,
        )
        if err != nil {
            http.Error(w, err.Error(), http.StatusBadGateway)
            return
        }

        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(resp)
    }
}
Enter fullscreen mode Exit fullscreen mode

In a real service, you'd parse chainID, tokenIn, tokenOut, and amount from query parameters or a request body. The example hardcodes a 1 ETH to USDC swap on Arbitrum for clarity.

Try It Yourself

Before writing Go code, verify the API works with a quick curl:

curl "https://api.swapapi.dev/v1/swap/42161?tokenIn=0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE&tokenOut=0xaf88d065e77c8cC2239327C5EDb3A432268e5831&amount=1000000000000000000&sender=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
Enter fullscreen mode Exit fullscreen mode

Try the same swap on Base:

curl "https://api.swapapi.dev/v1/swap/8453?tokenIn=0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE&tokenOut=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&amount=1000000000000000000&sender=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
Enter fullscreen mode Exit fullscreen mode

And on Polygon (native POL to USDC):

curl "https://api.swapapi.dev/v1/swap/137?tokenIn=0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE&tokenOut=0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359&amount=1000000000000000000&sender=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
Enter fullscreen mode Exit fullscreen mode

All three return the same response envelope with executable tx data.

Step 7: Add Pre-Flight Safety Checks

Never submit swap transactions blindly. The API docs outline a pre-flight checklist: estimate gas, simulate with eth_call, and check token approvals. Here's a helper that validates the response before your service forwards the transaction.

func ValidateSwap(resp *SwapResponse) error {
    if !resp.Success || resp.Data == nil {
        return fmt.Errorf("swap not successful")
    }
    if resp.Data.Status != "Successful" {
        return fmt.Errorf("status: %s", resp.Data.Status)
    }
    if resp.Data.PriceImpact < -0.05 {
        return fmt.Errorf("price impact %.2f%% exceeds threshold",
            resp.Data.PriceImpact*100)
    }
    if resp.Data.Tx == nil {
        return fmt.Errorf("no transaction data")
    }
    return nil
}
Enter fullscreen mode Exit fullscreen mode

After validation passes, use the returned rpcUrls as a fallback list for gas estimation and eth_call simulation. The API provides up to 5 ranked public RPCs per chain, so you don't need to maintain your own RPC list. Submit the transaction within 30 seconds of receiving the quote -- the calldata includes a deadline.

Step 8: Put It All Together

Here's the complete main.go that ties everything together as a runnable service:

package main

import (
    "fmt"
    "log"
)

func main() {
    client := swap.NewClient()

    resp, err := client.GetSwapQuote(
        1,
        "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
        "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
        "1000000000000000000",
        "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
        0.005,
    )
    if err != nil {
        log.Fatal(err)
    }

    if err := swap.ProcessSwapResult(resp); err != nil {
        log.Fatal(err)
    }

    fmt.Printf("TX to: %s\n", resp.Data.Tx.To)
    fmt.Printf("TX value: %s wei\n", resp.Data.Tx.Value)
    fmt.Printf("RPC: %s\n", resp.Data.RpcURL)
}
Enter fullscreen mode Exit fullscreen mode

This fetches a quote for 1 ETH to USDC on Ethereum mainnet (chain ID 1), validates the response, and prints the transaction details. In production, you'd forward resp.Data.Tx to your signing service for on-chain execution.

Frequently Asked Questions

How do I add a cross chain swap to a Go microservice?

Use the net/http package to call a DEX aggregator API like swapapi.dev. Send a GET request with the chain ID, token addresses, amount, and sender address. The API returns executable transaction calldata that your service can forward to a signing wallet. No SDK or API key needed.

What chains are supported for token swaps?

The swapapi.dev API supports 46 EVM chains including Ethereum, Arbitrum, Base, Polygon, BSC, Optimism, Avalanche, Linea, Scroll, zkSync Era, Blast, Mantle, Sonic, Berachain, Monad, and MegaETH. Each chain is identified by its standard chain ID passed as a path parameter.

Do I need an API key for programmatic swaps?

No. swapapi.dev is free and requires no API keys, no authentication, and no account registration. The rate limit is approximately 30 requests per minute per IP, which is sufficient for most microservice workloads.

How do I handle partial fills and failed routes?

Check the data.status field in the response. Successful means a full route was found. Partial means only part of your amount can be filled -- the response includes adjusted amountIn and expectedAmountOut values. NoRoute means no route exists for that pair. All three return HTTP 200, so always check status rather than the HTTP code.

Why are BSC token decimals different?

USDC and USDT on BSC use 18 decimals instead of the usual 6. This is a deployment-level difference, not a bug. If you pass the wrong decimal count, your swap amount will be off by a factor of 10^12. Always verify token decimals per chain using the API's response tokenFrom.decimals and tokenTo.decimals fields.

Get Started

The full API requires one GET request and returns everything you need to execute a swap on-chain:

Try it right now -- swap 1 ETH for USDC on Ethereum:

curl "https://api.swapapi.dev/v1/swap/1?tokenIn=0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE&tokenOut=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48&amount=1000000000000000000&sender=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
Enter fullscreen mode Exit fullscreen mode

No signup, no keys, no SDK. Just a GET request and production-ready swap calldata for your Go microservice.

Top comments (0)