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
senderparameter (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}
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"`
}
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,
}
}
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)
}
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)
}
}
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
}
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)
}
}
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"
Try the same swap on Base:
curl "https://api.swapapi.dev/v1/swap/8453?tokenIn=0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE&tokenOut=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&amount=1000000000000000000&sender=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
And on Polygon (native POL to USDC):
curl "https://api.swapapi.dev/v1/swap/137?tokenIn=0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE&tokenOut=0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359&amount=1000000000000000000&sender=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
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
}
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)
}
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:
-
API base URL:
https://api.swapapi.dev - OpenAPI spec: https://swapapi.dev/openapi.json
- Documentation: https://swapapi.dev
- Supported chains: 46 EVM networks
- Authentication: None required
- Rate limit: ~30 requests/minute per IP
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"
No signup, no keys, no SDK. Just a GET request and production-ready swap calldata for your Go microservice.
Top comments (0)