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
- Remove liquidity from WETH/DAI
- Swap WETH for UNI
- Swap DAI for USDC
- 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.
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.
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:
- 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.
- 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.
- Python
- JavaScript
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
const chainId = 1;
const zapTokenAddr = '0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc';
const zapPriceUrl = `https://api.odos.xyz/pricing/token/${chainId}/${zapTokenAddr}`;
const response = await fetch(zapPriceUrl);
const priceRes = await response.json();
if (response.status === 200 && priceRes.price) {
console.log(priceRes.price)
// pool/token price is available and it can be used in routing
} else {
console.error('Error in Quote:', response);
// 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.
- Python
- JavaScript
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
const quoteUrl = 'https://api.odos.xyz/sor/quote/v2/zap';
const quoteRequestBody = {
chainId: 1, // Replace with desired chainId
inputTokens: [
{
tokenAddress: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', // WETH token address 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
}
],
userAddr: '0x...', // checksummed user address
slippageLimitPercent: 0.3, // set your slippage limit percentage (1 = 1%),
referralCode: 0, // referral code (recommended)
compact: true,
};
const response = await fetch(
quoteUrl,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(quoteRequestBody),
});
if (response.status === 200) {
const quote = await response.json();
// handle quote response data
} else {
console.error('Error in Quote:', response);
// 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
.
- Python
- JavaScript
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
const assembleUrl = 'https://api.odos.xyz/sor/assemble';
const assembleRequestBody = {
userAddr: '0x...', // the checksummed address used to generate the quote
pathId: quote.pathId, // Replace with the pathId from quote response in step 1
simulate: true, // this can be set to true if the user isn't doing their own estimate gas call for the transaction
};
const response = await fetch(
assembleUrl,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(assembleRequestBody),
});
if (response.status === 200) {
const assembledTransaction = await response.json();
// handle Transaction Assembly response data
} else {
console.error('Error in Transaction Assembly:', response);
// handle quote 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):
- Python: web3py (Requires installation from here)
- Javascript: web3.js (Requires installation from here)
- Python
- JavaScript
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)
const Web3 = require('web3');
// 1. create web3 provider
const web3 = new Web3('node http endpoint for your network or wallet provider');
// 2. Extract transaction object from assemble API response
const transaction = assembledTransaction.transaction;
let txHash;
// 3a. Sign transaction with a web3 provider / wallet
txHash = await web3.eth.accounts.signTransaction(transaction);
// 3b. sign transaction with private key
const pk = 'Your EOA private key';
const signedTx = web3.eth.accounts.signTransaction(transaction, pk);
txHash = await web3.eth.send_raw_transaction(signedTx.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.