Skip to main content

Zap SOR API Guide

Overview

Uniswap V2’s user-friendly codebase has been widely adopted and adapted, influencing numerous networks and derivative projects like Solidly. However, its requirement for liquidity to be added or removed in strict 50/50 ratios has led to a less-than-ideal user experience. In cases where users provide imbalanced ratios, the excess is forfeited, resulting in fewer LP tokens during minting or an enforced 50/50 split upon burning. Odos improves this by accepting any ratio of tokens and converting them efficiently into the most advantageous LP tokens in a single transaction.

Before Odos Zaps

In order to exit a liquidity position and enter a new one, a user must make a minimum of 4 transactions. Below is an example of going from a LP position in Uniswap V2 to another one. For this example, the pools will be WETH/DAI and USDC/UNI

  1. Remove liquidity from WETH/DAI
  2. Swap WETH for UNI
  3. Swap DAI for USDC
  4. Deposit liquidity into USDC/UNI pool

During these transactions, the market may move, or slippage may occur, resulting in less than a 50/50 split of assets.

Diagram

After Odos Zaps

Odos simplifies the process of moving from an LP to another LP, but doing it perfectly and in a single, atomic transaction.

Diagram

Supported Pools

Odos Zapping supports Uniswap V2 and Solidly style pools, along with minting and burning of Curve LP tokens for StableSwap pools within the core Odos SOR.

Optimal Rebalancing

Odos strives to distribute your input evenly across a pool’s 50/50 ratio requirement. However, price fluctuations during the transaction can create an imbalance, resulting in a surplus that the pool won’t accept and could be lost. To avoid this, Odos Zaps employ two protective measures:

  1. For standard Uniswap V2 pools: It calculates the exact swap amount to align your contribution with the current pool reserves, optimizing the use of your funds.
  2. For non-standard pools: It can return any surplus tokens to your wallet, ensuring you’re only contributing what’s needed for LP tokens.

Since both options will have a small additional fixed gas cost, they will only be performed if the quantity of wasted assets is above a specified threshold.

Multi-input Zaps

With Odos zapping, users can adjust their investments across several liquidity pools or specific tokens in just one step. This tool is particularly handy for streamlining the process of selling off or reorganizing a set of LP tokens. You can go from many LP positions into a single position or into a combination of tokens.

A summary of what is possible with Odos Zaps:

  • Any number of tokens to a single LP
  • Any number of LPs to a single LP
  • Any number of LPs and tokens to a single LP
  • Any number of LPs and tokens to any number of tokens

Usage

Odos Zaps are accessible through the API and utilize the same smart contract router as the core SOR service. This commonality eliminates the requirement for additional approvals, minimizing security risks. Existing Odos SOR users can conveniently use Odos Zaps to manage their entry into and exit from liquidity positions with ease.

Fees

When using Odos Zaps, the swapMulti function on the router is always employed, incurring a fee of 0.01%. It’s important to note that no additional fees are collected from positive slippage when swapMulti is used.

Setup

Firstly, you'll need to setup your project. This involves importing the required libraries for making HTTP requests.

Recommended HTTP libraries (examples below):

  • Python: requests (Requires installation from here)
  • Javascript: fetch (natively supported in most environments)

Types Of Zap Quotes Supported by Odos

The following request bodies can be sent to the /sor/quote/v2/zap endpoint

Zap into a single pool using a single asset

1 WETH -> Uniswap V2 LP Position WETH/USDC Uniswap V2 Pool

{
"chainId": 1,
"inputTokens": [
{
"amount": "1000000000000000000",
"tokenAddress": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
}
],
"outputTokens": [
{
"proportion": 1,
"tokenAddress": "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc"
}
],
"referralCode": 0,
"slippageLimitPercent": 0.3,
"sourceBlacklist": [],
"sourceWhitelist": [],
"userAddr": "0x...",
"compact": true
}

Zap into a single pool using multiple assets

100 DAI + 250 USDC -> Uniswap V2 LP Position WETH/USDC Uniswap V2 Pool

{
"chainId": 1,
"inputTokens": [
{
"amount": "100000000000000000000",
"tokenAddress": "0x6B175474E89094C44Da98b954EedeAC495271d0F"
},
{
"amount": "250000000",
"tokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
}
],
"outputTokens": [
{
"proportion": 1,
"tokenAddress": "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc"
}
],
"referralCode": 0,
"slippageLimitPercent": 0.3,
"sourceBlacklist": [],
"sourceWhitelist": [],
"userAddr": "0x...",
"compact": true
}

Zap into a single pool using a mix of assets and other LP positions

1 WETH + 1 Uniswap V2 LP Position WETH/USDC Uniswap V2 Pool -> SushiSwap V2 LP Position WETH/USDT SushiSwap V2 Pool

{
"chainId": 1,
"inputTokens": [
{
"amount": "1000000000000000000",
"tokenAddress": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
},
{
"amount": "1000000000000000000",
"tokenAddress": "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc"
}
],
"outputTokens": [
{
"proportion": 1,
"tokenAddress": "0x06da0fd433C1A5d7a4faa01111c044910A184553"
}
],
"referralCode": 0,
"slippageLimitPercent": 0.3,
"sourceBlacklist": [],
"sourceWhitelist": [],
"userAddr": "0x...",
"compact": true
}

Zap out of a single pool into a single asset

1 Uniswap V2 LP Position WETH/USDC Uniswap V2 Pool -> LINK

{
"chainId": 1,
"inputTokens": [
{
"amount": "1000000000000000000",
"tokenAddress": "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc"
}
],
"outputTokens": [
{
"proportion": 1,
"tokenAddress": "0x514910771AF9Ca656af840dff83E8264EcF986CA"
}
],
"referralCode": 0,
"slippageLimitPercent": 0.3,
"sourceBlacklist": [],
"sourceWhitelist": [],
"userAddr": "0x...",
"compact": true
}

Zap out of a single pool into multiple assets

1 Uniswap V2 LP Position WETH/USDC Uniswap V2 Pool -> 80% WETH + 20% WBTC

{
"chainId": 1,
"inputTokens": [
{
"amount": "1000000000000000000",
"tokenAddress": "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc"
}
],
"outputTokens": [
{
"proportion": 0.8,
"tokenAddress": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
},
{
"proportion": 0.2,
"tokenAddress": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
}
],
"referralCode": 0,
"slippageLimitPercent": 0.3,
"sourceBlacklist": [],
"sourceWhitelist": [],
"userAddr": "0x...",
"compact": true
}

Zap out of multiple pools into multiple assets

1 Uniswap V2 LP Position WETH/USDC Uniswap V2 Pool + 1 SushiSwap V2 LP Position WETH/USDT SushiSwap V2 Pool -> 50% USDC + 50% USDT

{
"chainId": 1,
"inputTokens": [
{
"amount": "1000000000000000000",
"tokenAddress": "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc"
},
{
"amount": "1000000000000000000",
"tokenAddress": "0x06da0fd433C1A5d7a4faa01111c044910A184553"
}
],
"outputTokens": [
{
"proportion": 0.5,
"tokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
},
{
"proportion": 0.5,
"tokenAddress": "0xdAC17F958D2ee523a2206206994597C13D831ec7"
}
],
"referralCode": 0,
"slippageLimitPercent": 0.3,
"sourceBlacklist": [],
"sourceWhitelist": [],
"userAddr": "0x...",
"compact": true
}

Step 0: Check Zap Pool/Token is Routable

Before requesting a quote, the Odos Pricing service can be used to check if an address is routable for zapping using the GET /pricing/token/{chain_id}/{zap_token_addr} endpoint. If the request returns a price, then the token can be routed using the /sor/quote/v2/zap.

Below are some example requests that can be made programatically to check the price for an address.

import json
import requests

chain_id = 1
zap_token_addr = "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc"
zap_price_url = f"https://api.odos.xyz/pricing/token/{chain_id}/{zap_token_addr}"

response = requests.get(zap_price_url)

price_res = response.json()

if response.status_code == 200 and "price" in price_res:
print(price_res["price"])
# pool/token price is available and it can be used in routing
else:
print(f"Routing unavailable for {zap_token_addr}")
# handle pool/token routing unavailable

Step 1: Generate a Quote

The first step to execute a zap using the Odos SOR API is generating a quote. This can be achieved by sending a POST request to the /sor/quote/v2/zap endpoint, using the same request body format as the /sor/quote/v2 endpoint. It is recommended to start with one of the example request bodies above and customize as necessary.

For more in-depth explanation of specific request parameters, please check out the SOR Quick Start Guide.

import json
import requests

quote_url = "https://api.odos.xyz/sor/quote/v2/zap"

quote_request_body = {
"chainId": 1,
"inputTokens": [
{
"tokenAddress": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", # WETH token address
"amount": "1000000000000000000" # amount of WETH to use for zap-in
}
],
"outputTokens": [
{
"tokenAddress": "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc", # address of Uniswap V2 pool to zap in to
"proportion": 1 # proportion of input to use for Uniswap V2 zap
}
],
"slippageLimitPercent": 0.3, # set your slippage limit percentage (1 = 1%)
"userAddr": "0x...", # checksummed user address
"referralCode": 0, # referral code (recommended)
"compact": True,
}

response = requests.post(
quote_url,
headers={"Content-Type": "application/json"},
json=quote_request_body
)

if response.status_code == 200:
quote = response.json()
# handle quote response data
else:
print(f"Error in Quote: {response.json()}")
# handle quote failure cases

Replace the placeholder values in the request body with actual values for your desired quote. The userAddr should be the checksummed address of the user who may eventually assemble and execute the transaction.

You'll receive a response with the quote details. The pathId response field will be necessary for assembling your transactions in the next step. Paths are valid for 60 seconds after the quote is received, after which a new quote will have to be requested.

Step 2: Assemble the Transaction

In order to execute an Odos quote on chain, the transaction must be assembled. The Odos API will completely assemble the transaction for you, manual assembly is discouraged and not supported. Once you have the pathId from the quote, you can send a POST request to the /sor/assemble endpoint to get the on-chain transaction data. This is the same endpoint that is used to assemble quotes from /sor/quote/v2.

assemble_url = "https://api.odos.xyz/sor/assemble"

assemble_request_body = {
"userAddr": "0x...", # the checksummed address used to generate the quote
"pathId": quote["pathId"], # Replace with the pathId from quote response in step 1
"simulate": False, # this can be set to true if the user isn't doing their own estimate gas call for the transaction
}

response = requests.post(
assemble_url,
headers={"Content-Type": "application/json"},
json=assemble_request_body
)

if response.status_code == 200:
assembled_transaction = response.json()
# handle Transaction Assembly response data
else:
print(f"Error in Transaction Assembly: {response.json()}")
# handle Transaction Assembly failure cases

Replace the placeholder values in the request body with actual values. The userAddr should be the checksummed address of the user who requested the quote and will be executing the transaction.

The response will contain all of the necessary parameters for transaction execution in the transaction field which can be used to sign and execute the swap on-chain.

Step 3: Execute the Transaction

Once you have the transaction details from the /sor/assemble response, you can use the transaction object to sign and send directly with an EOA wallet or conduct a low level call from a smart contract with the transaction.data field. Reminder that modifying transaction.data or assembling any Odos router function call from scratch is not officially supported and 100% at your own risk.

Recommended Web3 libraries (examples below):

from web3 import Web3

# 1. create web3 provider
w3 = Web3(Web3.HTTPProvider("node http endpoint for your network"))

# 2. Extract transaction object from assemble API response
transaction = assembled_transaction["transaction"]

# 3. Sign tx with a private key
pk = "Your EOA private key"

# add the chainId to the transaction object from the API if signing raw transaction
transaction["chainId"] = 1

# web3py requires the value to be an integer
transaction["value"] = int(transaction["value"])

signed_tx = w3.eth.account.sign_transaction(transaction, pk)

# 4. Send the signed transaction
tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction)

Need Help?

  • For a deeper dive, the API Endpoints page is your go-to resource, providing an extensive look at all available endpoints, parameters, and more.
  • For technical assistance, visit our Support Page.
  • Join the Odos Discord Community to connect with other developers and our team.