Build a Token Swap App (Permit2)

View as Markdown

⚠️ The Permit2 flow is intended for advanced integrators only.

If you’re new to 0x Swap API or want to avoid the double-signature UX, consider using AllowanceHolder instead.

This guide walks through a Next.js demo app that demonstrates a full token swap flow using Permit2, a contract designed for advanced integrators who are comfortable handling EIP-712 signatures and advanced token approvals.

This guide is intended for advanced integrators only. If you are new to Swap API or want to avoid the double-signature flow, we recommend using the Swap API AllowanceHolder flow.

Try it Out

What We’re Building

This app demonstrates principles used in production swapping apps such as Matcha.xyz:

  • Fetch indicative prices via /swap/permit2/price
  • Get firm quotes via /swap/permit2/quote
  • Approve token allowances using Permit2
  • Sign Permit2 EIP-712 messages and submit transactions

Video tutorial version here:

Pre-requisites

You should be familiar with:

This demo uses Base, but works on all supported chains.

Core Concepts

To build our swap app, we’ll focus on:

  • 🌈 RainbowKit for wallet connection
  • 🏷 PriceView (indicative pricing)
  • 💸 QuoteView (firm quotes + sign & append Permit2 message + submit transaction)
  • 🪙 Token list management

What is it?

These components are transferable to almost any token swapping app being built.

Swap v2 App Core Concepts

🌈 RainbowKit

What is it?

RainbowKit is a React library that makes it easy to add wallet connection to your app. We are using their Next.js App Router example in our app.

Check out their installation instructions to understand the configurations. You can configure your desired chains and generate the required connectors.

Code

Setup WalletConnect projectId in providers.tsx

RainbowKit relies on WalletConnect. Obtain a free projectId from WalletConnect Cloud and replace the key in the .env file.

1// Inside /swap-v2-next-app/.env
2# To get a RainbowKit relies on WalletConnect, get a free projectId here:
3# https://cloud.walletconnect.com/app
4NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID="ADD_YOUR_KEY"
1// Inside /app/providers/tsx
2const projectId = process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID as string; // Uses your free key setup in .env

🏷 PriceView

What is it?

PriceView displays indicative pricing using/swap/allowance-holder/price. Indicative pricing is for users to browse for a price without committing to a trade.

Indicative prices:

  • Do not reserve liquidity
  • Are ideal for browsing or real-time updates
  • Avoid unnecessary /quote calls, which should only be used when the user intends to trade

Firm quotes are fetched later in QuoteView.

UX flow

  1. Select tokens
  2. Enter amount → triggers indicative price fetch
  3. Connect wallet (RainbowKit handles network switching)
  4. Approve allowance for AllowanceHolder
  5. Review trade → proceed to QuoteView

Code

Fetch Indicative Price

Token Allowances & Approvals

Important: Read Before Using 0x API

  • NEVER set an allowance on the Settler contract. Doing so may result in unintended consequences, including potential loss of tokens or exposure to security risks. The Settler contract does not support or require token allowances for its operation. Setting an allowance on the Settler contract will lead to misuse by other parties.

  • ONLY set allowances on AllowanceHolder or Permit2 contracts, as indicated by the API responses.

  • The correct allowance target is returned in issues.allowance.spender or allowanceTarget.

Before placing a trade on the PageView, users must set allowances on all tokens involved. A token allowance lets a third party move funds on your behalf. Essentially, you permit them to move your tokens.

In our case, we want to approve an allowance for AllowanceHolder contract to trade our ERC20 tokens for us. To do this, we need to approve a specific allowance, allowing this contract to move a certain amount of our ERC20 tokens on our behalf. Read more about token allowances.

token allowance ui

The logic to check if the user has approved a token allowance the selected sell token is setup in /app/components/price.ts.

  1. Read existing allowance with useReadContract
  2. If insufficient → simulate + write approval (useSimulateContract, useWriteContract)
  3. Wait for confirmation using useWaitForTransactionReceipt

Be aware that approvals cost gas. Looking for a gasless approach? Check out Gasless API.

Need to quickly revoke an allowance while testing? To revoke an allowance, you can set the allowance to 0. This can be done programmatically or through a UI such as https://revoke.cash/ .

💸 QuoteView

What is it?

QuoteView displays a firm, executable quote returned from /swap/allowance-holder/quote.

Firm quotes:

  • Reserve liquidity from market makers
  • Contain a fully executable order

swap demo quoteview

UX Flow

  • Shows final sell and buy amounts
  • Metadata (symbols, decimals) comes from the token list
  • User clicks “Place Order” to sign and broadcast the transaction

Code

Fetch Firm Quote

Sign the Permit2 EIP-712 message

Before submitting the quote order to the blockchain, we need to sign the permit2.eip712 object from our quote response. Code here.

1// sign permit2.eip712 returned from quote
2
3let signature: Hex;
4signature = await signTypedDataAsync(quote.permit2.eip712);

Append Signature Length and Signature Data to transaction.data

Next, append the signature length and signature data to transaction.data. Code here.

The format should be <sig len><sig data>, where:

  • <sig len>: 32-byte unsigned big-endian integer representing the length of the signature
  • <sig data>: The actual signature data
1import { concat, numberToHex, size } from "viem";
2
3const signatureLengthInHex = numberToHex(size(signature), {
4 signed: false,
5 size: 32,
6});
7quote.transaction.data = concat([
8 transaction.data,
9 signatureLengthInHex,
10 signature,
11]);

Submit Transaction

The last step is to submit the transaction with all the required parameters using your preferred web3 library (e.g. wagmi, viem, ethers.js, web3.js). In this example, we use wagmi’s useSendTransaction.

Pass the required params we get from the quote response to sendTransaction. Code here.

1sendTransaction({
2 account: walletClient?.account.address,
3 gas: !!quote?.transaction.gas ? BigInt(quote?.transaction.gas) : undefined,
4 to: quote?.transaction.to,
5 data: quote?.transaction.data,
6 chainId: chainId,
7});

🪙 Token lists

What is it?

Token Lists provide curated sets of ERC20 tokens—along with their associated metadata—that applications can use when enabling asset selection (e.g., choosing tokens to swap). These lists typically include:

  • Name (e.g., Wrapped Ether)
  • Symbol (e.g., ETH)
  • Contract address
  • logoURI

Developers can either consume an existing list in full or create a customized list derived from one or more established sources.

Commonly Used Token List Sources

Code

In our demo, we curated a token list in /src/constants.ts. In production level apps, it’s common practice to maintain a token list since some apps don’t support all available tokens.

Monetize Your Swap Integration

Swap API provides two built-in ways to monetize your swap integration—available on both free and paid plan:

  • Affiliate fees: Earn a commission on each trade (demonstrated in this example app).
  • Trade surplus: trade surplus (i.e. positive slippage) 1

For implementation details and pricing considerations, see the how to monetize your app using 0x Swap API.

1 Trade surplus is available only to select integrators on a custom plan. Contact support for access.

Conclusion

By following the best practices outlined in this blog post, you can create a user-friendly and effective app that enables trustless token swapping on our supported chains.

You now have a fully functional swap workflow that demonstrates:

  • Fetching indicative pricing
  • Fetching firm quotes
  • Setting token allowances
  • Signing and appending the Permit2 EIP-712 message
  • Submitting transactions on-chain

This pattern works across all supported chains..

Happy swapping!