# EOA Swap example

### EOA Swap flow

A standard wallet swap (MetaMask, WalletConnect, or any EOA signer) in six steps:

1. Fetch supported networks and tokens
2. Find a route
3. Approve token spending (ERC-20 only)
4. Send swap transaction
5. Extract requestId from receipt
6. Poll for completion

#### Step 1: Get Networks and Tokens

```
GET /networks
```

Returns an object keyed by network name. Each entry contains the chain configuration and supported tokens.

```json
{
  "sepolia": {
    "name": "sepolia",
    "chainId": 11155111,
    "router": "0x...",
    "tokens": [
      {
        "chainId": 11155111,
        "address": "0x...",
        "name": "USD Coin",
        "symbol": "USDC",
        "decimals": 6,
        "tags": ["erc20", "stable"],
        "permit": false
      }
    ]
  }
}
```

Use `chainId` and token `address` values from this response when building a scan request.

#### Step 2: Find a Route

```
POST /routing/scan/stream
```

Request body:

```json
{
  "from": "0xYOUR_WALLET_ADDRESS",
  "recipient": "0xRECIPIENT_ADDRESS",
  "params": {
    "tokenIn": "0xTOKEN_IN",
    "amountIn": "1000000",
    "chainIdIn": 11155111,
    "tokenOut": "0xTOKEN_OUT",
    "chainIdOut": 64165
  },
  "slippage": 1
}
```

| Field               | Type      | Required | Description                                                |
| ------------------- | --------- | -------- | ---------------------------------------------------------- |
| `from`              | string    | Yes      | Sender address                                             |
| `recipient`         | string    | No       | Recipient address (defaults to sender)                     |
| `params.tokenIn`    | string    | Yes      | Input token address                                        |
| `params.amountIn`   | string    | Yes      | Amount in smallest token units                             |
| `params.chainIdIn`  | number    | Yes      | Source chain ID                                            |
| `params.tokenOut`   | string    | Yes      | Output token address                                       |
| `params.chainIdOut` | number    | Yes      | Destination chain ID                                       |
| `slippage`          | number    | Yes      | Slippage tolerance in percent                              |
| `feeShareBps`       | number    | No       | Partner fee in basis points (100 = 1%)                     |
| `providers`         | string\[] | No       | Filter by provider: `"cross-curve"`, `"rubic"`, `"bungee"` |

The response is NDJSON — each line is a separate JSON object:

**Success line:**

```json
{ "route": [...], "simulation": { /* route object — see Routing Response */ } }
```

**Error line (route could not be evaluated):**

```json
{ "route": [...], "error": "error message" }
```

Collect the `simulation` objects from success lines. Routes arrive in arbitrary order, so sort by `amountOut` descending to find the best route and pass it to the next step.

> You can also use `POST /routing/scan` which returns all routes as a single JSON array. The request body is the same. Each array item has the same shape as the `simulation` object above.

#### Step 3: Build the Transaction

```
POST /tx/create
```

Request body:

```json
{
  "from": "0xYOUR_WALLET_ADDRESS",
  "recipient": "0xRECIPIENT_ADDRESS",
  "routing": { },
  "buildCalldata": true
}
```

| Field           | Type    | Required | Description                                                                              |
| --------------- | ------- | -------- | ---------------------------------------------------------------------------------------- |
| `from`          | string  | Yes      | Sender address                                                                           |
| `recipient`     | string  | Yes      | Recipient address                                                                        |
| `routing`       | object  | Yes      | The `simulation` object from `/routing/scan/stream` (or array item from `/routing/scan`) |
| `buildCalldata` | boolean | No       | When `true`, returns encoded calldata ready to send                                      |

Pass the entire route object as `routing`. Do not modify or cherry-pick fields from it.

**Response when `buildCalldata` is `true`:**

```json
{
  "to": "0xROUTER_ADDRESS",
  "data": "0xENCODED_CALLDATA...",
  "value": "0"
}
```

**Response when `buildCalldata` is `false` or omitted:**

```json
{
  "to": "0xROUTER_ADDRESS",
  "abi": "[...]",
  "args": [...],
  "value": "0"
}
```

#### Step 3: Approve Token Spending (ERC-20 only)

If the input token is an ERC-20 (not the native token), approve the router contract to spend your tokens before sending the swap transaction. The router address is the `to` field from the `/tx/create` response.

```js
// Check current allowance
const allowance = await tokenContract.allowance(walletAddress, tx.to);

if (allowance < BigInt(route.amountIn)) {
  await tokenContract.approve(tx.to, route.amountIn);
}
```

Skip this step if the input token is the chain's native token (ETH, MATIC, etc.) — native tokens do not require approval.

> For ERC-4337 and EIP-7702 wallets, the approval is already included in the `calls[]` array returned by `/tx/create`. This step applies only to EOA transactions.

#### Step 4: Send Swap Transaction

Send the transaction on-chain using your wallet or signer.

#### Step 5: Extract requestId

After the transaction is confirmed, extract the `requestId` from the `ComplexOpProcessed` event in the receipt:

```js
const COMPLEX_OP_TOPIC = "0x830adbcf80ee865e0f0883ad52e813fdbf061b0216b724694a2b4e06708d243c";
const log = receipt.logs.find((l) => l.topics[0] === COMPLEX_OP_TOPIC);
const requestId = log.topics[2]; // currentRequestId (bytes32)
```

#### Step 6: Poll for Completion

Poll `GET /transaction/{requestId}` until the status reaches a final state.

See Transaction Tracking for response format and polling recommendations.

#### Full Example (JavaScript + ethers.js)

```js
import { ethers } from "ethers";

const API = "https://api.crosscurve.fi";
const API_KEY = "YOUR_API_KEY";

const provider = new ethers.JsonRpcProvider("https://YOUR_RPC_URL");
const signer = new ethers.Wallet("0xYOUR_PRIVATE_KEY", provider);
const WALLET_ADDRESS = signer.address;

const TOKEN_IN = "0xTOKEN_IN";
const TOKEN_OUT = "0xTOKEN_OUT";
const CHAIN_ID_IN = 11155111;
const CHAIN_ID_OUT = 64165;
const AMOUNT_IN = "1000000"; // in smallest token units

const ERC20_ABI = [
  "function approve(address spender, uint256 amount) returns (bool)",
  "function allowance(address owner, address spender) view returns (uint256)",
];

// Helper: parse NDJSON stream into an array of route objects
async function scanRoutes(body) {
  const response = await fetch(`${API}/routing/scan/stream`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Accept": "application/x-ndjson",
      "api-key": API_KEY,
    },
    body: JSON.stringify(body),
  });

  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  const routes = [];
  let buffer = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });
    let idx;
    while ((idx = buffer.indexOf("\n")) >= 0) {
      const line = buffer.slice(0, idx).trim();
      buffer = buffer.slice(idx + 1);
      if (!line) continue;
      const parsed = JSON.parse(line);
      if (parsed.simulation) routes.push(parsed.simulation);
    }
  }
  if (buffer.trim()) {
    const parsed = JSON.parse(buffer);
    if (parsed.simulation) routes.push(parsed.simulation);
  }

  return routes;
}

// Step 1: Find a route
const routes = await scanRoutes({
  from: WALLET_ADDRESS,
  recipient: WALLET_ADDRESS,
  params: {
    tokenIn: TOKEN_IN,
    amountIn: AMOUNT_IN,
    chainIdIn: CHAIN_ID_IN,
    tokenOut: TOKEN_OUT,
    chainIdOut: CHAIN_ID_OUT,
  },
  slippage: 1,
});
routes.sort((a, b) => {
  if (BigInt(a.amountOut) > BigInt(b.amountOut)) return -1;
  if (BigInt(a.amountOut) < BigInt(b.amountOut)) return 1;
  return 0;
});
const route = routes[0];

// Step 2: Build the transaction
const txRes = await fetch(`${API}/tx/create`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    from: WALLET_ADDRESS,
    recipient: WALLET_ADDRESS,
    routing: route,
    buildCalldata: true,
  }),
});
const tx = await txRes.json();

// Step 3: Approve router if input is ERC-20 (skip for native token)
const tokenContract = new ethers.Contract(TOKEN_IN, ERC20_ABI, signer);
const allowance = await tokenContract.allowance(WALLET_ADDRESS, tx.to);
if (allowance < BigInt(route.amountIn)) {
  const approveTx = await tokenContract.approve(tx.to, route.amountIn);
  await approveTx.wait();
}

// Step 4: Send swap on-chain
const swapTx = await signer.sendTransaction({
  to: tx.to,
  data: tx.data,
  value: tx.value,
});
const receipt = await swapTx.wait();

// Step 5: Extract requestId from receipt
const COMPLEX_OP_TOPIC = "0x830adbcf80ee865e0f0883ad52e813fdbf061b0216b724694a2b4e06708d243c";
const log = receipt.logs.find((l) => l.topics[0] === COMPLEX_OP_TOPIC);
const requestId = log.topics[2]; // currentRequestId (bytes32)

// Step 6: Poll for completion
async function pollStatus(requestId) {
  while (true) {
    const res = await fetch(`${API}/transaction/${requestId}`);
    const status = await res.json();

    console.log("Status:", status.status);

    if (["completed", "failed", "reverted", "canceled"].includes(status.status)) {
      return status;
    }

    await new Promise((r) => setTimeout(r, 12000));
  }
}

const result = await pollStatus(requestId);
console.log("Final status:", result.status);
```

#### Full Example (TypeScript + viem)

```ts
import { createWalletClient, createPublicClient, http, parseAbi, parseEventLogs } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { sepolia } from "viem/chains";

const API = "https://api.crosscurve.fi";
const API_KEY = "YOUR_API_KEY";

const TOKEN_IN = "0xTOKEN_IN";
const TOKEN_OUT = "0xTOKEN_OUT";
const CHAIN_ID_IN = 11155111;
const CHAIN_ID_OUT = 64165;
const AMOUNT_IN = "1000000";

const account = privateKeyToAccount("0xYOUR_PRIVATE_KEY");
const publicClient = createPublicClient({ chain: sepolia, transport: http() });
const walletClient = createWalletClient({
  account,
  chain: sepolia,
  transport: http(),
});

// Helper: parse NDJSON stream into an array of route objects
async function scanRoutes(body: Record<string, unknown>) {
  const response = await fetch(`${API}/routing/scan/stream`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Accept": "application/x-ndjson",
      "api-key": API_KEY,
    },
    body: JSON.stringify(body),
  });

  const reader = response.body!.getReader();
  const decoder = new TextDecoder();
  const routes: unknown[] = [];
  let buffer = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });
    let idx: number;
    while ((idx = buffer.indexOf("\n")) >= 0) {
      const line = buffer.slice(0, idx).trim();
      buffer = buffer.slice(idx + 1);
      if (!line) continue;
      const parsed = JSON.parse(line);
      if (parsed.simulation) routes.push(parsed.simulation);
    }
  }
  if (buffer.trim()) {
    const parsed = JSON.parse(buffer);
    if (parsed.simulation) routes.push(parsed.simulation);
  }

  return routes;
}

// Step 1: Find a route
const routes = await scanRoutes({
  from: account.address,
  recipient: account.address,
  params: {
    tokenIn: TOKEN_IN,
    amountIn: AMOUNT_IN,
    chainIdIn: CHAIN_ID_IN,
    tokenOut: TOKEN_OUT,
    chainIdOut: CHAIN_ID_OUT,
  },
  slippage: 1,
});
routes.sort((a: any, b: any) => {
  if (BigInt(a.amountOut) > BigInt(b.amountOut)) return -1;
  if (BigInt(a.amountOut) < BigInt(b.amountOut)) return 1;
  return 0;
});
const route = routes[0];

// Step 2: Build the transaction
const txRes = await fetch(`${API}/tx/create`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    from: account.address,
    recipient: account.address,
    routing: route,
    buildCalldata: true,
  }),
});
const tx: { to: string; data: string; value: string } = await txRes.json();

// Step 3: Approve router if input is ERC-20 (skip for native token)
const erc20Abi = parseAbi([
  "function approve(address,uint256) returns (bool)",
  "function allowance(address,address) view returns (uint256)",
]);
const allowance = await publicClient.readContract({
  address: TOKEN_IN as `0x${string}`,
  abi: erc20Abi,
  functionName: "allowance",
  args: [account.address, tx.to as `0x${string}`],
});
if (allowance < BigInt(route.amountIn)) {
  await walletClient.writeContract({
    address: TOKEN_IN as `0x${string}`,
    abi: erc20Abi,
    functionName: "approve",
    args: [tx.to as `0x${string}`, BigInt(route.amountIn)],
  });
}

// Step 4: Send swap on-chain
const hash = await walletClient.sendTransaction({
  to: tx.to as `0x${string}`,
  data: tx.data as `0x${string}`,
  value: BigInt(tx.value),
});

console.log("Transaction sent:", hash);

// Step 5: Extract requestId from receipt
const receipt = await publicClient.waitForTransactionReceipt({ hash });
const complexOpAbi = parseAbi([
  "event ComplexOpProcessed(uint64 indexed currentChainId, bytes32 indexed currentRequestId, uint64 indexed nextChainId, bytes32 nextRequestId, uint8 result, uint8 lastOp)",
]);
const events = parseEventLogs({ abi: complexOpAbi, logs: receipt.logs, eventName: "ComplexOpProcessed" });
const requestId = events[0].args.currentRequestId;

// Step 6: Poll for completion
async function pollStatus(requestId: string) {
  while (true) {
    const res = await fetch(`${API}/transaction/${requestId}`);
    const status = await res.json();

    console.log("Status:", status.status);

    if (["completed", "failed", "reverted", "canceled"].includes(status.status)) {
      return status;
    }

    await new Promise((r) => setTimeout(r, 12000));
  }
}
```

***
