> For the complete documentation index, see [llms.txt](https://docs.kumbaya.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kumbaya.xyz/developers/dex-integration/swapping.md).

# Executing swaps

Once you have a quote, you submit a transaction to one of two routers. This page covers when to use which and shows end-to-end calldata flows.

| Router                | When to use                                                       |
| --------------------- | ----------------------------------------------------------------- |
| **`SwapRouter02`**    | Standard V3 swaps. The simplest path.                             |
| **`UniversalRouter`** | Bundling Permit2 + swap, multi-protocol routes, atomic operations |

For Kumbaya mainnet addresses see [**Contract Addresses**](/developers/networks-and-contracts/contract-addresses.md).

## Path 1: SwapRouter02 - exact input single

For a single-pool exact-input swap, this is the minimum viable flow:

```ts
import { createWalletClient, createPublicClient, http, parseUnits, parseAbi } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'

const SWAP_ROUTER_02 = '0xE5BbEF8De2DB447a7432A47EBa58924d94eE470e' // mainnet
const RPC = 'https://mainnet.megaeth.com/rpc'

const account = privateKeyToAccount('0xYOUR_PRIVATE_KEY')
const wallet = createWalletClient({ account, transport: http(RPC) })

const swapAbi = parseAbi([
  'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96) params) external payable returns (uint256 amountOut)',
])

// Step 1 - approve once per (token, router) if not already approved.
// (Skip for ETH; required for any ERC-20.)
// await tokenContract.write.approve([SWAP_ROUTER_02, MAX_UINT256])

// Step 2 - submit the swap.
const txHash = await wallet.writeContract({
  address: SWAP_ROUTER_02,
  abi: swapAbi,
  functionName: 'exactInputSingle',
  args: [{
    tokenIn:  '0xWETH_ADDRESS',
    tokenOut: '0xTOKEN_OUT',
    fee:      3000,
    recipient: account.address,
    amountIn: parseUnits('1', 18),
    amountOutMinimum: minimumOut,    // from your quote × (1 - slippageBps/10000)
    sqrtPriceLimitX96: 0n,
  }],
})
```

**Paying with native ETH:** pass `tokenIn = WETH`, set `value: parseEther('amount')` on the call (pulled from `msg.value`); SwapRouter02 wraps it for you.

**Receiving native ETH out:** set `recipient = address(2)` (the router's `ADDRESS_THIS` constant), then `multicall([exactInputSingle, unwrapWETH9(amountMinimum, userAddress)])` so the router holds the WETH and unwraps it to the user. Sending `recipient = msg.sender` directly gives you WETH (no unwrap).

## Path 2: SwapRouter02 - multi-hop exact input

For routes through more than one pool, use `exactInput` with a packed path:

```ts
import { encodePacked } from 'viem'

// Path: tokenIn -> WETH (3000) -> tokenOut (3000)
const path = encodePacked(
  ['address', 'uint24', 'address', 'uint24', 'address'],
  [tokenInAddress, 3000, WETH, 3000, tokenOutAddress],
)

await wallet.writeContract({
  address: SWAP_ROUTER_02,
  abi: parseAbi([
    'function exactInput((bytes path, address recipient, uint256 amountIn, uint256 amountOutMinimum) params) external payable returns (uint256 amountOut)',
  ]),
  functionName: 'exactInput',
  args: [{ path, recipient: account.address, amountIn, amountOutMinimum: minimumOut }],
})
```

The path is `tokenAddress, fee, tokenAddress, fee, ...` packed - fees go between adjacent token pairs.

## Path 3: Using the Exchange API's calldata

If you got your quote from the Exchange API, the response already contains the calldata. Just submit it.

**For a `GET /api/v1/quote` response** (`methodParameters.calldata`):

```ts
const quote = await fetch(
  'https://exchange.kumbaya.xyz/api/v1/quote?…'
).then(r => r.json())

const txHash = await wallet.sendTransaction({
  to:    quote.methodParameters.to,           // SwapRouter02 or UniversalRouter
  value: BigInt(quote.methodParameters.value),
  data:  quote.methodParameters.calldata,     // lowercase 'd'
})
```

**For a `POST /api/v1/quote` partner response** (`transaction.callData`):

```ts
const quote = await fetch(
  'https://exchange.kumbaya.xyz/api/v1/quote',
  { method: 'POST', headers: { 'x-api-key': KEY, 'content-type': 'application/json' }, body: ... }
).then(r => r.json())

const txHash = await wallet.sendTransaction({
  to:    quote.transaction.to,
  value: BigInt(quote.transaction.value),
  data:  quote.transaction.callData,          // ⚠ camelCase 'D'
})
```

> The two endpoints return different field names (`calldata` vs `callData`). Mind the casing.

This is the lowest-effort production path. Just do the approval first if needed.

## Path 4: UniversalRouter with Permit2

`UniversalRouter` lets you bundle a Permit2 signature + swap into one tx, so you don't need an upfront ERC-20 `approve`. Use [`@kumbaya_xyz/universal-router-sdk`](/developers/sdks/universal-router-sdk.md) to encode the commands:

```ts
import { SwapRouter, RoutePlanner, CommandType } from '@kumbaya_xyz/universal-router-sdk'

// Pseudocode - the SDK exposes high-level helpers that build the right RoutePlanner
// for a Trade object. Use SwapRouter.swapCallParameters({ trade, … }) to get
// `{ calldata, value }` ready to send to the UNIVERSAL_ROUTER address.
```

Use this when:

* You want to skip ERC-20 approvals via Permit2 signatures.
* You're combining swap + transfer + wrap/unwrap atomically.
* Your route spans multiple Uniswap protocols (V2 + V3).

## Recipient sentinels

SwapRouter02 accepts two special recipient values that resolve at execution time:

| `recipient`                   | Resolves to               | Use case                                                                                         |
| ----------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------ |
| `address(1)` (`MSG_SENDER`)   | The original `msg.sender` | Direct send to the user (skip if you set `recipient` to the user explicitly)                     |
| `address(2)` (`ADDRESS_THIS`) | The router itself         | Hold tokens in the router for a follow-up call (`unwrapWETH9`, `sweepToken`, etc.) via multicall |

Source: `swap-router-contracts/contracts/libraries/Constants.sol`. Use these in calldata when chaining operations.

## Approvals

Before any non-ETH swap with `SwapRouter02`, the router needs allowance for `tokenIn`. Common patterns:

* **One-shot max approve** - `approve(SWAP_ROUTER_02, MAX_UINT256)`. Simple, but trusts the router with your full balance.
* **Per-trade approve** - approve exactly the amount you're swapping. Higher gas overhead, lower trust footprint.
* **Permit2** - sign instead of approve. Use `UniversalRouter` for this path.

## Slippage handling

`amountOutMinimum` is your slippage protection. Compute it from your quote:

```ts
const slippageBps = 50n // 0.5%
const minimumOut  = (quotedOut * (10_000n - slippageBps)) / 10_000n
```

If on-chain conditions move price more than this between your quote and execution, the swap reverts and you lose only the gas.

## Common reverts

| Revert reason               | Likely cause                                      |
| --------------------------- | ------------------------------------------------- |
| `Too little received`       | Slippage exceeded - bump tolerance or reduce size |
| `STF` (SafeTransferFrom)    | Token allowance is too low or zero                |
| `LOK`                       | Pool re-entered (extremely rare)                  |
| Plain revert with no reason | Path packed wrong or recipient mismatch           |

## Where to next

* [**Quoting prices**](/developers/dex-integration/quoting.md) - getting the input numbers above
* [**Reading pools and positions**](/developers/dex-integration/reading-pools.md) - checking on-chain state
* [**Quote endpoints (API)**](/developers/apis/exchange-api/quote.md) - the hosted alternative
