For Frontends
Use the Liquid Lane API to redeem supported RWAs for USDC in your app. Connect the user's wallet, request approval when needed, and sign an EIP-712 order.
| Network | Base URL |
|---|---|
| Mainnet | https://swap.symbiotic.fi/api/v1 |
| Hoodi | https://swap.hoodi.symbiotic.fi/api/v1 |
| Sepolia | https://swap.sepolia.symbiotic.fi/api/v1 |
Use the matching chain and Reactor address. See the API reference for full schemas.
Integration Steps
Request a quote
Call POST /quote with the tokens, amount, wallet (swapper), and output recipients. Use type: "EXACT_INPUT", set both chain IDs to the deployment chain (1 for mainnet), and set each output's token to tokenOut.
The response contains quotes, sorted best-first. Each entry includes solver, quote, and signatureData. Choose quotes[0] for the best quote. Display the user's amount from the selected quote.orderInfo.outputs, formatted with the token's decimals. Refresh quotes while the user reviews them.
404means no quote is available. Let the user change the amount or try again.
Approve the Reactor
Call POST /check_approval with { walletAddress, chainId, token, amount }. The backend returns unsigned { to, data, value } payloads for the wallet to sign and submit:
- Sufficient allowance:
cancelandapprovalare bothnull; continue to signing. - Zero allowance: send only
approval, which grants the Reactor an unlimited ERC-20 allowance. - Nonzero but insufficient allowance: send
cancelfirst to reset the Reactor's allowance to zero, then sendapproval.
Wait for each transaction to succeed before sending the next one or signing the order. Wallets with atomic batching support can execute [cancel, approval] together in that order. cancel resets the token allowance; it does not cancel an order.
Sign
Sign signatureData as EIP-712 typed data with primaryType: "Request", passing value as the message.
Submit the order
Call POST /order with { quote, signature }, keeping the quote unchanged. Persist the returned orderId before polling. Retrying the same quote and signature returns the existing order.
404means the quote is unknown.409means it expired or no solver can honor it. Request a new quote and signature.
Track settlement
Poll GET /orders?orderId=<orderId> every few seconds.
orderStatus | Action |
|---|---|
open | Keep polling. |
filled | Show txHash and settledAmounts. |
expired, error, cancelled, insufficient-funds | Offer a new quote. |
Bound each request and the polling window. A timeout does not cancel an order: retain its ID and resume tracking after reload.
Examples
async function (: string, : object) {
const = "https://swap.symbiotic.fi/api/v1";
const = await (`${}${}`, {
: "POST",
: { "Content-Type": "application/json" },
: .(),
: .(15_000),
});
if (!.) throw new (`${}: HTTP ${.}`);
return .();
}
import type { } from "viem";
const [] = await .();
// Quote
const { } = await ("/quote", {
: 1,
: 1,
,
,
,
: "EXACT_INPUT",
,
: [{ : , : }],
});
// Choose the best quote (highest output)
const { , } = [0];
// Approve the Reactor if needed
const { , } = await ("/check_approval", {
: ,
: 1,
: ,
,
});
for (const of [, ]) {
if (!) continue;
const = await .({
: ,
...,
: (.),
});
const = await .({ });
if (. !== "success") throw new ("Approval reverted.");
}
// Sign
if (.. <= .() / 1000)
throw new ("Quote expired. Request a new quote.");
const { , , , } = ;
const = await .({
: ,
,
: as unknown as ,
,
: ,
});
// Submit; save orderId to track settlement
const { } = await ("/order", { , });async function (: string, : object) {
const = "https://swap.symbiotic.fi/api/v1";
const = await (`${}${}`, {
: "POST",
: { "Content-Type": "application/json" },
: .(),
: .(15_000),
});
if (!.) throw new (`${}: HTTP ${.}`);
return .();
}
import { , } from "@fireblocks/ts-sdk";
import type { , } from "viem";
const = { : ., : "0" };
const = ..! as ; // Address of vault 0.
async function (: string) {
for (let = 0; < 40; ++) {
const { } = await ..({ });
if (. === "COMPLETED") return ;
if (["FAILED", "BLOCKED", "CANCELLED", "REJECTED", "TIMEOUT"].(. ?? ""))
throw new (`Fireblocks: ${.}`);
await new (() => (, 3_000));
}
throw new (`Check Fireblocks transaction ${} before retrying.`);
}
// Quote
const { } = await ("/quote", {
: 1,
: 1,
,
,
,
: "EXACT_INPUT",
,
: [{ : , : }],
});
// Choose the best quote (highest output)
const { , } = [0];
// Approve the Reactor if needed
const { , } = await ("/check_approval", {
: ,
: 1,
: ,
,
});
for (const of [, ]) {
if (!) continue;
const { } = await ..({
: {
: .,
: "ETH",
,
: {
: .,
: { : . },
},
: "0",
: { : . },
},
});
await (.!);
}
// Sign
if (.. <= .() / 1000)
throw new ("Quote expired. Request a new quote.");
const { , , , } = ;
const = [
{ : "name", : "string" },
{ : "version", : "string" },
{ : "chainId", : "uint256" },
{ : "verifyingContract", : "address" },
];
const { } = await ..({
: {
: .,
: "ETH",
,
: {
: {
: [
{
: "EIP712",
: {
: { , ... },
,
,
: ,
},
},
],
},
},
},
});
const = await (.!);
const { , , } = .![0]!.!;
const : = `0x${}${}${(! + 27).(16)}`;
// Submit; save orderId to track settlement
if (.. <= .() / 1000)
throw new ("Quote expired. Request a new quote.");
const { } = await ("/order", { , });Fees
Add an output with your fee recipient and portionBps (25 = 0.25%). Exactly one output must omit portionBps to receive the remainder.
{
"outputs": [
{ "token": "0xUsdc" },
{ "token": "0xUsdc", "recipient": "0xYourFeeWallet", "portionBps": 25 }
]
}Solvers
Each quotes[] entry includes solver metadata: use solver.name for the display name and solver.metadata.logoUrl for the logo when present.
The following solvers are configured on Mainnet:
| Solver ID | Name | Network |
|---|---|---|
symbiotic_symbiotic | Symbiotic | Mainnet |
symbiotic_keyrock | Keyrock | Mainnet |
symbiotic_kpk | KPK | Mainnet |
symbiotic_cassa | Cassa | Mainnet |
symbiotic_clearstar | Clearstar | Mainnet |
symbiotic_presto | Presto | Mainnet |
symbiotic_infinifi (InfiniFi) and symbiotic_bizantine (Bizantine) are reserved IDs and are not currently configured on Mainnet.
Use the solver IDs above in quote and liquidity filters.
Liquidity
Call POST /liquidity with { tokenIn } and optional solverId or solverIds. The response includes tokenIn, tokenOut, their metadata (tokenInInfo, tokenOutInfo), and these liquidity fields:
{
"totalLiquidity": "2500000000",
"solvers": [
{ "solverId": "symbiotic_symbiotic", "liquidity": "2000000000" },
{ "solverId": "symbiotic_keyrock", "liquidity": "1000000000" }
]
}Each solver's liquidity is the sum of the effective adapters[].maxAssets available to it during quote construction, after authorization and live-discount validation. The solvers array is sorted by liquidity from highest to lowest.
totalLiquidity is not the sum of the solver values. It counts the shared internal inventory once, then adds each external solver's liquidity, so it can differ from the sum in either direction:
- Lower when solvers share adapters. In the example above, 500 tokens of inventory back both solvers and count once, so the total is 2,500 rather than 3,000.
- Higher when eligible internal inventory is not bound to any returned solver. Internal solvers still receive that inventory in quote requests, but it has no solver row.
The total is never below any single solver's liquidity. Validate each value on its own and don't require the solver values to add up to the total.
Pass solverId: "symbiotic_keyrock" to restrict both the list and totalLiquidity to that solver. In the example above, the list then contains only symbiotic_keyrock and the total becomes "1000000000". If that solver has no eligible liquidity, the total is "0" and the list is empty. The solverIds array filters both the list and the total when solverId is omitted. If both are supplied, solverId takes precedence.
Amounts are integer strings in tokenOut base units. Format them with tokenOutInfo.decimals: the example total is 2,500 tokens when decimals is 6. Use BigInt for sums and comparisons.
Read several assets at once
To read several input tokens in one request, call POST /liquidity-batch with tokensIn and the same optional solverId or solverIds:
{ "tokensIn": ["0x…", "0x…"] }tokensIntakes 1 to 20 distinct addresses. Duplicates are compared case-insensitively and rejected with HTTP 400.- The response is
{ tokensIn: [...] }, holding the/liquiditysnapshot shown above for each token, in request order. solverIdandsolverIdsapply to every token.- If any token is unsupported, the whole request fails with HTTP 400 before any inventory is read, and the error details name that token.
POST /liquidity stays single-token: it takes tokenIn and returns one snapshot.
Show liquidity in a frontend
Use totalLiquidity to sort assets, loading it in the background with one /liquidity-batch request for the whole asset list. For Capacity: <capacity>, match the selected quote's solver.id to solvers[].solverId and display that solver's liquidity. Solvers without eligible inventory are omitted. Empty inventory returns HTTP 200 with totalLiquidity: "0" and solvers: [].
This response replaces the sampled levels and price-impact fields. It reads inventory without requesting solver quotes and does not reserve funds. Request a fresh /quote for the user's chosen amount before signing.
API Errors
Error responses contain { error: { code, message, status, timestamp, details? } }. Validation errors include details.issues[].
For support, record the HTTP status and X-Request-Id response header on both successful and failed requests. The backend exposes this header through CORS. Treat the ID as an opaque string.
After collecting wallet signatures for a discount, submit the signed terms to POST /discount and wait for HTTP 200 before marking it published. Publication and cancellation return { discountId }. Read request correlation from the header, which identifies the HTTP attempt and is not an idempotency key.