Skip to main contentSkip to FAQSkip to contact

This guide walks you through connecting your pricing engine to TetraFi as a solver (liquidity provider). By the end, you'll be able to receive RFQs, submit competitive quotes, and fill settlements.

Track your progress through each step:

Integration Checklist

Before starting, ensure you have:

  • A TetraFi API key (use sandbox for development)
  • Node.js 18+ or Python 3.10+
  • A wallet with testnet stablecoins for sandbox settlement
  • Business entity verification (KYB) completed
TypeScript
1# Install SDK
2npm install @tetrafi/sdk
3
4# Set environment variables
5export TETRAFI_API_KEY=sk_test_...
6export TETRAFI_BASE_URL=https://sandbox.tetrafi.io/v1
7export TETRAFI_ENVIRONMENT=sandbox
7 linesbash

Use sk_test_ keys for sandbox development. Sandbox never touches real funds and compliance checks are mocked. Switch to sk_live_ only when ready for production.

Register your solver entity with your KYB details, supported trading pairs, and capacity limits:

TypeScript
1import { TetraFi } from "@tetrafi/sdk";
2
3const tetrafi = new TetraFi({
4 apiKey: process.env.TETRAFI_API_KEY!,
5 environment: "sandbox",
6});
7
8const registration = await tetrafi.solvers.register({
9 name: "Acme Market Making LLC",
10 entityType: "company",
11 jurisdictions: ["US", "EU"],
12 supportedPairs: ["USDC/USDT", "USDC/EURC", "USDT/EURC"],
13 corridors: ["ethereum-optimism", "ethereum-base"],
14 minTradeSize: "10000", // USD equivalent
15 maxTradeSize: "10000000", // USD equivalent
16 walletAddress: "0x...",
17});
18
19console.log("Solver ID:", registration.solverId);
20console.log("Status:", registration.status); // "pending_review" | "approved"
20 linestypescript

Registration triggers a KYB review. In sandbox mode, review is instant. In production, expect 1-3 business days. You'll receive a webhook notification when approved.

Subscribe to incoming RFQs via WebSocket. Your pricing engine must respond within the auction window (typically 5 seconds):

TypeScript
1import WebSocket from "ws";
2
3const ws = new WebSocket("wss://api.tetrafi.io/v1/solver/stream");
4
5ws.on("open", () => {
6 // Authenticate
7 ws.send(JSON.stringify({
8 type: "auth",
9 token: process.env.TETRAFI_API_KEY,
10 }));
11
12 // Subscribe to RFQ events
13 ws.send(JSON.stringify({
14 type: "subscribe",
15 channels: ["rfq.*"],
16 corridors: ["ethereum-optimism", "ethereum-base"],
17 }));
18
19 console.log("Connected to TetraFi RFQ stream");
20});
21
22// Heartbeat - server sends ping every 30s
23ws.on("ping", () => ws.pong());
24
25ws.on("message", (data) => {
26 const event = JSON.parse(data.toString());
27 if (event.type === "rfq.created") {
28 handleRFQ(event.rfq);
29 }
30});
31
32// Reconnect with exponential backoff
33ws.on("close", () => {
34 setTimeout(() => reconnect(), 1000 * Math.min(30, 2 ** retryCount++));
35});
35 linestypescript

Implement WebSocket reconnection with exponential backoff. A dropped connection means missed RFQs. The server sends a ping every 30 seconds - you must respond with pong to stay connected.

Validate each RFQ against your supported pairs and size limits before pricing:

TypeScript
1interface RFQ {
2 id: string;
3 pair: string; // e.g., "USDC/USDT"
4 side: "buy" | "sell";
5 amount: string; // In source token
6 corridor: string; // e.g., "ethereum-optimism"
7 deadline: number; // Unix timestamp
8 complianceAttestation: string; // Hash proving taker is verified
9}
10
11async function handleRFQ(rfq: RFQ) {
12 // Validate pair support
13 if (!SUPPORTED_PAIRS.includes(rfq.pair)) {
14 console.log("Pair not supported, skipping:", rfq.pair);
15 return;
16 }
17
18 // Validate size
19 const amount = parseFloat(rfq.amount);
20 if (amount < MIN_TRADE_SIZE || amount > MAX_TRADE_SIZE) {
21 console.log("Trade size out of range:", amount);
22 return;
23 }
24
25 // Price and submit quote
26 const price = await calculatePrice(rfq);
27 await submitQuote(rfq.id, price);
28}
29
30const SUPPORTED_PAIRS = ["USDC/USDT", "USDC/EURC"];
31const MIN_TRADE_SIZE = ;
32const MAX_TRADE_SIZE = ;
32 linestypescript

Submit sealed quotes using EIP-712 signatures. Competing solvers cannot see your bids:

TypeScript
1import { ethers } from "ethers";
2
3const QUOTE_TYPE = {
4 Quote: [
5 { name: "rfqId", type: "string" },
6 { name: "price", type: "string" },
7 { name: "ttl", type: "uint256" },
8 { name: "minAmount", type: "string" },
9 { name: "maxAmount", type: "string" },
10 { name: "nonce", type: "uint256" },
11 ]
12};
13
14async function submitQuote(rfqId: string, price: string) {
15 const signer = new ethers.Wallet(process.env.SOLVER_PRIVATE_KEY!);
16
17 const quote = {
18 rfqId,
19 price, // e.g., "0.9995"
20 ttl: 30, // seconds until quote expires
21 minAmount: "10000",
22 maxAmount: "10000000",
23 nonce: Date.now(),
24 };
25
26 // EIP-712 typed signature - sealed, not visible to other solvers
27 const signature = await signer.signTypedData(
28 { name: "TetraFi", version: "1", chainId: 1 },
29 QUOTE_TYPE,
30 quote
31 );
32
33 await tetrafi.quotes.submit({ ...quote, signature });
34 console.log("Quote submitted:", price);
35}
35 linestypescript

Submit quotes within 2 seconds of RFQ receipt for optimal fill rates. The auction window is typically 5 seconds, and faster responses indicate higher solver reliability.

When your quote is accepted, monitor escrow events and fill on the destination chain:

TypeScript
1// Listen for escrow lock events
2ws.on("message", async (data) => {
3 const event = JSON.parse(data.toString());
4
5 if (event.type === "settlement.pending") {
6 const settlement = event.settlement;
7 console.log("Escrow locked:", settlement.escrowTxHash);
8 console.log("Destination chain:", settlement.destinationChainId);
9
10 // Fill on destination chain - must complete within window (typically 30s)
11 await fillSettlement(settlement);
12 }
13});
14
15async function fillSettlement(settlement: Settlement) {
16 // Monitor ERC-7683 fill events on destination chain
17 const provider = new ethers.JsonRpcProvider(DEST_RPC_URL);
18 const settler = new ethers.Contract(OUTPUT_SETTLER_ADDR, OutputSettlerABI, signer);
19
20 // Deliver funds on destination chain
21 const tx = await settler.fill(
22 settlement.orderId,
23 settlement.originData,
24 settlement.destinationData,
25 );
26
27 await tx.wait();
28 console.log("Fill TX:", tx.hash);
29}
29 linestypescript

You must fill within the settlement window (typically 30 seconds). If you fail to fill, the escrow automatically refunds the taker and your solver reputation score decreases.

Verify DvP completion and update your internal accounting:

TypeScript
1ws.on("message", async (data) => {
2 const event = JSON.parse(data.toString());
3
4 if (event.type === "settlement.complete") {
5 const { settlement } = event;
6 console.log("Settlement complete!");
7 console.log(" Origin TX:", settlement.originTxHash);
8 console.log(" Destination TX:", settlement.destinationTxHash);
9 console.log(" Amount:", settlement.amount);
10
11 // Fetch full audit trail
12 const audit = await tetrafi.audit.getTrail(settlement.id);
13 console.log("WORM audit entries:", audit.length);
14
15 // Update your internal accounting
16 await reconcileSettlement(settlement);
17 }
18
19 if (event.type === "settlement.failed") {
20 const { settlement } = event;
21 console.error("Settlement failed:", settlement.error);
22 console.log("Taker refunded via:", settlement.refundTxHash);
23 // No action needed - escrow auto-refunds taker
24 }
25});
25 linestypescript

DvP atomicity guarantees that either both legs settle or neither does. If you experience a failed fill, the taker is automatically refunded via the HTLC timelock mechanism. Your solver reputation score may decrease for repeated failures.