> For the complete documentation index, see [llms.txt](https://docs-swap.crypto-chief.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs-swap.crypto-chief.com/guides/first-swap.md).

# Your First Swap

## Your First Swap

A complete script: quote 1,000 USDC to ETH on Ethereum, build the transactions, sign and send them with ethers v6. Fill in three environment variables and run it with Node.

You need:

* `CC_API_KEY` — your project UUID, the Merchant ID from the Dashboard ([Authentication](/getting-started/authentication.md); it is in the URL, so keep this script server-side);
* `CC_WALLET_KEY` — the private key of **your wallet** holding the USDC (the API never sees it);
* `ETH_RPC_URL` — any Ethereum JSON-RPC endpoint, for example your RPC Gateway endpoint with the same project key.

```javascript
import { JsonRpcProvider, Wallet } from "ethers";

const API_KEY = process.env.CC_API_KEY;
const PRIVATE_KEY = process.env.CC_WALLET_KEY;
const RPC_URL = process.env.ETH_RPC_URL;

// POST an operation and return the parsed JSON plus the HTTP status.
// (The plain version is in Authentication; this one adds retries for
// availability answers — 429, 502, 503, 504.)
async function swap(operation, body) {
  for (let attempt = 0; ; attempt++) {
    const res = await fetch(`https://swap-api.crypto-chief.com/${API_KEY}/v1/${operation}`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body ?? {}),
    });
    const data = await res.json().catch(() => ({}));
    const out = { status: res.status, ...data };
    const retriable = [429, 502, 503, 504].includes(out.status);
    if (!retriable || attempt >= 3) return out;
    await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt)); // 1 s, 2 s, 4 s
  }
}

// What a step's `transaction` carries → what ethers v6 wants.
// ethers takes `gasLimit` (not `gas`) and a numeric `type`; quantities as bigint.
function toEthers(tx) {
  const out = {
    to: tx.to,
    data: tx.data,
    value: BigInt(tx.value),
    chainId: BigInt(tx.chainId),
    type: Number(tx.type),
  };
  if (tx.gas !== undefined) out.gasLimit = BigInt(tx.gas);
  if (tx.maxFeePerGas !== undefined) {
    out.maxFeePerGas = BigInt(tx.maxFeePerGas);
    out.maxPriorityFeePerGas = BigInt(tx.maxPriorityFeePerGas);
  } else {
    out.gasPrice = BigInt(tx.gasPrice); // legacy type 0x0
  }
  return out;
}

const wallet = new Wallet(PRIVATE_KEY, new JsonRpcProvider(RPC_URL));

// The swap: 1000 USDC → ETH on Ethereum.
const params = {
  network: "ETH_MAINNET",
  token_in: "USDC",
  token_out: "NATIVE",
  amount_in: "1000000000", // 1000 USDC, base units
  from_address: wallet.address,
  slippage_bps: 50,
};

// 1. Quote.
const quote = await swap("quote", params);
if (quote.error) throw new Error(`${quote.status} ${quote.error}: ${quote.message}`);
console.log(`Expecting ${quote.amount_out_human} ETH, at least ${quote.amount_out_min_human}`);
console.log(`Route: ${quote.executor.id}, risk ${quote.risk.level} (${quote.risk.score})`);
for (const w of quote.warnings ?? []) console.warn("quote:", w);

// 2. Build, pinned to the quote.
let build = await swap("build", { ...params, quote_id: quote.quote_id });
if (build.error) throw new Error(`${build.status} ${build.error}: ${build.message}`);
for (const w of build.warnings ?? []) console.warn("build:", w);
if (BigInt(build.amount_out_min) < BigInt(quote.amount_out_min)) {
  // Fresh state gave a lower guaranteed minimum than the quote. Decide: accept it, or stop.
  throw new Error(`minimum fell: ${quote.amount_out_min_human} -> ${build.amount_out_min_human} ETH`);
}

// 3. Execute the steps in order; rebuild when a step says so.
let steps = build.steps;
let simulation = build.simulation;
let executedApprove = false;
let i = 0;
while (i < steps.length) {
  const step = steps[i];

  if (step.rebuild_after_previous && i > 0) {
    // Do not sign this step. Rebuild with the same parameters and continue
    // with the new response — the steps already executed are not in it.
    // The quote is past its 30 seconds by now: let the build search afresh.
    build = await swap("build", params);
    if (build.error) throw new Error(`${build.status} ${build.error}: ${build.message}`);
    for (const w of build.warnings ?? []) console.warn("build:", w);
    steps = build.steps;
    simulation = build.simulation;
    i = 0;
    continue;
  }

  if (step.kind === "approve" && executedApprove) {
    // The rebuild still asks for an approval: it is not visible yet. Do not approve twice.
    throw new Error("approval not visible yet: rebuild in a few seconds");
  }

  if (step.kind === "swap" && simulation.status !== "passed") {
    // Strict behaviour: never sign a swap that was not simulated clean.
    throw new Error(`simulation ${simulation.status}: ${simulation.reason ?? "no reason given"}`);
  }

  const sent = await wallet.sendTransaction(toEthers(step.transaction));
  console.log(`${step.kind} sent: ${sent.hash}`);
  const receipt = await sent.wait();
  if (!receipt || receipt.status !== 1) {
    throw new Error(`${step.kind} failed on chain: ${sent.hash}`);
  }
  console.log(`${step.kind} confirmed`);
  if (step.kind === "approve") executedApprove = true;
  i++;
}

console.log("Swap complete.");
```

## What the script does

**Quotes first.** The quote is 1,000 credits against the build's 10,000 and tells you the output, the minimum and the risk before anything is built. It lives 30 seconds.

**Builds pinned to the quote.** `quote_id` keeps the quote's route and executor while they are still best ([the rule](/getting-started/how-a-swap-works.md#quote-vs-build)). The rebuild after the approve is not pinned: the quote has expired by then.

**Stops when the minimum fell.** A build's `amount_out_min` comes from fresh state and can be below the quote's. The script refuses to continue in that case; decide what your product does there.

**Executes the steps in order, and rebuilds when told to.** The first build of a token swap is typically `approve` + `swap` with `rebuild_after_previous: true` on the swap. The script signs the approve, waits for the receipt, rebuilds, and gets back one `swap` step, measured and simulated, which it signs and sends. If the rebuild still returns an `approve`, the approval is not visible yet; the script stops instead of approving twice.

**Refuses to sign an unsimulated swap.** When the script is about to sign a `swap` step, the build that produced it has simulated it. `simulation.status: "passed"` is the strict condition ([Simulation](/getting-started/how-a-swap-works.md#simulation)).

**Retries what a retry fixes.** `429`, `502`, `503` and `504` are retried with backoff, starting at one second. A `4xx` is thrown — retrying it unchanged fixes nothing. [Errors](/getting-started/errors.md) has the full table.

## The ethers v6 mapping

The `transaction` of an EVM step is a JSON-RPC `eth_sendTransaction` object, which ethers v6 does **not** take as is:

| The API sends                                                                             | ethers v6 wants                    |
| ----------------------------------------------------------------------------------------- | ---------------------------------- |
| `gas` (only when measured)                                                                | `gasLimit`                         |
| `type: "0x2"` / `"0x0"` (hex string)                                                      | `type: 2` / `0` (number)           |
| `value`, `chainId`, `maxFeePerGas`, `maxPriorityFeePerGas`, `gasPrice` (`0x` hex strings) | `bigint` — `BigInt(tx.value)` etc. |

A browser wallet takes the object unchanged: `window.ethereum.request({method: "eth_sendTransaction", params: [tx]})`. viem and the other networks are in [Signing on Each Network](/guides/signing.md).

## Run it on a testnet

Same script, three changes — Ethereum Sepolia (`ETH_SEPOLIA`, chain id 11155111), its USDC contract, and a smaller amount:

```javascript
const params = {
  network: "ETH_SEPOLIA",
  token_in: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", // USDC on Sepolia
  token_out: "NATIVE",
  amount_in: "1000000", // 1 USDC
  from_address: wallet.address,
  slippage_bps: 50,
};
```

`ETH_RPC_URL` points at a Sepolia endpoint, and the wallet needs testnet ETH and USDC. Other enabled testnets are listed in [Networks and Tokens](/getting-started/networks-and-tokens.md#network-codes).

## If something goes wrong

| You saw                                                                                                     | What it means                                                                                                                       |
| ----------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `422 NO_ROUTE` / `INSUFFICIENT_LIQUIDITY`                                                                   | No route for the pair or the amount — try a smaller amount or another pair                                                          |
| `422 BALANCE_INSUFFICIENT`                                                                                  | The wallet cannot cover the input plus the network fees                                                                             |
| `422 SIMULATION_FAILED`                                                                                     | The swap would revert on chain right now — no steps were issued                                                                     |
| `429 RATE_LIMITED`                                                                                          | Slow down: your project's request limit, or more than 64 calls in progress. Retry after about one second                            |
| `503 STATE_STALE` / `STATE_INCOMPLETE` / `EXECUTOR_UNAVAILABLE` / `RPC_UNAVAILABLE` / `NETWORK_UNAVAILABLE` | Availability — retry shortly                                                                                                        |
| A step's transaction reverted                                                                               | The price moved past your slippage, or the deadline passed — rebuild and rerun. The gas of a failed transaction is spent either way |
