Documentation Index
Fetch the complete documentation index at: https://docs.lidian.finance/llms.txt
Use this file to discover all available pages before exploring further.
Overview
This guide teaches you how to build your own liquidity provider service from scratch. You’ll learn the complete technical flow for:
- Polling for quote requests from users
- Calculating and submitting competitive quotes
- Monitoring for quote acceptance
- Executing the on-chain order fulfillment
- Settling cross-chain via LayerZero
- Withdrawing your earned fees
By the end, you’ll understand how to implement a production-ready LP service that earns 3-15 basis points per swap.
This is a technical implementation guide. You should be comfortable with TypeScript, blockchain transactions, and smart contract interactions.
Prerequisites
- Blockchain development experience: Understanding of transactions, gas, signatures
- TypeScript/JavaScript knowledge
- Web3 library: Viem, ethers.js, or web3.js
- Funded wallet with gas tokens and stablecoins on multiple chains
- API key from Lidian Developer Portal
Core Concepts
The LP Business Model
As an LP, you profit from the spread between input and output amounts:
User sends: 1000 USDC on Base Sepolia
LP quotes: 998.50 USDC on Polygon Amoy (15 bps fee = 1.50 USDC)
LP provides: 998.50 USDC from their liquidity
LP receives: 1000 USDC + 1.50 USDC fee (after settlement)
Net profit: 1.50 USDC (minus gas costs ~$0.10)
The Order Lifecycle
1. QUOTE REQUEST → LP receives user's swap intent
2. QUOTE CREATION → LP calculates output amount with fee
3. QUOTE ACCEPTANCE → User signs order with EIP-712
4. DEPOSIT → LP relays signed order to source chain contract
5. FILL → LP provides liquidity on destination chain
6. SETTLEMENT → LP initiates LayerZero message back to source
7. WITHDRAWAL → LP claims input tokens + earned fee
Step 1: Poll for Quote Requests
Fetch Quote Requests from API
interface QuoteRequest {
id: string;
inputChain: string; // e.g., "base-sepolia"
inputToken: string; // e.g., "0x036CbD..."
inputAmount: string; // e.g., "1000000" (1 USDC with 6 decimals)
outputChain: string; // e.g., "polygon-amoy"
outputToken: string; // e.g., "0x41E94..."
offerer: string; // User's wallet address
recipient: string; // Recipient's wallet address
startTime: number;
endTime: number;
}
async function pollForQuoteRequests() {
const response = await fetch('https://api.lidian.finance/api/v1/quote-requests', {
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.LIDIAN_API_KEY
}
});
const { data } = await response.json();
return data; // Array of QuoteRequest[]
}
Continuous Polling Loop
const processedRequests = new Set<string>();
async function monitorQuoteRequests() {
while (true) {
try {
const requests = await pollForQuoteRequests();
for (const request of requests) {
if (processedRequests.has(request.id)) continue;
console.log(`New quote request: ${request.id}`);
await handleQuoteRequest(request);
processedRequests.add(request.id);
}
} catch (error) {
console.error('Polling error:', error);
}
await sleep(2000); // Poll every 2 seconds
}
}
Step 2: Calculate Quote with Fee
Fee Structure
Different tokens have different basis point fees:
const TOKEN_FEES = {
'base-sepolia': {
'0x036CbD53842c5426634e7929541eC2318f3dCF7e': 15, // USDC: 15 bps
'0x66F5018cdb5d6Eb0a3d1AC57F8b77243eb0010fF': 5, // USDT: 5 bps
},
'polygon-amoy': {
'0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582': 3, // USDC: 3 bps
'0x86f6D9931BE92Ad61572e80c0F5FAe45950D4cA1': 3, // USDT: 3 bps
}
};
Calculate Output Amount
function calculateOutputAmount(
inputAmount: bigint,
inputDecimals: number,
outputDecimals: number,
feeBasisPoints: number
): bigint {
// For stablecoins (1:1 ratio), calculate fee deduction
// feeBasisPoints: 15 = 0.15% = 0.0015
const FEE_DENOMINATOR = 10000n;
const feeBps = BigInt(feeBasisPoints);
// Calculate output = input * (1 - fee)
// output = input * (10000 - feeBps) / 10000
const outputAmount = (inputAmount * (FEE_DENOMINATOR - feeBps)) / FEE_DENOMINATOR;
// Adjust for decimal differences if needed
if (inputDecimals !== outputDecimals) {
const decimalDiff = outputDecimals - inputDecimals;
if (decimalDiff > 0) {
return outputAmount * (10n ** BigInt(decimalDiff));
} else {
return outputAmount / (10n ** BigInt(-decimalDiff));
}
}
return outputAmount;
}
// Example:
// Input: 1,000,000 (1 USDC with 6 decimals)
// Fee: 15 bps
// Output: 1,000,000 * (10000 - 15) / 10000 = 998,500 (0.9985 USDC)
// LP earns: 1,500 (0.0015 USDC = $1.50 on $1000 swap)
Check Liquidity Availability
import { createPublicClient, http, parseUnits } from 'viem';
import { polygonAmoy } from 'viem/chains';
async function checkLiquidity(
chain: typeof polygonAmoy,
tokenAddress: string,
requiredAmount: bigint,
lpWallet: string
): Promise<boolean> {
const client = createPublicClient({
chain,
transport: http()
});
const balance = await client.readContract({
address: tokenAddress,
abi: [{
name: 'balanceOf',
type: 'function',
stateMutability: 'view',
inputs: [{ name: 'account', type: 'address' }],
outputs: [{ type: 'uint256' }]
}],
functionName: 'balanceOf',
args: [lpWallet]
});
return balance >= requiredAmount;
}
Step 3: Submit Quote to API
Create Quote Object
async function submitQuote(
quoteRequestId: string,
outputAmount: string
): Promise<string> {
const response = await fetch('https://api.lidian.finance/api/v1/quotes', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.LIDIAN_API_KEY
},
body: JSON.stringify({
quoteRequestId,
outputAmount,
liquidityProviderId: process.env.LP_WALLET_ADDRESS
})
});
const { data } = await response.json();
return data.id; // Returns quote ID (e.g., "quotes/abc123")
}
Handle Quote Request (Complete Flow)
async function handleQuoteRequest(request: QuoteRequest) {
// 1. Get token fee
const inputFee = TOKEN_FEES[request.inputChain]?.[request.inputToken] || 0;
const outputFee = TOKEN_FEES[request.outputChain]?.[request.outputToken] || 0;
const maxFee = Math.max(inputFee, outputFee);
// 2. Calculate output amount
const inputAmount = BigInt(request.inputAmount);
const outputAmount = calculateOutputAmount(
inputAmount,
6, // USDC decimals
6, // USDC decimals
maxFee
);
// 3. Check if we have liquidity
const hasLiquidity = await checkLiquidity(
getChain(request.outputChain),
request.outputToken,
outputAmount,
process.env.LP_WALLET_ADDRESS
);
if (!hasLiquidity) {
console.log('Insufficient liquidity, skipping quote');
return;
}
// 4. Submit quote
const quoteId = await submitQuote(request.id, outputAmount.toString());
console.log(`Quote submitted: ${quoteId} - Output: ${outputAmount.toString()}`);
// 5. Start monitoring for acceptance
monitorQuoteAcceptance(quoteId, request);
}
Step 4: Monitor for Quote Acceptance
Poll for Order Creation
When a user accepts your quote, they sign the order and create it in the API:
async function monitorQuoteAcceptance(
quoteId: string,
originalRequest: QuoteRequest
) {
const timeout = Date.now() + 300000; // 5 minute timeout
while (Date.now() < timeout) {
try {
// Fetch the quote to see if it was accepted
const response = await fetch(
`https://api.lidian.finance/api/v1/quotes/${quoteId}`,
{
headers: { 'x-api-key': process.env.LIDIAN_API_KEY }
}
);
const { data: quote } = await response.json();
// Check if order was created
if (quote.orderId) {
console.log(`Quote accepted! Order ID: ${quote.orderId}`);
await processOrder(quote.orderId, originalRequest, quote.outputAmount);
return;
}
// Check if quote expired/cancelled
if (quote.status === 'expired' || quote.status === 'cancelled') {
console.log(`Quote ${quoteId} ${quote.status}`);
return;
}
} catch (error) {
console.error('Error monitoring quote:', error);
}
await sleep(2000); // Poll every 2 seconds
}
console.log(`Quote monitoring timeout: ${quoteId}`);
}
Fetch Order Details
interface Order {
id: string;
signature: string; // EIP-712 signature from user
quoteId: string;
status: string;
moneyMoverAccountId: string; // User's wallet
liquidityProviderAccountId: string; // Your wallet
}
async function fetchOrder(orderId: string): Promise<Order> {
const response = await fetch(
`https://api.lidian.finance/api/v1/orders/${orderId}`,
{
headers: { 'x-api-key': process.env.LIDIAN_API_KEY }
}
);
const { data } = await response.json();
return data;
}
Step 5: Execute Deposit on Source Chain
Build Order Struct for Contract
import { encodeFunctionData, keccak256, encodePacked } from 'viem';
// Helper: Calculate chain ID hash
function getChainHash(chainKey: string): `0x${string}` {
return keccak256(encodePacked(['string'], [chainKey]));
}
interface OrderStruct {
inputAmount: bigint;
outputAmount: bigint;
inputToken: `0x${string}`;
outputToken: `0x${string}`;
startTime: number;
endTime: number;
srcChainId: `0x${string}`;
dstChainId: `0x${string}`;
offerer: `0x${string}`;
recipient: `0x${string}`;
}
Call deposit() on Lidian Contract
import { createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { baseSepolia } from 'viem/chains';
const LIDIAN_CONTRACT = '0xc2c00E85ab6bcf865986DE84053a6169bCBfa70F'; // Base Sepolia
const LIDIAN_ABI = [{
name: 'deposit',
type: 'function',
stateMutability: 'nonpayable',
inputs: [
{
name: 'order',
type: 'tuple',
components: [
{ name: 'inputAmount', type: 'uint128' },
{ name: 'outputAmount', type: 'uint128' },
{ name: 'inputToken', type: 'address' },
{ name: 'outputToken', type: 'address' },
{ name: 'startTime', type: 'uint32' },
{ name: 'endTime', type: 'uint32' },
{ name: 'srcChainId', type: 'bytes32' },
{ name: 'dstChainId', type: 'bytes32' },
{ name: 'offerer', type: 'address' },
{ name: 'recipient', type: 'address' }
]
},
{ name: 'signature', type: 'bytes' }
],
outputs: []
}];
async function executeDeposit(
order: OrderStruct,
signature: `0x${string}`
) {
const account = privateKeyToAccount(process.env.LP_PRIVATE_KEY as `0x${string}`);
const client = createWalletClient({
account,
chain: baseSepolia,
transport: http()
});
console.log('Executing deposit on source chain...');
const hash = await client.writeContract({
address: LIDIAN_CONTRACT,
abi: LIDIAN_ABI,
functionName: 'deposit',
args: [order, signature]
});
console.log(`Deposit tx: ${hash}`);
// Wait for confirmation
const receipt = await client.waitForTransactionReceipt({ hash });
console.log(`Deposit confirmed at block ${receipt.blockNumber}`);
return receipt;
}
Important: The deposit transaction uses the user’s signature to pull tokens from their wallet. The LP does not need to hold the input tokens.
Step 6: Execute Fill on Destination Chain
Approve Token Spending (if needed)
const ERC20_ABI = [{
name: 'approve',
type: 'function',
stateMutability: 'nonpayable',
inputs: [
{ name: 'spender', type: 'address' },
{ name: 'amount', type: 'uint256' }
],
outputs: [{ type: 'bool' }]
}];
async function approveTokenIfNeeded(
tokenAddress: `0x${string}`,
spenderAddress: `0x${string}`,
amount: bigint
) {
const account = privateKeyToAccount(process.env.LP_PRIVATE_KEY as `0x${string}`);
const client = createWalletClient({
account,
chain: polygonAmoy,
transport: http()
});
// Check current allowance
const publicClient = createPublicClient({
chain: polygonAmoy,
transport: http()
});
const allowance = await publicClient.readContract({
address: tokenAddress,
abi: [{
name: 'allowance',
type: 'function',
stateMutability: 'view',
inputs: [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' }
],
outputs: [{ type: 'uint256' }]
}],
functionName: 'allowance',
args: [account.address, spenderAddress]
});
if (allowance >= amount) {
console.log('Sufficient allowance already exists');
return;
}
// Approve tokens
console.log('Approving tokens...');
const hash = await client.writeContract({
address: tokenAddress,
abi: ERC20_ABI,
functionName: 'approve',
args: [spenderAddress, amount]
});
await client.waitForTransactionReceipt({ hash });
console.log('Approval confirmed');
}
Call fill() on Destination Chain
const LIDIAN_CONTRACT_AMOY = '0x7bE37DE6d81C6B231c8579901C6717f1A08c41ed'; // Polygon Amoy
async function executeFill(order: OrderStruct) {
const account = privateKeyToAccount(process.env.LP_PRIVATE_KEY as `0x${string}`);
const client = createWalletClient({
account,
chain: polygonAmoy,
transport: http()
});
// 1. Approve Lidian contract to spend output tokens
await approveTokenIfNeeded(
order.outputToken,
LIDIAN_CONTRACT_AMOY,
order.outputAmount
);
// 2. Call fill()
console.log('Executing fill on destination chain...');
const hash = await client.writeContract({
address: LIDIAN_CONTRACT_AMOY,
abi: [{
name: 'fill',
type: 'function',
stateMutability: 'payable',
inputs: [{
name: 'order',
type: 'tuple',
components: [
{ name: 'inputAmount', type: 'uint128' },
{ name: 'outputAmount', type: 'uint128' },
{ name: 'inputToken', type: 'address' },
{ name: 'outputToken', type: 'address' },
{ name: 'startTime', type: 'uint32' },
{ name: 'endTime', type: 'uint32' },
{ name: 'srcChainId', type: 'bytes32' },
{ name: 'dstChainId', type: 'bytes32' },
{ name: 'offerer', type: 'address' },
{ name: 'recipient', type: 'address' }
]
}],
outputs: []
}],
functionName: 'fill',
args: [order]
});
console.log(`Fill tx: ${hash}`);
const receipt = await client.waitForTransactionReceipt({ hash });
console.log(`Fill confirmed at block ${receipt.blockNumber}`);
return receipt;
}
After fill, the LP’s output tokens are locked in the destination chain contract. You must complete settlement to unlock your funds on the source chain.
Step 7: Settle via LayerZero
Quote LayerZero Fee
const LZ_ADAPTER_AMOY = '0x9ecf258aA68a483ac89057cBB9deAD71D6a7a3E0';
const ADAPTER_ABI = [{
name: 'quote',
type: 'function',
stateMutability: 'view',
inputs: [
{ name: 'dstChainId', type: 'bytes32' },
{ name: 'msgType', type: 'uint8' },
{ name: '_options', type: 'bytes' },
{ name: 'payInLzToken', type: 'bool' },
{ name: 'srcChainId', type: 'bytes32' },
{ name: 'sender', type: 'address' }
],
outputs: [{ name: 'nativeFee', type: 'uint256' }]
}];
async function quoteLzFee(
srcChainHash: `0x${string}`,
dstChainHash: `0x${string}`,
lpAddress: `0x${string}`
): Promise<bigint> {
const client = createPublicClient({
chain: polygonAmoy,
transport: http()
});
// LayerZero options: 300k gas for lzReceive
const options = '0x00030100110100000000000000000000000000030d40'; // 300k gas
const fee = await client.readContract({
address: LZ_ADAPTER_AMOY,
abi: ADAPTER_ABI,
functionName: 'quote',
args: [
dstChainHash, // destination (source chain)
0, // msgType = settlement
options,
false, // don't pay in LZ token
srcChainHash, // source chain
lpAddress // filler address
]
});
return fee as bigint;
}
Execute Settlement
async function executeSettlement(
srcChainHash: `0x${string}`,
dstChainHash: `0x${string}`
) {
const account = privateKeyToAccount(process.env.LP_PRIVATE_KEY as `0x${string}`);
const client = createWalletClient({
account,
chain: polygonAmoy,
transport: http()
});
// 1. Quote the LayerZero fee
const lzFee = await quoteLzFee(srcChainHash, dstChainHash, account.address);
console.log(`LayerZero fee: ${lzFee.toString()} wei`);
// 2. Call settle() with LZ fee as msg.value
console.log('Executing settlement...');
const options = '0x00030100110100000000000000000000000000030d40';
const hash = await client.writeContract({
address: LZ_ADAPTER_AMOY,
abi: [{
name: 'settle',
type: 'function',
stateMutability: 'payable',
inputs: [
{ name: 'filler', type: 'address' },
{ name: 'dstChainId', type: 'bytes32' },
{ name: '_options', type: 'bytes' }
],
outputs: []
}],
functionName: 'settle',
args: [account.address, dstChainHash, options],
value: lzFee
});
console.log(`Settlement tx: ${hash}`);
const receipt = await client.waitForTransactionReceipt({ hash });
console.log(`Settlement confirmed at block ${receipt.blockNumber}`);
return receipt;
}
Settlement triggers a LayerZero cross-chain message. It takes ~30-60 seconds for the message to be delivered and verified on the source chain.
Step 8: Withdraw Funds + Fee
Wait for Settlement Delivery
async function waitForSettlement(orderId: string): Promise<boolean> {
const timeout = Date.now() + 120000; // 2 minute timeout
while (Date.now() < timeout) {
try {
// Check if settlement message was delivered
// You can track this via LayerZero scan or contract events
const settled = await checkIfSettled(orderId);
if (settled) {
console.log('Settlement message delivered');
return true;
}
} catch (error) {
console.error('Error checking settlement:', error);
}
await sleep(5000); // Check every 5 seconds
}
console.log('Settlement timeout');
return false;
}
Execute Withdrawal
async function executeWithdrawal(
inputToken: `0x${string}`,
amount: bigint
) {
const account = privateKeyToAccount(process.env.LP_PRIVATE_KEY as `0x${string}`);
const client = createWalletClient({
account,
chain: baseSepolia,
transport: http()
});
console.log('Executing withdrawal...');
console.log(`Amount: ${amount.toString()}`);
const hash = await client.writeContract({
address: LIDIAN_CONTRACT,
abi: [{
name: 'withdraw',
type: 'function',
stateMutability: 'nonpayable',
inputs: [
{ name: 'token', type: 'address' },
{ name: 'amount', type: 'uint256' }
],
outputs: []
}],
functionName: 'withdraw',
args: [inputToken, amount]
});
console.log(`Withdrawal tx: ${hash}`);
const receipt = await client.waitForTransactionReceipt({ hash });
console.log(`Withdrawal confirmed at block ${receipt.blockNumber}`);
console.log('✅ Order complete! Fees earned.');
return receipt;
}
Complete Implementation Example
async function processOrder(
orderId: string,
quoteRequest: QuoteRequest,
outputAmount: string
) {
try {
// 1. Fetch order with signature
const order = await fetchOrder(orderId);
// 2. Build order struct
const orderStruct: OrderStruct = {
inputAmount: BigInt(quoteRequest.inputAmount),
outputAmount: BigInt(outputAmount),
inputToken: quoteRequest.inputToken as `0x${string}`,
outputToken: quoteRequest.outputToken as `0x${string}`,
startTime: quoteRequest.startTime,
endTime: quoteRequest.endTime,
srcChainId: getChainHash(quoteRequest.inputChain),
dstChainId: getChainHash(quoteRequest.outputChain),
offerer: quoteRequest.offerer as `0x${string}`,
recipient: quoteRequest.recipient as `0x${string}`
};
// 3. Deposit on source chain
await executeDeposit(orderStruct, order.signature as `0x${string}`);
// 4. Fill on destination chain
await executeFill(orderStruct);
// 5. Settle back to source chain
await executeSettlement(
orderStruct.srcChainId,
orderStruct.dstChainId
);
// 6. Wait for settlement delivery
const settled = await waitForSettlement(orderId);
if (!settled) {
throw new Error('Settlement timeout');
}
// 7. Withdraw funds + fee
const withdrawAmount = orderStruct.inputAmount; // This includes your fee
await executeWithdrawal(orderStruct.inputToken, withdrawAmount);
console.log(`✅ Order ${orderId} completed successfully!`);
} catch (error) {
console.error(`❌ Order processing failed:`, error);
throw error;
}
}
Profit Calculation
After completing an order, calculate your earnings:
function calculateProfit(
inputAmount: bigint,
outputAmount: bigint,
gasUsed: {
deposit: bigint;
fill: bigint;
settle: bigint;
withdraw: bigint;
},
gasPrice: bigint
): bigint {
// Fee earned (difference between input and output)
const feeEarned = inputAmount - outputAmount;
// Total gas cost
const totalGas = gasUsed.deposit + gasUsed.fill + gasUsed.settle + gasUsed.withdraw;
const gasCost = totalGas * gasPrice;
// Net profit (in wei)
const netProfit = feeEarned - gasCost;
return netProfit;
}
// Example:
// Input: 1,000 USDC (1,000,000,000 with 6 decimals)
// Output: 998.5 USDC (998,500,000 with 6 decimals)
// Fee earned: 1.5 USDC (1,500,000)
// Gas cost: ~$0.10 (100,000 wei assuming USDC = ETH for simplicity)
// Net profit: 1.4 USDC ($1.40)
Production Considerations
Error Handling
async function processOrderWithRetry(
orderId: string,
maxRetries: number = 3
) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await processOrder(orderId, quoteRequest, outputAmount);
return; // Success
} catch (error) {
console.error(`Attempt ${attempt} failed:`, error);
if (attempt === maxRetries) {
// Final failure - alert admin
await sendAlert(`Order ${orderId} failed after ${maxRetries} attempts`);
throw error;
}
// Wait before retry (exponential backoff)
await sleep(Math.pow(2, attempt) * 1000);
}
}
}
Balance Monitoring
async function checkBalances(): Promise<void> {
const chains = ['base-sepolia', 'polygon-amoy'];
for (const chain of chains) {
const balance = await getTokenBalance(
chain,
USDC_ADDRESS,
LP_WALLET
);
if (balance < MIN_BALANCE_THRESHOLD) {
await sendAlert(`Low balance on ${chain}: ${balance}`);
}
}
}
// Run every 5 minutes
setInterval(checkBalances, 5 * 60 * 1000);
Nonce Management
class NonceManager {
private nonces = new Map<string, number>();
async getNonce(address: string, chainId: number): Promise<number> {
const key = `${address}-${chainId}`;
if (!this.nonces.has(key)) {
const nonce = await fetchOnChainNonce(address, chainId);
this.nonces.set(key, nonce);
}
return this.nonces.get(key)!;
}
incrementNonce(address: string, chainId: number): void {
const key = `${address}-${chainId}`;
const current = this.nonces.get(key) || 0;
this.nonces.set(key, current + 1);
}
}
Testing Your Implementation
Testnet Setup
-
Get testnet tokens from faucets:
-
Fund your LP wallet with:
- Gas tokens (ETH, MATIC)
- Stablecoins (USDC, USDT)
-
Test with small amounts first:
- Start with $1-10 swaps
- Verify all transactions succeed
- Check balance updates
Debug Logging
function logOrderProgress(step: string, data: any) {
console.log(`[${new Date().toISOString()}] ${step}`, {
orderId: data.orderId,
chain: data.chain,
txHash: data.txHash,
gasUsed: data.gasUsed,
...data
});
}
Contract Addresses
Testnet Contracts
| Network | Lidian Contract | LayerZero Adapter |
|---|
| Base Sepolia | 0xc2c00E85ab6bcf865986DE84053a6169bCBfa70F | 0xe6dF658A8f53D47fec5294F351Adf11111C6fa01 |
| Polygon Amoy | 0x7bE37DE6d81C6B231c8579901C6717f1A08c41ed | 0x9ecf258aA68a483ac89057cBB9deAD71D6a7a3E0 |
| Avalanche Fuji | 0x875200c20719eB6914405c5C989226F97418e928 | 0xDfaF9D6f669625eb63C98F3A34d03eC06ea2A073 |
Next Steps
Now that you understand the implementation:
- Build your service using this guide as a reference
- Test thoroughly on testnets before mainnet
- Monitor performance and optimize for your use case
- Scale gradually as you gain confidence
For production API keys and support, contact the Lidian team.
Additional Resources