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

# Swap and route local stablecoins with LI.FI

> Recipe to swap wARS, wBRL, wMXN, wCOP, wPEN and wCLP against USDC or each other, and to receive wFiat from any token on any chain, with the LI.FI API.

This recipe shows how to add swaps of Ripio's [wFiat](https://docs.ripio.com/wfiat/overview.md) stablecoins to your product with [LI.FI](https://li.fi), a routing layer that aggregates DEXs, solvers and bridges behind one API. With a single quote endpoint you can let users swap wFiat against USDC, swap one local stablecoin for another, or pay from any token on any chain and deliver wFiat on the destination.

> [!NOTE]
>
> Ripio doesn't operate LI.FI or the venues it routes through. Swaps execute on third-party protocols at the prices they offer, so the amount received depends on the route LI.FI selects at quote time and can differ from the estimate.

## What you can build

> [card] Swap wFiat and USDC
>
> Let users move between their local currency and dollars on-chain: wARS to USDC, USDC to wBRL.

> [card] Local-to-local FX
>
> Swap one local stablecoin for another in one transaction, for example wARS to wBRL.

> [card] Receive wFiat from any chain
>
> The payer sends any token on any chain, and the recipient gets wMXN on Base.

## How it works

**1. Get a quote**

`GET /v1/quote` returns a route for the pair and amount, the estimated and minimum output, the fees, and a `transactionRequest` ready to sign.

**2. Approve the token**

If the source token is an ERC-20, the user approves the `approvalAddress` from the quote to spend `fromAmount`.

**3. Send the transaction**

The user signs and sends the `transactionRequest`. Same-chain swaps settle in that transaction.

**4. Track the status**

For cross-chain routes, poll `GET /v1/status` until the transfer is `DONE` or `FAILED`.

LI.FI's API doesn't require an API key: one only raises your rate limits. LI.FI charges a fixed **0.25%** fee on each transaction, and you can add your own fee on top (see [Monetize the integration](#monetize-the-integration)).

## Token addresses

LI.FI recognizes the wFiat token addresses on Ethereum, Base, World Chain, BNB Smart Chain, Polygon, Gnosis and Celo. Each token uses the same address on every EVM chain, with 18 decimals. See [Contracts](https://docs.ripio.com/wfiat/contracts.md) for the full network list.

| Token | Address (EVM chains) |
| - | - |
| ![wARS](https://b2b-widget-onramp.ripio.com/wars.png)**wARS** ![](https://flagcdn.com/ar.svg) | `0x0DC4F92879B7670e5f4e4e6e3c801D229129D90D` |
| ![wBRL](https://b2b-widget-onramp.ripio.com/wbrl.png)**wBRL** ![](https://flagcdn.com/br.svg) | `0xD76f5Faf6888e24D9F04Bf92a0c8B921FE4390e0` |
| ![wMXN](https://b2b-widget-onramp.ripio.com/wmxn.png)**wMXN** ![](https://flagcdn.com/mx.svg) | `0x337E7456B420bD3481e7FA61fA9850343d610d34` |
| ![wCOP](https://b2b-widget-onramp.ripio.com/wcop.png)**wCOP** ![](https://flagcdn.com/co.svg) | `0x8a1D45e102e886510e891d2Ec656a708991e2D76` |
| ![wPEN](https://coin-images.coingecko.com/coins/images/70975/large/wpen_logo_1.png)**wPEN** ![](https://flagcdn.com/pe.svg) | `0x4F34c8b3b5FB6D98Da888F0feA543d4d9C9F2eBE` |
| ![wCLP](https://coin-images.coingecko.com/coins/images/70976/large/wclp_logo_1.png)**wCLP** ![](https://flagcdn.com/cl.svg) | `0x61D450a098b6a7f69fC4b98CE68198fe59768651` |

## Recipe

The examples below use the LI.FI REST API with [viem](https://viem.sh/), on **Base** and **Arbitrum**. The same calls work on any chain above: add it to the client map and change the chain IDs and token addresses.

LI.FI quotes run against mainnet and wFiat isn't deployed on testnets, so test with small amounts.

### 0. Setup

```bash theme={null}
npm install viem @lifi/types
```

```ts constants.ts theme={null}
export const LIFI_API = 'https://li.quest/v1';

export const BASE = 8453;
export const ARBITRUM = 42161;

// wFiat: same address on every supported EVM chain
export const WARS = '0x0DC4F92879B7670e5f4e4e6e3c801D229129D90D';
export const WBRL = '0xD76f5Faf6888e24D9F04Bf92a0c8B921FE4390e0';
export const WMXN = '0x337E7456B420bD3481e7FA61fA9850343d610d34';

// USDC, per chain
export const USDC_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
export const USDC_ARBITRUM = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831';
```

```ts clients.ts theme={null}
import 'viem/window'; // types window.ethereum
import { createPublicClient, createWalletClient, custom, http, type Chain } from 'viem';
import { arbitrum, base } from 'viem/chains';

const CHAINS: Record<number, Chain> = { [base.id]: base, [arbitrum.id]: arbitrum };

// Browser only: the wallet client signs with the user's wallet
export function getClients(chainId: number) {
  const chain = CHAINS[chainId];
  if (!chain) throw new Error(`Unsupported chain ${chainId}`);
  return {
    publicClient: createPublicClient({ chain, transport: http() }),
    walletClient: createWalletClient({ chain, transport: custom(window.ethereum!) }),
  };
}
```

Resolve token addresses from your own config or from LI.FI's `GET /v1/token`, never from a symbol alone.

### 1. Get a quote

```ts quote.ts theme={null}
import type { LiFiStep } from '@lifi/types';
import { LIFI_API } from './constants';

export type QuoteParams = {
  fromChain: number;
  toChain: number;
  fromToken: string;
  toToken: string;
  fromAmount: bigint; // in the source token's smallest unit (wFiat has 18 decimals)
  fromAddress: `0x${string}`;
  toAddress?: `0x${string}`; // defaults to fromAddress
  slippage?: number; // 0.005 = 0.5%
};

export async function getQuote(p: QuoteParams): Promise<LiFiStep> {
  const params = new URLSearchParams({
    fromChain: String(p.fromChain),
    toChain: String(p.toChain),
    fromToken: p.fromToken,
    toToken: p.toToken,
    fromAmount: p.fromAmount.toString(),
    fromAddress: p.fromAddress,
    slippage: String(p.slippage ?? 0.005),
    integrator: 'your-app-name',
  });
  if (p.toAddress) params.set('toAddress', p.toAddress);

  const res = await fetch(`${LIFI_API}/quote?${params}`);
  if (!res.ok) {
    const body = await res.json().catch(() => ({ message: res.statusText }));
    throw Object.assign(new Error(body.message), body); // { code, message }, e.g. 1002 when no route is found
  }
  return res.json();
}
```

A quote for 100,000 wARS to USDC on Base:

```ts theme={null}
import { parseUnits } from 'viem';
import { getClients } from './clients';
import { getQuote } from './quote';
import { BASE, WARS, USDC_BASE } from './constants';

const { walletClient } = getClients(BASE);
const [userAddress] = await walletClient.requestAddresses();

const quote = await getQuote({
  fromChain: BASE,
  toChain: BASE,
  fromToken: WARS,
  toToken: USDC_BASE,
  fromAmount: parseUnits('100000', 18),
  fromAddress: userAddress,
});

quote.estimate.toAmount;    // expected USDC out (6 decimals)
quote.estimate.toAmountMin; // minimum out after slippage
quote.estimate.feeCosts;    // LI.FI's 0.25% fee, plus yours if you set one
quote.transactionRequest;   // { to, data, value, gasLimit, chainId, ... }
```

Show `toAmount` and the fees before the user confirms, and request a new quote if they wait: prices move. A 0.5% slippage is a reasonable starting point for wFiat and USDC pairs; a lower value protects the user more but reverts more often. LI.FI also skips routes above 10% price impact by default (`maxPriceImpact`).

### 2. Approve the source token

```ts approve.ts theme={null}
import { erc20Abi, zeroAddress } from 'viem';
import type { getClients } from './clients';

type Clients = ReturnType<typeof getClients>;

export async function ensureAllowance(
  { publicClient, walletClient }: Clients,
  token: `0x${string}`,
  owner: `0x${string}`,
  spender: `0x${string}`, // quote.estimate.approvalAddress
  amount: bigint,
) {
  if (token === zeroAddress) return; // native token: nothing to approve

  const allowance = await publicClient.readContract({
    address: token,
    abi: erc20Abi,
    functionName: 'allowance',
    args: [owner, spender],
  });
  if (allowance >= amount) return;

  const hash = await walletClient.writeContract({
    account: owner,
    address: token,
    abi: erc20Abi,
    functionName: 'approve',
    args: [spender, amount], // the exact amount; a larger allowance saves future approvals
  });
  const receipt = await publicClient.waitForTransactionReceipt({ hash });
  if (receipt.status !== 'success') throw new Error('Approval reverted');
}
```

### 3. Send the transaction

```ts execute.ts theme={null}
import type { LiFiStep } from '@lifi/types';
import { getClients } from './clients';
import { ensureAllowance } from './approve';

export async function executeQuote(quote: LiFiStep, owner: `0x${string}`) {
  const tx = quote.transactionRequest!;
  const clients = getClients(tx.chainId!); // the source chain
  await clients.walletClient.switchChain({ id: tx.chainId! });

  await ensureAllowance(
    clients,
    quote.action.fromToken.address as `0x${string}`,
    owner,
    quote.estimate.approvalAddress as `0x${string}`,
    BigInt(quote.action.fromAmount),
  );

  const hash = await clients.walletClient.sendTransaction({
    account: owner,
    to: tx.to as `0x${string}`,
    data: tx.data as `0x${string}`,
    value: BigInt(tx.value ?? 0),
    gas: tx.gasLimit ? BigInt(tx.gasLimit) : undefined,
  });
  const receipt = await clients.publicClient.waitForTransactionReceipt({ hash });
  if (receipt.status !== 'success') throw new Error('Swap reverted: request a new quote');
  return hash;
}
```

For a same-chain swap, the user has the output token once this transaction is confirmed. If the market moved past `slippage` the transaction reverts and the user keeps the source token, minus the gas paid for the failed transaction: request a new quote and try again.

### 4. Swap one local stablecoin for another

Local-to-local FX is the same call with two wFiat. For example, wARS to wBRL on Base:

```ts theme={null}
import { parseUnits } from 'viem';
import { getQuote } from './quote';
import { executeQuote } from './execute';
import { BASE, WARS, WBRL } from './constants';

const quote = await getQuote({
  fromChain: BASE,
  toChain: BASE,
  fromToken: WARS,
  toToken: WBRL,
  fromAmount: parseUnits('100000', 18),
  fromAddress: userAddress,
});
await executeQuote(quote, userAddress);
```

LI.FI picks the route: a direct pool or solver when one exists, or a path through USDC.

### 5. Receive wFiat from any chain

To let someone pay from another chain and deliver wFiat to a recipient, set a different `toChain` and the recipient as `toAddress`. For example, a payer sends 50 USDC on Arbitrum and a merchant receives wMXN on Base. `executeQuote` switches the payer's wallet to Arbitrum, the source chain, before signing:

```ts theme={null}
import { parseUnits } from 'viem';
import { getQuote } from './quote';
import { executeQuote } from './execute';
import { waitForTransfer } from './status';
import { ARBITRUM, BASE, USDC_ARBITRUM, WMXN } from './constants';

// payerAddress: the connected wallet; merchantAddress: where the merchant receives wMXN
const quote = await getQuote({
  fromChain: ARBITRUM,
  toChain: BASE,
  fromToken: USDC_ARBITRUM,
  toToken: WMXN,
  fromAmount: parseUnits('50', 6),
  fromAddress: payerAddress,
  toAddress: merchantAddress,
});
const txHash = await executeQuote(quote, payerAddress);
const transfer = await waitForTransfer(txHash, ARBITRUM, BASE);
```

Then track the transfer until it lands:

```ts status.ts theme={null}
import { LIFI_API } from './constants';

export async function waitForTransfer(txHash: string, fromChain: number, toChain: number, maxWaitMs = 30 * 60_000) {
  const startedAt = Date.now();
  let delay = 10_000;

  while (Date.now() - startedAt < maxWaitMs) {
    const params = new URLSearchParams({ txHash, fromChain: String(fromChain), toChain: String(toChain) });
    const res = await fetch(`${LIFI_API}/status?${params}`);

    if (res.ok) {
      const status = await res.json();
      if (status.status === 'DONE' || status.status === 'FAILED') return status;
      if (status.status === 'INVALID') throw new Error('The hash does not belong to this transfer');
    } else if (res.status !== 404) {
      // 404 (code 1003) is normal for the first minute or two after broadcast; anything else isn't
      throw new Error(`LI.FI status error ${res.status}`);
    }

    await new Promise((r) => setTimeout(r, delay));
    delay = Math.min(delay * 2, 60_000);
  }
  throw new Error('Transfer still pending: check it later on scan.li.fi');
}
```

`DONE` and `FAILED` are final. Check `substatus` on a `DONE` transfer: `COMPLETED` means the recipient got the requested token, while `PARTIAL` or `REFUNDED` mean they received a different token or the payer got the funds back. Compare `receiving.amount` with the quote's `toAmountMin`. See LI.FI's [status tracking guide](https://docs.li.fi/introduction/user-flows-and-examples/status-tracking).

> [!NOTE]
>
> **Moving the same wFiat between chains?** Use the [Ripio Bridge](https://docs.ripio.com/wfiat/bridge.md) instead: it burns the token on the source chain and mints it on the destination, with a single contract call.

## Embed the LI.FI widget

If you'd rather not build the swap UI, LI.FI's React widget handles quotes, approvals, execution and status. Preconfigure it with wFiat as the destination, and add the wFiat you want users to pick, since the widget's default token list doesn't include every wFiat on every chain:

```tsx swap-widget.tsx theme={null}
'use client';

import { LiFiWidget, type WidgetConfig } from '@lifi/widget';

const config: WidgetConfig = {
  toChain: 8453, // Base
  toToken: '0x0DC4F92879B7670e5f4e4e6e3c801D229129D90D', // wARS
  tokens: {
    include: [
      { chainId: 8453, address: '0xD76f5Faf6888e24D9F04Bf92a0c8B921FE4390e0', symbol: 'wBRL', decimals: 18, name: 'wBRL' },
      { chainId: 8453, address: '0x337E7456B420bD3481e7FA61fA9850343d610d34', symbol: 'wMXN', decimals: 18, name: 'wMXN' },
    ],
  },
};

export function SwapWidget() {
  return <LiFiWidget integrator="your-app-name" config={config} />;
}
```

In Next.js, render the widget on the client only, and connect wallets through your existing wagmi setup or LI.FI's Ethereum provider package. See the widget's install and compatibility guides.

> [card] [LI.FI Widget docs](https://docs.li.fi/widget/overview)
>
> Install the widget and configure chains, tokens, theme and wallet management.

## Monetize the integration

Add `fee` to the quote (for example `fee=0.003` for 0.3%) together with your `integrator` string to take a commission on every swap. The fee is deducted from the source token and appears in `feeCosts` next to LI.FI's. Create an integrator account and set the wallet that receives your fees in the [LI.FI Partner Portal](https://portal.li.fi/). See [Monetizing the integration](https://docs.li.fi/introduction/integrating-lifi/monetizing-integration).

## Rate limits and API key

Without an API key, `/quote` allows 75 requests per two hours, which is enough to test but not for production. An API key defaults to 100 requests per minute, applied over a two-hour rolling window. Request a key in the [Partner Portal](https://portal.li.fi/) and send it as the `x-lifi-api-key` header **from your backend only**: never expose it in the browser. A `429` response with error code `1005` means you hit the limit. See LI.FI's [rate limits](https://docs.li.fi/api-reference/rate-limits).

## Resources

* [LI.FI API reference](https://docs.li.fi/api-reference/introduction)
* [Get a quote](https://docs.li.fi/api-reference/get-a-quote-for-a-token-transfer) and [check the status](https://docs.li.fi/api-reference/check-the-status-of-a-cross-chain-transfer)
* [LI.FI Widget](https://docs.li.fi/widget/overview)
* [wFiat contracts and networks](https://docs.ripio.com/wfiat/contracts.md)
* [Ripio Bridge developer guide](https://docs.ripio.com/wfiat/bridge.md)

Questions about wFiat? Write to [b2b@ripio.com](mailto:b2b@ripio.com). For routing and integration questions, contact [LI.FI](https://li.fi).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.