> For a complete index of Agora Finance documentation, fetch https://docs.agora.finance/llms.txt

# Executing a Swap

> Guide to executing token swaps through the Pair contract with fixed-input and fixed-output options

The following pages will walk you through how to execute both a fixed-input and fixed-output swap on Ethereum's testnet Sepolia.
They'll consistently exchange **AUSD** for **CTK** since this is our main pair on testnets.  You can also adapt the scripts to do the inverse for testing purposes.

# Prerequisites

## Wallet Client and Getting Gas

Just like all of the other guides, be sure that you have followed the steps [here](/instant-settlement/guides/quick-start#setting-up-a-viem-wallet-client) to configure your Wallet Client.
If this has been done correctly you should be able to import your client at the top of your script:

```tsx
import { client } from "walletClient";
```

You also need to make sure that your wallet does have Sepolia ETH for any gas fees on testnet.
The instructions for using a faucet to get a small amount of Sepolia are also found in [our quick-start guide](/instant-settlement/guides/quick-start#fund-your-test-wallet).

## Testnet AUSD

In order to be able to initiate a swap you must have at least one of the Tokens that you're swapping.
Since our examples show swapping from AUSD to CTK, you need to have AUSD in your wallet at the start.  You can also choose to get some of both if you want.

Instructions for getting testnet AUSD are in our [Getting Testnet Tokens guide](/instant-settlement/guides/getting-testnet-tokens).

## Whitelisted Wallet

The `AgoraStableSwapPair` is permissioned, meaning that only approved wallets may call its swap functions.
Before spending gas, make sure that your address has the `APPROVED_SWAPPER` role. If it does not, the swap **will not be executed**.

### Checking your wallet's role

To check whether the wallet is whitelisted, we'll call `hasRole` on the contract with the role identifier (`APPROVED_SWAPPER` ) and the wallet address.

```tsx
async function isWhiteListed(
  pair: `0x${string}`,
  callerAddress: `0x${string}`,
) {
  return await client.readContract({
    address: pair,
    abi: instantSettlementAbi,
    functionName: "hasRole",
    args: ["APPROVED_SWAPPER", callerAddress],
  });
}
```

If running this returns `true` then you're all set!

### Self-whitelisting

> **Testnet only**
>
> This self-service whitelisting is **only available on testnets**. On mainnet, whitelisting requires completing KYC verification through Agora.

On testnets, Agora deploys a **Whitelister contract** that holds the `WHITELISTER_ROLE` on all pairs. This allows any user to whitelist themselves by calling `setApprovedSwapper`.

The Whitelister contract address is the same on all testnets:

```
0x7c10F56d6f04a51376393a1C3670e966863F6BD5
```

See [Protocol Deployments](/instant-settlement/protocol-deployments#testnets) for the full list of testnet addresses.

Call the `setApprovedSwapper` function with your wallet address to grant yourself the `APPROVED_SWAPPER` role on all testnet pairs:

**Helper Function:**

```tsx
const WHITELISTER = "0x7c10F56d6f04a51376393a1C3670e966863F6BD5";

const whitelisterAbi = [
  {
    type: "function",
    name: "setApprovedSwapper",
    inputs: [{ name: "swapper", type: "address" }],
    outputs: [],
    stateMutability: "nonpayable",
  },
] as const;

async function whitelistSelf(callerAddress: `0x${string}`) {
  const { request } = await client.simulateContract({
    address: WHITELISTER,
    abi: whitelisterAbi,
    functionName: "setApprovedSwapper",
    args: [callerAddress],
  });
  return await client.writeContract(request);
}
```

**Using this helper function:**

```tsx
(async () => {
  const [callerAddress] = await client.getAddresses();
  console.log(`Caller Address: ${callerAddress}`);

  // Check if already whitelisted
  const isWhitelisted = await isWhiteListed(TESTNET_PAIR, callerAddress);

  if (!isWhitelisted) {
    console.log("Not whitelisted, requesting whitelist...");
    const txHash = await whitelistSelf(callerAddress);
    await client.waitForTransactionReceipt({ hash: txHash });
    console.log(`Whitelisted! Transaction: ${txHash}`);
  } else {
    console.log("Already whitelisted!");
  }
})();
```

**Output:**

```text
Caller Address: 0x99B0E95Fa8F5C3b86e4d78ED715B475cFCcf6E97
Not whitelisted, requesting whitelist...
Whitelisted! Transaction: 0x3f6478e712e688eab150dd86ffc4bdd5a49b62619ad80fcf2c5a4f226c0ec6e5
```

Once whitelisted, you can proceed to execute swaps on any testnet pair.

## Fetch token decimals

In order to calculate the swap amounts correctly, you need to know the decimal precision of both tokens in the pair. The `AgoraStableSwapPair` contract exposes the decimals for both tokens through getter functions:

```tsx
async function getTokenDecimals(
  client: PublicActions,
  pairAddress: `0x${string}`,
) {
  const [decimalsToken0, decimalsToken1] = await Promise.all([
    client.readContract({
      address: pairAddress,
      abi: instantSettlementAbi,
      functionName: "token0Decimals",
      args: [],
    }),
    client.readContract({
      address: pairAddress,
      abi: instantSettlementAbi,
      functionName: "token1Decimals",
      args: [],
    }),
  ]);

  return { decimalsToken0, decimalsToken1 };
}
```

## Define a `swapPath`

An important step in swapping using our pair contract is making sure that you determine a `swapPath`. This essentially means that you clearly define which token you are inputting and what token will be output.
This keeps things clear so that you make sure you don't accidentally swap in the wrong direction.

* Setting a `swapPath` of **`[token0, token1]`** means you will swap *token0* for *token1*
* Setting a `swapPath` of **`[token1, token0]`** means you will swap *token1* for *token0*

### Finding token order in the pair

How do I know what token is token0 and which is token1 in an Agora pair?

Each Agora pair has a `name` function. The name returned is purposefully configured to show the order in the pair.

```tsx
async function getTokenOrder(
  client: PublicActions,
  pairAddress: `0x${string}`,
) {
  const [name, token0, token1] = await Promise.all([
    client.readContract({
      address: pairAddress,
      abi: instantSettlementAbi,
      functionName: "name",
      args: [],
    }),
    client.readContract({
      address: pairAddress,
      abi: instantSettlementAbi,
      functionName: "token0",
      args: [],
    }),
    client.readContract({
      address: pairAddress,
      abi: instantSettlementAbi,
      functionName: "token1",
      args: [],
    }),
  ]);

  const [name0, name1] = (name as string).split("/");

  return {
    token0: { name: name0, address: token0 as `0x${string}` },
    token1: { name: name1, address: token1 as `0x${string}` },
  };
}
```

The above example will return a name of "CTK/AUSD". With that name, it means CTK is token0 and AUSD is token1.

### Example `swapPath`

Because you funded your wallet with **AUSD** earlier (see [Getting Testnet Tokens](/instant-settlement/guides/getting-testnet-tokens)), we'll set our `swapPath`to swap AUSD for CTK:

```tsx
const { token0, token1 } = await getTokenOrder(client, TESTNET_PAIR_AUSD_CTK);

// this will give us
// token0 = { name: "CTK", address: "0x..." }
// token1 = { name: "AUSD", address: "0x..." }

const swapPath = [token1.address, token0.address] as const; // AUSD → CTK

// Need the opposite direction?  Use:
// const swapPath = [token0.address, token1.address] as const; // CTK → AUSD
```

## Complete Self-Contained Swap Example

The following is a complete, self-contained TypeScript example using viem that demonstrates both fixed-input and fixed-output swaps on Agora Instant Settlement. It includes all required setup, ABI definitions, and helper functions — no external imports beyond viem and dotenv.

### Prerequisites

* Node.js >= 18
* `npm install viem@^1 dotenv@^16`
* A `.env` file with `TESTNET_HOTWALLET_PK=0x...` (Sepolia test wallet private key)
* Sepolia ETH for gas (get from [https://cloud.google.com/application/web3/faucet/ethereum/sepolia](https://cloud.google.com/application/web3/faucet/ethereum/sepolia))
* Testnet AUSD tokens (request from faucet contract `0xd236c18D274E54FAccC3dd9DDA4b27965a73ee6C` by calling `requestFunds(yourAddress)`)

### Key Addresses (Sepolia Testnet)

* **Factory**: `0x8468587Af422ad440F58a57E955eCA6A970b5375`
* **AUSD/CTK Pair**: `0x1Aa8958Aa34cEC8096EF4381cb335effe977b0ae`
* **AUSD Token**: `0xa9012a055bd4e0eDfF8Ce09f960291C09D5322dC` (6 decimals)
* **CTK Token**: `0x7BEb5D9DB0d85cBEa543C04f0dE8c23c2176cd9D` (18 decimals)
* **Whitelister (testnet only)**: `0x7c10F56d6f04a51376393a1C3670e966863F6BD5`
* **AUSD Faucet**: `0xd236c18D274E54FAccC3dd9DDA4b27965a73ee6C`
* **CTK Faucet**: `0xf8A143b3406faF59FD9A34891076104B10200B1D`

### Important Constraints

* Swaps are **permissioned** — your wallet must have the `APPROVED_SWAPPER` role. On testnets, you can self-whitelist via the Whitelister contract. On mainnet, whitelisting requires KYC verification through Agora.
* The `swapPath` array determines swap direction: `[inputToken, outputToken]`. Token order in the pair is fixed (token0 \< token1 by address sort). Use the pair's `name()` function to discover which token is token0 vs token1.
* All prices and fees use 18-decimal precision internally.
* Swaps require ERC-20 `approve()` before the pair contract can pull tokens from your wallet.

### Minimal ABI Definitions

```tsx
const instantSettlementAbi = [
  { type: "function", name: "hasRole", inputs: [{ name: "_role", type: "string" }, { name: "_address", type: "address" }], outputs: [{ type: "bool" }], stateMutability: "view" },
  { type: "function", name: "name", inputs: [], outputs: [{ type: "string" }], stateMutability: "view" },
  { type: "function", name: "token0", inputs: [], outputs: [{ type: "address" }], stateMutability: "view" },
  { type: "function", name: "token1", inputs: [], outputs: [{ type: "address" }], stateMutability: "view" },
  { type: "function", name: "token0Decimals", inputs: [], outputs: [{ type: "uint8" }], stateMutability: "view" },
  { type: "function", name: "token1Decimals", inputs: [], outputs: [{ type: "uint8" }], stateMutability: "view" },
  { type: "function", name: "reserve0", inputs: [], outputs: [{ type: "uint256" }], stateMutability: "view" },
  { type: "function", name: "reserve1", inputs: [], outputs: [{ type: "uint256" }], stateMutability: "view" },
  { type: "function", name: "getPrice", inputs: [], outputs: [{ type: "uint256" }], stateMutability: "view" },
  { type: "function", name: "getPrice", inputs: [{ name: "_timestamp", type: "uint256" }], outputs: [{ type: "uint256" }], stateMutability: "view" },
  { type: "function", name: "token0PurchaseFee", inputs: [], outputs: [{ type: "uint256" }], stateMutability: "view" },
  { type: "function", name: "token1PurchaseFee", inputs: [], outputs: [{ type: "uint256" }], stateMutability: "view" },
  { type: "function", name: "getAmountsOut", inputs: [{ name: "_amountIn", type: "uint256" }, { name: "_path", type: "address[]" }], outputs: [{ type: "uint256[]" }], stateMutability: "view" },
  { type: "function", name: "getAmountsIn", inputs: [{ name: "_amountOut", type: "uint256" }, { name: "_path", type: "address[]" }], outputs: [{ type: "uint256[]" }], stateMutability: "view" },
  { type: "function", name: "swapExactTokensForTokens", inputs: [{ name: "_amountIn", type: "uint256" }, { name: "_amountOutMin", type: "uint256" }, { name: "_path", type: "address[]" }, { name: "_to", type: "address" }, { name: "_deadline", type: "uint256" }], outputs: [{ type: "uint256[]" }], stateMutability: "nonpayable" },
  { type: "function", name: "swapTokensForExactTokens", inputs: [{ name: "_amountOut", type: "uint256" }, { name: "_amountInMax", type: "uint256" }, { name: "_path", type: "address[]" }, { name: "_to", type: "address" }, { name: "_deadline", type: "uint256" }], outputs: [{ type: "uint256[]" }], stateMutability: "nonpayable" },
] as const;

const erc20Abi = [
  { type: "function", name: "approve", inputs: [{ name: "spender", type: "address" }, { name: "amount", type: "uint256" }], outputs: [{ type: "bool" }], stateMutability: "nonpayable" },
  { type: "function", name: "balanceOf", inputs: [{ name: "account", type: "address" }], outputs: [{ type: "uint256" }], stateMutability: "view" },
] as const;

const whitelisterAbi = [
  { type: "function", name: "setApprovedSwapper", inputs: [{ name: "swapper", type: "address" }], outputs: [], stateMutability: "nonpayable" },
] as const;
```

### Complete Script

```tsx
import { http, createWalletClient, publicActions, nonceManager } from "viem";
import { sepolia } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
import dotenv from "dotenv";

dotenv.config();
if (!process.env.TESTNET_HOTWALLET_PK)
  throw new Error("TESTNET_HOTWALLET_PK not set in .env");

// --- Setup ---
const account = privateKeyToAccount(
  process.env.TESTNET_HOTWALLET_PK as `0x${string}`,
  { nonceManager },
);

const client = createWalletClient({
  chain: sepolia,
  transport: http(
    process.env.SEPOLIA_RPC_URL || "https://ethereum-sepolia-rpc.publicnode.com",
  ),
  account,
}).extend(publicActions);

const PAIR = "0x1Aa8958Aa34cEC8096EF4381cb335effe977b0ae" as const;
const WHITELISTER = "0x7c10F56d6f04a51376393a1C3670e966863F6BD5" as const;

// --- Helper: Ensure wallet is whitelisted (testnet only) ---
async function ensureWhitelisted(pair: `0x${string}`, caller: `0x${string}`) {
  const approved = await client.readContract({
    address: pair, abi: instantSettlementAbi, functionName: "hasRole",
    args: ["APPROVED_SWAPPER", caller],
  });
  if (!approved) {
    const { request } = await client.simulateContract({
      address: WHITELISTER, abi: whitelisterAbi,
      functionName: "setApprovedSwapper", args: [caller],
    });
    const hash = await client.writeContract(request);
    await client.waitForTransactionReceipt({ hash });
  }
}

// --- Helper: Discover token order and build swap path ---
async function buildSwapPath(pair: `0x${string}`, inputSymbol: string) {
  const [name, token0, token1] = await Promise.all([
    client.readContract({ address: pair, abi: instantSettlementAbi, functionName: "name" }),
    client.readContract({ address: pair, abi: instantSettlementAbi, functionName: "token0" }),
    client.readContract({ address: pair, abi: instantSettlementAbi, functionName: "token1" }),
  ]);
  const [sym0, sym1] = (name as string).split("/");
  // swapPath = [input, output]
  if (sym0 === inputSymbol) return [token0, token1] as [`0x${string}`, `0x${string}`];
  if (sym1 === inputSymbol) return [token1, token0] as [`0x${string}`, `0x${string}`];
  throw new Error(`Token ${inputSymbol} not found in pair ${name}`);
}

// --- Helper: Approve ERC-20 spending ---
async function approveToken(token: `0x${string}`, spender: `0x${string}`, amount: bigint) {
  const { request } = await client.simulateContract({
    address: token, abi: erc20Abi, functionName: "approve", args: [spender, amount],
  });
  return client.writeContract(request);
}

// --- Fixed-Input Swap (swapExactTokensForTokens) ---
// You specify exactly how many tokens to send; the output amount is calculated.
async function swapFixedInput(
  pair: `0x${string}`,
  amountIn: bigint,
  swapPath: [`0x${string}`, `0x${string}`],
) {
  // 1. Get quote
  const amounts = await client.readContract({
    address: pair, abi: instantSettlementAbi, functionName: "getAmountsOut",
    args: [amountIn, [...swapPath]],
  });
  const amountOutMin = (amounts as bigint[])[1];

  // 2. Approve input token
  await approveToken(swapPath[0], pair, amountIn);

  // 3. Execute swap (5-minute deadline)
  const deadline = BigInt(Math.floor(Date.now() / 1000) + 300);
  const [caller] = await client.getAddresses();
  const { request } = await client.simulateContract({
    address: pair, abi: instantSettlementAbi, functionName: "swapExactTokensForTokens",
    args: [amountIn, amountOutMin, [...swapPath], caller, deadline],
  });
  return client.writeContract(request);
}

// --- Fixed-Output Swap (swapTokensForExactTokens) ---
// You specify exactly how many tokens to receive; the input amount is calculated.
async function swapFixedOutput(
  pair: `0x${string}`,
  amountOut: bigint,
  swapPath: [`0x${string}`, `0x${string}`],
) {
  // 1. Get quote for required input
  const amounts = await client.readContract({
    address: pair, abi: instantSettlementAbi, functionName: "getAmountsIn",
    args: [amountOut, [...swapPath]],
  });
  const amountInMax = (amounts as bigint[])[0];

  // 2. Approve input token (up to the max)
  await approveToken(swapPath[0], pair, amountInMax);

  // 3. Execute swap (5-minute deadline)
  const deadline = BigInt(Math.floor(Date.now() / 1000) + 300);
  const [caller] = await client.getAddresses();
  const { request } = await client.simulateContract({
    address: pair, abi: instantSettlementAbi, functionName: "swapTokensForExactTokens",
    args: [amountOut, amountInMax, [...swapPath], caller, deadline],
  });
  return client.writeContract(request);
}

// --- Usage Example ---
(async () => {
  const [caller] = await client.getAddresses();

  // Step 1: Ensure whitelisted
  await ensureWhitelisted(PAIR, caller);

  // Step 2: Build swap path (AUSD → CTK)
  const swapPath = await buildSwapPath(PAIR, "AUSD");

  // Step 3a: Fixed-input swap — send exactly 100 AUSD, receive calculated CTK
  const amountIn = 100n * 10n ** 6n; // AUSD has 6 decimals
  const txHash = await swapFixedInput(PAIR, amountIn, swapPath);
  console.log("Fixed-input swap tx:", txHash);

  // Step 3b: OR Fixed-output swap — receive exactly 100 CTK, send calculated AUSD
  // const amountOut = 100n * 10n ** 18n; // CTK has 18 decimals
  // const txHash = await swapFixedOutput(PAIR, amountOut, swapPath);
  // console.log("Fixed-output swap tx:", txHash);
})();
```

### Error Handling

Common revert reasons when swapping:

* `PairIsPaused()` — The pair is currently paused by the PAUSER_ROLE
* `Expired()` — The deadline timestamp has passed; increase the deadline
* `InsufficientOutputAmount()` — The quoted output is less than your amountOutMin; price may have moved
* `ExcessiveInputAmount()` — The required input exceeds your amountInMax; price may have moved
* `InsufficientLiquidity()` — Not enough reserves in the pair to fill your swap
* `InvalidPath()` / `InvalidPathLength()` — The swapPath must be exactly 2 addresses matching the pair's token0 and token1
* `AddressIsNotRole("APPROVED_SWAPPER")` — Your wallet is not whitelisted; see the whitelisting section above

### Mainnet Differences

* Whitelisting requires KYC verification through Agora (no self-service whitelister contract)
* Use the appropriate chain and RPC URL (see Protocol Deployments for mainnet factory/pair addresses)
* Factory address: use `getAllPairData()` on the factory to discover available pairs on any chain