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 }. If approval is null, continue to signing. Otherwise, send its { to, data, value } transaction and wait for a successful receipt. It grants the Reactor an unlimited ERC-20 allowance.
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
const API = "https://swap.symbiotic.fi/api/v1";
async function post(path: string, body: object) {
const response = await fetch(`${API}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
signal: AbortSignal.timeout(15_000),
});
if (!response.ok) throw new Error(`${path}: HTTP ${response.status}`);
return response.json();
}
const [swapper] = await walletClient.getAddresses();
// Quote
const { quotes } = await post("/quote", {
tokenInChainId: 1,
tokenOutChainId: 1,
tokenIn,
tokenOut,
amount,
type: "EXACT_INPUT",
swapper,
outputs: [{ token: tokenOut, recipient: swapper }],
});
// Choose the best quote (highest output)
const { quote, signatureData } = quotes[0];
// Approve the Reactor if needed
const { approval } = await post("/check_approval", {
walletAddress: swapper,
chainId: 1,
token: tokenIn,
amount,
});
if (approval) {
const hash = await walletClient.sendTransaction({
account: swapper,
...approval,
value: BigInt(approval.value),
});
const receipt = await publicClient.waitForTransactionReceipt({ hash });
if (receipt.status !== "success") throw new Error("Approval reverted.");
}
// Sign
if (quote.orderInfo.deadline <= Date.now() / 1000)
throw new Error("Quote expired. Request a new quote.");
const { domain, types, primaryType, value } = signatureData;
const signature = await walletClient.signTypedData({
account: swapper,
domain,
types,
primaryType,
message: value,
});
// Submit; save orderId to track settlement
const { orderId } = await post("/order", { quote, signature });import { TransactionOperation, TransferPeerPathType } from "@fireblocks/ts-sdk";
const API = "https://swap.symbiotic.fi/api/v1";
async function post(path: string, body: object) {
const response = await fetch(`${API}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
signal: AbortSignal.timeout(15_000),
});
if (!response.ok) throw new Error(`${path}: HTTP ${response.status}`);
return response.json();
}
const source = { type: TransferPeerPathType.VaultAccount, id: "0" };
const swapper = process.env.FIREBLOCKS_SWAPPER_ADDRESS!; // Address of vault 0.
async function waitForCompletion(txId: string) {
for (let attempt = 0; attempt < 40; attempt++) {
const { data } = await fireblocks.transactions.getTransaction({ txId });
if (data.status === "COMPLETED") return data;
if (["FAILED", "BLOCKED", "CANCELLED", "REJECTED", "TIMEOUT"].includes(data.status ?? ""))
throw new Error(`Fireblocks: ${data.status}`);
await new Promise((resolve) => setTimeout(resolve, 3_000));
}
throw new Error(`Check Fireblocks transaction ${txId} before retrying.`);
}
// Quote
const { quotes } = await post("/quote", {
tokenInChainId: 1,
tokenOutChainId: 1,
tokenIn,
tokenOut,
amount,
type: "EXACT_INPUT",
swapper,
outputs: [{ token: tokenOut, recipient: swapper }],
});
// Choose the best quote (highest output)
const { quote, signatureData } = quotes[0];
// Approve the Reactor if needed
const { approval } = await post("/check_approval", {
walletAddress: swapper,
chainId: 1,
token: tokenIn,
amount,
});
if (approval) {
const { data } = await fireblocks.transactions.createTransaction({
transactionRequest: {
operation: TransactionOperation.ContractCall,
assetId: "ETH",
source,
destination: {
type: TransferPeerPathType.OneTimeAddress,
oneTimeAddress: { address: approval.to },
},
amount: "0",
extraParameters: { contractCallData: approval.data },
},
});
await waitForCompletion(data.id!);
}
// Sign
if (quote.orderInfo.deadline <= Date.now() / 1000)
throw new Error("Quote expired. Request a new quote.");
const { domain, types, primaryType, value } = signatureData;
const EIP712Domain = [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "chainId", type: "uint256" },
{ name: "verifyingContract", type: "address" },
];
const { data } = await fireblocks.transactions.createTransaction({
transactionRequest: {
operation: TransactionOperation.TypedMessage,
assetId: "ETH",
source,
extraParameters: {
rawMessageData: {
messages: [
{
type: "EIP712",
content: {
types: { EIP712Domain, ...types },
domain,
primaryType,
message: value,
},
},
],
},
},
},
});
const signed = await waitForCompletion(data.id!);
const { r, s, v } = signed.signedMessages![0]!.signature!;
const signature = `0x${r}${s}${(v! + 27).toString(16)}`;
// Submit; save orderId to track settlement
if (quote.orderInfo.deadline <= Date.now() / 1000)
throw new Error("Quote expired. Request a new quote.");
const { orderId } = await post("/order", { quote, signature });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.
| Solver ID | Name | Network |
|---|---|---|
symbiotic_first | Symbiotic | Mainnet |
symbiotic_second | Keyrock | Mainnet |
symbiotic_third | KPK | Mainnet |
symbiotic_fifth | Clearstar | Mainnet |
Liquidity Preview
Call POST /liquidity with { tokenIn } and optional solverIds to preview depth. Each levels[] entry includes an input amount and a bestQuote, or null if no solver quoted that size. bestQuote.priceImpactBps measures the shortfall from the reference price (50 = 0.50%).
Use these levels as estimates. Request a /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[]. Log the requestId from successful responses for support.
