# Create a swap

`POST /v1/swaps`

Returns a firm quote for an account, with the steps that execute it. The account signs and sends each step with its own wallet. Circuitry never holds keys or funds.

Before it returns, Circuitry checks the account's balances and allowances, adds only the approvals the swap needs, and simulates every step against the latest block. Circuitry returns the swap only if the simulation spends at most `input.maximum` and receives at least `output.minimum`.

## Request

```bash
curl https://api.circuitry.company/v1/swaps \
  -H "Authorization: Bearer $CIRCUITRY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "account": "0x1111111111111111111111111111111111111111",
    "chain": "base",
    "input": { "asset": { "type": "fx", "currency": "USD" }, "amount": "20" },
    "output": { "asset": { "type": "crypto", "symbol": "ETH" } }
  }'
```

The request has the same fields as [Price a swap](/reference/swap-price#request), with two differences:

| Field | Meaning |
| --- | --- |
| `account` | Required. The address that holds the input, receives the output, signs every step, and pays gas. The swap uses only tokens that it holds enough of |
| `accountCanBatch` | Optional. `true` when the account can send several calls as one transaction, such as a smart account. Then the calls come back as one `calls` step. Defaults to `false` |

## Response

```json
{
  "id": "k17aervc9b9sq4ja60esj941ps8gb1r2",
  "object": "swap",
  "status": "quoted",
  "createdAt": "2026-10-15T01:38:19.122Z",
  "expiresAt": "2026-10-15T01:39:19.053Z",
  "account": "0x1111111111111111111111111111111111111111",
  "chain": "base",
  "chainId": 8453,
  "input": {
    "asset": {
      "type": "fx",
      "currency": "USD",
      "token": {
        "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
        "symbol": "USDC",
        "name": "USD Coin",
        "decimals": 6
      }
    },
    "amount": { "value": "20", "raw": "20000000" },
    "maximum": { "value": "20", "raw": "20000000" }
  },
  "output": {
    "asset": {
      "type": "crypto",
      "symbol": "ETH",
      "token": { "address": null, "symbol": "ETH", "name": "Ether", "decimals": 18 }
    },
    "amount": { "value": "0.008265758119362246", "raw": "8265758119362246" },
    "minimum": { "value": "0.008224429328765434", "raw": "8224429328765434" }
  },
  "fees": [
    {
      "type": "network",
      "estimated": true,
      "amount": { "value": "0.00000148086", "raw": "1480860000000" }
    }
  ],
  "route": { "venues": ["uniswap"] },
  "warnings": [],
  "steps": [
    {
      "id": "approve",
      "kind": "transaction",
      "status": "ready",
      "description": "Allow Permit2 to spend 20 USDC",
      "transaction": {
        "chainId": "0x2105",
        "from": "0x1111111111111111111111111111111111111111",
        "to": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
        "data": "0x095ea7b3…",
        "value": "0x0",
        "gas": "0x13575"
      }
    },
    {
      "id": "permit2",
      "kind": "transaction",
      "status": "ready",
      "description": "Allow Uniswap's Universal Router to spend 20 USDC through Permit2",
      "transaction": {
        "chainId": "0x2105",
        "from": "0x1111111111111111111111111111111111111111",
        "to": "0x000000000022D473030F116dDEE9F6B43aC78BA3",
        "data": "0x87517c45…",
        "value": "0x0",
        "gas": "0x10f6f"
      }
    },
    {
      "id": "swap",
      "kind": "transaction",
      "status": "ready",
      "description": "Swap 20 USDC for at least 0.008224429328765434 ETH",
      "transaction": {
        "chainId": "0x2105",
        "from": "0x1111111111111111111111111111111111111111",
        "to": "0xd6145b2D3F379919E8CdEda7B97e37c4b2Ca9c40",
        "data": "0x3593564c…",
        "value": "0x0",
        "gas": "0x2e56b"
      }
    }
  ]
}
```

The response has every field of [a price](/reference/swap-price#response), and these:

| Field | Meaning |
| --- | --- |
| `id` | The swap's ID |
| `object` | Always `swap` |
| `status` | `quoted` when Circuitry creates the swap. See [Statuses](/reference/get-swap#statuses) |
| `createdAt` | When Circuitry created the swap |
| `expiresAt` | When the quote expires, 60 seconds after Circuitry created it. The swap transaction has the same deadline. A transaction that arrives too late reverts, and doesn't fill at an old price |
| `account` | The account that signs every step and receives the output |
| `steps` | What the account sends, in order. See [Steps](#steps) |

## Steps

Send the steps in order. Wait for the receipt of each transaction before you send the next step.

A `transaction` step has `chainId`, `from`, `to`, `data`, `value`, and `gas`, as hex strings, so you can pass it to `eth_sendTransaction`. Your wallet chooses the nonce and the fee. `gas` is the simulated gas plus a margin.

```ts
import { createWalletClient, custom, publicActions } from "viem";
import { base } from "viem/chains";

const wallet = createWalletClient({
  account: swap.account,
  chain: base,
  transport: custom(window.ethereum),
}).extend(publicActions);

for (const step of swap.steps) {
  const hash = await wallet.sendTransaction({
    to: step.transaction.to,
    data: step.transaction.data,
    value: BigInt(step.transaction.value),
    gas: BigInt(step.transaction.gas),
  });
  await wallet.waitForTransactionReceipt({ hash });
}
```

Circuitry follows the swap until it completes. [Retrieve the swap](/reference/get-swap) to see its status and the amounts that actually moved. [Report each transaction](/reference/swap-steps) from your server to update the status immediately.

| Step ID | When it appears | What it does |
| --- | --- | --- |
| `approve` | The input is a token, and the account's allowance for the spender is less than `input.maximum` | Approves exactly `input.maximum`. On a Uniswap route the spender is Permit2. On an Aerodrome route it is Aerodrome's router |
| `permit2` | The route is on Uniswap, and the account's Permit2 allowance for Uniswap's Universal Router is less than `input.maximum` or expires before `expiresAt` | Gives the Universal Router a Permit2 allowance of exactly `input.maximum`, which expires at `expiresAt` |
| `swap` | Always | Swaps through the venue's router. It spends at most `input.maximum`, pays at least `output.minimum` to `account`, and reverts after `expiresAt` |

Approvals are never unlimited. An exact-output swap can spend up to `input.maximum`, so its approval is for that amount, not for `input.amount`.

When the input is ETH, there is no approval, and the swap step sends the ETH as `value`. When the output is ETH, the account receives native ETH.

### Batched steps

With `accountCanBatch` set to `true`, all the calls come back in one step with kind `calls`. Send its `calls` as one transaction, for example with EIP-5792 `wallet_sendCalls`, or your account's own batch call. Circuitry matches the step to that one transaction, so leave `accountCanBatch` unset for a wallet that sends batched calls as separate transactions.

```json
{
  "id": "swap",
  "kind": "calls",
  "status": "ready",
  "description": "Approve and swap 20 USDC for at least 0.008224429328765434 ETH",
  "chainId": "0x2105",
  "from": "0x1111111111111111111111111111111111111111",
  "gas": "0x52a4f",
  "calls": [
    { "to": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "data": "0x095ea7b3…", "value": "0x0" },
    { "to": "0x000000000022D473030F116dDEE9F6B43aC78BA3", "data": "0x87517c45…", "value": "0x0" },
    { "to": "0xd6145b2D3F379919E8CdEda7B97e37c4b2Ca9c40", "data": "0x3593564c…", "value": "0x0" }
  ]
}
```

`gas` is the total for all the calls.

### Contracts

Steps call only these contracts, and the input token for an approval. You can refuse a step whose `to` is any other address.

| Chain | Contract | Address |
| --- | --- | --- |
| Base | Uniswap Universal Router | `0xd6145b2D3F379919E8CdEda7B97e37c4b2Ca9c40` |
| Base | Permit2 | `0x000000000022D473030F116dDEE9F6B43aC78BA3` |
| Base | Aerodrome Router | `0xcF77a3Ba9A5CA399B7c97c74d54e5b1Beb874E43` |
| Base | Aerodrome Slipstream SwapRouter | `0xBE6D8f0d05cC4be24d5167a3eF062215bE6D18a5` |
| Base | Aerodrome Slipstream SwapRouter | `0xcbBb8035cAc7D4B3Ca7aBb74cF7BdF900215Ce0D` |
| Base | Aerodrome Slipstream SwapRouter | `0x698Cb2b6dd822994581fEa6eA4Fc755d1363A92F` |
| Robinhood Chain | Uniswap Universal Router | `0x204FAca1764B154221e35c0d20aBb3c525710498` |
| Robinhood Chain | Permit2 | `0x000000000022D473030F116dDEE9F6B43aC78BA3` |

## Errors

This endpoint returns the errors in [Price a swap](/reference/swap-price#errors), and:

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid_request` | `account` is missing, as well as the reasons a price gives |
| 422 | `insufficient_balance` | The account doesn't hold enough of any token for the input, up to `input.maximum` |
| 422 | `no_route` | No venue trades the pair, or the best route failed in simulation. A token that takes a fee on transfer or blocks transfers fails this way |
| 429 | `rate_limited` | Firm quotes have their own, lower limit. See [Rate limits](/rate-limits) |

Circuitry screens every account against sanctions lists. An account on a list gets `sanctioned_address`.
