> ## Documentation Index
> Fetch the complete documentation index at: https://docs.route.fun/llms.txt
> Use this file to discover all available pages before exploring further.

# prepare a native-ETH swap

> Get a quote and build an unsigned ETH swap with the V2 API.

This example prepares **native ETH → ERC-20** through `provider=route`. It checks the current zero-fee configuration, gets a simulated quote, sets the minimum received and verifies the returned transaction data. It stops before signing or sending anything.

Use the public API at `https://api.route.fun`. For local testing, pass your local API base instead. Install `viem` in your own integration project and save the following module as `prepare-swap.mjs`.

```javascript theme={null}
import { encodeFunctionData, getAddress, parseAbi, zeroAddress } from 'viem';

const chainId = 4663;
const abi = parseAbi([
  'struct Leg { address adapter; address tokenOut; uint24 fee; int24 tickSpacing; address hooks; bool nativePool; }',
  'struct Branch { uint256 amountIn; Leg[] legs; }',
  'function swap(address tokenIn,address tokenOut,uint256 amountIn,uint256 minOut,address recipient,uint256 deadline,Branch[] branches) payable returns(uint256 amountOut)',
]);
const same = (a, b) => getAddress(a) === getAddress(b);

async function json(base, path, options = {}) {
  const response = await fetch(new URL(path, base), {
    ...options,
    signal: AbortSignal.timeout(20000),
    redirect: 'error',
  });
  const body = await response.json();
  if (!response.ok) {
    const error = new Error(body.error?.message ?? `HTTP ${response.status}`);
    error.code = body.error?.code;
    error.retryAfter = response.headers.get('Retry-After');
    error.requestId = body.error?.requestId;
    throw error;
  }
  return body;
}

export async function prepareNativeSwap({
  base = 'https://api.route.fun',
  recipient,
  tokenOut,
  amountIn,
  slippageBps = 50,
}) {
  recipient = getAddress(recipient);
  tokenOut = getAddress(tokenOut);
  if (recipient === zeroAddress || tokenOut === zeroAddress)
    throw new Error('Use a nonzero recipient and ERC-20 output');
  if (!/^[1-9][0-9]*$/.test(amountIn) || BigInt(amountIn) > (1n << 127n) - 1n)
    throw new Error('Input must be a positive base-unit integer within the API limit');
  if (!Number.isInteger(slippageBps) || slippageBps < 0 || slippageBps > 500)
    throw new Error('Slippage must be between 0 and 500 bps');

  const config = await json(base, '/api/v2/config');
  if (config.chainId !== chainId || config.settlementMode !== 'fee-free' ||
      config.protocolFeeBps !== 0 || !config.enabledProviders?.route)
    throw new Error('This recipe requires the enabled fee-free direct V2 release');
  const executor = getAddress(config.tokenOutputExecutors?.route ?? config.executors.route);
  if (executor === zeroAddress) throw new Error('Direct settlement is unavailable');

  const query = new URLSearchParams({
    chainId: String(chainId), tokenIn: 'ETH', tokenOut, amountIn, provider: 'route',
  });
  const quote = await json(base, `/api/v2/quote?${query}`);
  if (quote.chainId !== chainId || quote.provider !== 'route' ||
      !same(quote.tokenIn, zeroAddress) || !same(quote.tokenOut, tokenOut) ||
      quote.amountIn !== amountIn || quote.execution?.status !== 'simulated' ||
      quote.protocolFeeBps !== 0 || quote.feeAmount !== '0' ||
      quote.amountOut !== quote.grossAmountOut)
    throw new Error('Quote does not match the requested fee-free swap');
  const now = () => Math.floor(Date.now() / 1000);
  if (!Number.isSafeInteger(quote.expiresAt) || quote.expiresAt <= now())
    throw new Error('Quote expired; request and review another');
  const minimum = BigInt(quote.execution.amountOut) * BigInt(10000 - slippageBps) / 10000n;
  if (minimum <= 0n) throw new Error('Minimum is too small');
  const deadline = now() + 300;
  const request = {
    chainId, provider: 'route', tokenIn: zeroAddress, tokenOut, amountIn,
    amountOutMinimum: String(minimum), recipient, deadline,
    expiresAt: quote.expiresAt, branches: quote.branches,
  };
  const prepared = await json(base, '/api/v2/swap', {
    method: 'POST', headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(request),
  });
  const expectedData = encodeFunctionData({
    abi, functionName: 'swap',
    args: [zeroAddress, tokenOut, BigInt(amountIn), minimum, recipient, BigInt(deadline),
      quote.branches.map((b) => ({ ...b, amountIn: BigInt(b.amountIn) }))],
  });
  if (prepared.chainId !== chainId || prepared.approval !== null ||
      prepared.simulationRequired !== true || prepared.protocolFeeBps !== 0 ||
      prepared.feeAmount !== '0' || prepared.amountOut !== prepared.grossAmountOut ||
      BigInt(prepared.amountOut) < minimum ||
      !same(prepared.transaction.to, executor) || prepared.transaction.value !== amountIn ||
      prepared.transaction.data.toLowerCase() !== expectedData.toLowerCase() ||
      quote.expiresAt <= now())
    throw new Error('Prepared transaction changed reviewed terms or expired');

  return {
    transaction: prepared.transaction,
    simulationRequired: true,
    review: { chainId, executor, tokenIn: zeroAddress, tokenOut, amountIn,
      recipient, amountOutMinimum: String(minimum), deadline, expiresAt: quote.expiresAt },
    preparedAmountOut: prepared.amountOut,
  };
}
```

Call `prepareNativeSwap` with the connected wallet's public address, an ERC-20 output address and an input such as `"100000000000000"` for `0.0001 ETH`. No private key belongs in this module or on your backend. Do not pass `netAmountOut` as the minimum: it is a gas-cost model, not the contract's token payout.

Do not use WETH as the output of this native-input recipe: ETH↔WETH is wrapping and is not a supported V2 swap pair.

Show the swap details to the user, then follow the [wallet flow](/cookbook/wallet-lifecycle). If the quote expires, get a fresh one and ask the user to review it. Keep their accepted minimum unless they agree to change it.

## Add other swap types

For ERC-20 input, read allowance and handle the returned exact approval before refreshing. For Kyber and 0x, preserve the selected provider and variant, send `branches: []`, resolve the output-specific executor and validate that provider's ABI. For 0x, `txOrigin` must be the signing EOA. The direct ABI in this recipe must not be reused to validate an upstream transaction. See [swap request fields](/api-reference/v2-swap) and the [full schema](/api-reference/openapi).

The zero-fee checks will stop this example when the fee policy changes. Follow the [fee migration recipe](/cookbook/tiered-fees) to update those checks for the new contract and fee fields.
