Skip to main contentSkip to FAQSkip to contact
Hands-on· 30 min

Solver Integration Guide#

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.

Integration at a Glance#

Your whole integration surface is 4 HTTP endpoints + 4 on-chain functions - and the HTTP contract is the Open Intents Framework v0 REST spec, not a TetraFi-specific protocol. Any solver that already speaks OIF to other intent networks speaks it to us. The reference solver (tetrafi-solver) ships the full shape out of the box.

Solver API you host#

  • GET /tokens - capability probe (chains, tokens, settlers)
  • POST /quotes - RFQ handler (return quotes in response body)
  • POST /orders - accept winning quote + signed order
  • GET /orders/{id} - status polling

On-chain you execute#

  • open() - ComplianceSettlerEscrow (if not taker-opened)
  • fill() - ComplianceOutputSettler (first caller wins)
  • submit() - Oracle attestation of the fill
  • finalise() - ComplianceSettlerEscrow (claim input)
  • Competition - fill() is idempotent; first solver to land the tx wins.
  • Compliance - KYB + per-chain ComplianceRegistry attestation; _beforeNewFill() validates every fill on-chain.

Choose Your Integration Path#

Four paths, self-selected by where you start. If you already operate a solver against another intent network, Path D is usually the fastest. If you are building from scratch, most operators start with Path A and graduate to B only when they have proprietary pricing.

PathEffortBest for
A. Run the reference solverHoursGo live fast with built-in pricing (mock / coingecko / defillama). Clone tetrafi-solver, edit config/demo.toml, run.
B. Plug custom pricingDaysYou have proprietary inventory or OTC flow. Implement PricingInterface (solver-pricing crate); keep our endpoint + on-chain plumbing.
C. Custom REST adapter (OIF v0)1-2 weeksNon-Rust stack or deep MM-infra integration. Implement the 4 HTTP endpoints yourself in any language.
D. Register your existing solverMinutes (OIF-compliant) · scoped engagement (custom)You already operate a solver. If it speaks the OIF v0 REST contract, just register your base URL plus KYB and you're live. If it speaks a proprietary protocol, TetraFi writes a custom adapter on our side - your stack stays as-is. See Not OIF? Custom Adapter.

Integration Checklist#

Track your progress through each step:

Integration Checklist

Step 1: Prerequisites#

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
Bash
1# Set environment variables
2export TETRAFI_API_KEY=tfk_test_...
3export TETRAFI_BASE_URL=https://sandbox.tetrafi.io/api/v1
3 linesbash

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

Step 2: Register as Solver#

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

Bash
1curl -X POST https://api.tetrafi.io/api/v1/solvers/register \
2 -H "Authorization: Bearer tfk_test_..." \
3 -H "Content-Type: application/json" \
4 -d '{
5 "name": "Acme Market Making LLC",
6 "entityType": "company",
7 "jurisdictions": ["US", "EU"],
8 "supportedPairs": ["USDC/USDT", "USDC/EURC"],
9 "corridors": ["ethereum-optimism", "ethereum-base"],
10 "minTradeSize": "10000",
11 "maxTradeSize": "10000000",
12 "walletAddress": "0x..."
13 }'
14# { "solverId": "...", "status": "pending_review" }
14 linesbash

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.

Step 3: Expose the Solver API#

You host four HTTPS endpoints. TetraFi's aggregator registers your base URL at onboarding and calls them directly - no stream to connect to, no SDK required. Return JSON.

3.1 GET /tokens - capability probe#

The aggregator calls this at solver startup and periodically thereafter to learn which chains, tokens, and settler contracts you support. Missing chains or tokens here = no RFQs for those corridors.

JSON
1{
2 "networks": {
3 "8453": {
4 "inputSettler": "0x…",
5 "outputSettler": "0x…",
6 "tokens": [""]
7 },
8 "10": {
9 "inputSettler": "0x…",
10 "outputSettler": "0x…",
11 "tokens": [""]
12 }
13 }
14}
14 linesjson

3.2 POST /quotes - RFQ handler#

Request

JSON
1{
2 "user": "0x…",
3 "intent": {
4 "inputToken": "0xA0b8…eB48",
5 "outputToken": "0xdAC1…1ec7",
6 "inputAmount": "1000000000000",
7 "originChainId": 8453,
8 "destinationChainId": 10
9 },
10 "supportedTypes": ["oif-swap", "oif-escrow-v0", "oif-resource-lock-v0"]
11}
11 linesjson

Response - the quote IS the response body. There is no separate submit step.

TypeScript (Express)
1app.post("/quotes", async (req, res) => {
2 const { user, intent, supportedTypes } = req.body;
3
4 if (!canServe(intent)) return res.json({ quotes: [] });
5
6 const preview = await priceIntent(intent);
7
8 // The `order` field is EIP-712 typed data, NOT a signature.
9 // You build it; the taker signs it after picking your quote.
10 const order = {
11 signatureType: "Eip712",
12 domain: { name: "TetraFi", version: "1", chainId: intent.originChainId, verifyingContract: INPUT_SETTLER },
13 primaryType: "StandardOrder",
14 message: buildStandardOrderMessage(intent, preview),
15 types: STANDARD_ORDER_TYPES,
16 };
17
18 res.json({
19 quotes: [{
20 quoteId: "qt_" + crypto.randomUUID(),
21 order,
22 validUntil: Math.floor(Date.now() / 1000) + 30,
23 eta: 12,
24 provider: "acme",
25 failureHandling: "refund",
26 partialFill: false,
27 preview,
28 metadata: {},
29 }],
30 });
31});
31 linestypescript

You do not sign here. The order field carries EIP-712 typed data (domain, primaryType, message, types). The taker signs it after choosing your quote; the aggregator forwards the signature in POST /orders. Solver-side signTypedData(...) on the quote is not part of the OIF contract and would produce unusable orders.

Return { "quotes": [] } rather than a 4xx when you choose not to quote (out-of-scope corridor, size outside risk, inventory exhausted). Empty array = "not bidding this round" and carries no reputation penalty; 4xx is treated as a categorized error.

3.3 POST /orders - winning-quote delivery#

When your quote wins, the aggregator POSTs the signed order to your server. Verify the signature against domain.verifyingContract, persist, and asynchronously kick off on-chain fill (see Step 4).

Request

JSON
1{
2 "order": { /* same Order you returned in /quotes */ },
3 "signature": "0x…",
4 "quoteId": "qt_…",
5 "originSubmission": { "txHash": "0x…" },
6 "metadata": {}
7}
7 linesjson

Response - fire-and-forget; the aggregator polls GET /orders/{orderId} for progress.

JSON
1{
2 "orderId": "ord_…",
3 "status": "Accepted",
4 "message": "fill queued",
5 "order": { /* optional echo */ },
6 "metadata": {}
7}
7 linesjson

Status enum: Accepted · Pending · Completed · Rejected · Error.

3.4 GET /orders/{orderId} - status polling#

JSON
1{
2 "id": "ord_…",
3 "status": "Pending",
4 "createdAt": 1734375500,
5 "updatedAt": 1734375530,
6 "inputAmounts": [{ "token": "0x…", "amount": "1000000000000" }],
7 "outputAmounts": [{ "token": "0x…", "amount": "999725000000" }],
8 "settlement": { "token": "0x…", "amount": "999725000000", "recipient": "0x…" },
9 "fillTransaction": { "chainId": 10, "txHash": "0x…" }
10}
10 linesjson

Step 4: Settle On-chain#

When you accept an order in POST /orders, you have up to the fillDeadline (typically 30 s) to deliver outputs on the destination chain and claim inputs on the origin. Four settler functions drive the on-chain flow:

FunctionContractPurpose
open()ComplianceSettlerEscrowOpen the input-side escrow on the origin chain. Skip if the taker used Permit2 / ERC-3009 auto-open.
fill()ComplianceOutputSettlerDeliver outputs on the destination chain. Idempotent - first caller wins.
submit()Oracle (Hyperlane / Wormhole / Polymer / LayerZero)Request attestation of the fill back to the origin.
finalise()ComplianceSettlerEscrowClaim the input escrow on the origin against the oracle attestation.
TypeScript
1import { ethers } from "ethers";
2
3async function settleOrder(order: SignedOrder) {
4 // 1. Fill on destination - race-to-fill, idempotent
5 const dstProvider = new ethers.JsonRpcProvider(DST_RPC);
6 const outputSettler = new ethers.Contract(OUTPUT_SETTLER_ADDR, OutputSettlerABI, signer.connect(dstProvider));
7 const fillTx = await outputSettler.fill(order.orderId, order.originData, order.destinationData);
8 await fillTx.wait();
9
10 // 2. Submit oracle attestation request
11 const oracle = new ethers.Contract(ORACLE_ADDR, OracleABI, signer.connect(dstProvider));
12 await (await oracle.submit(order.orderId, fillTx.hash)).wait();
13
14 // 3. Finalise on origin once attestation lands (oracle SLA varies)
15 const origProvider = new ethers.JsonRpcProvider(ORIG_RPC);
16 const escrow = new ethers.Contract(INPUT_SETTLER_ADDR, EscrowABI, signer.connect(origProvider));
17 await (await escrow.finalise(order.orderId)).wait();
18}
18 linestypescript

Compliance pre-check runs on every fill. ComplianceOutputSettler._beforeNewFill() verifies your solver address has a live ComplianceRegistry attestation on the destination chain. Missing attestation = tx reverts. See Compliance Architecture.

Race-to-fill is real. fill() is idempotent - the first successful transaction wins and subsequent attempts return early without reverting. Build your submitter for speed, not for exclusivity.

Step 5: Post-Fill & Reconciliation#

Once finalise() confirms, update your own GET /orders/{id} response to Completed, fetch the WORM audit trail for accounting, and reconcile.

TypeScript
1// After finalise() confirms on origin
2await updateOrderStatus(orderId, {
3 status: "Completed",
4 outputAmounts: [{ token: DST_TOKEN, amount: fillAmount }],
5 fillTransaction: { chainId: DST_CHAIN_ID, txHash: fillTx.hash },
6});
7
8// Optional: pull the WORM audit trail for regulatory reporting
9const audit = await fetch(`https://api.tetrafi.io/api/v1/audit/trail/{orderId}`, {
10 headers: { Authorization: `Bearer ${apiKey}` },
11}).then(r => r.json());
12
13await reconcileAccounting({ orderId, fillTx, audit });
13 linestypescript

Failed fills auto-refund. If you miss the fillDeadline, the InputSettler allows the taker to reclaim locked funds. You lose only the gas you spent attempting to fill. Repeated failures drop your solver reputation score (see Quote Pipeline → Solver Reputation).

Real-Time Order Updates (Optional)#

The four REST endpoints above are sufficient to run a solver end-to-end. If you want push semantics on post-fill state transitions, pick at most one of these optional channels - none is required for quote auction participation:

ChannelDirectionCarriesSetup
Poll GET /orders/{id} on your own serverAggregator → You (HTTP)Your own order stateAlready in OIF contract
WebSocket wss://api.tetrafi.io/api/v1/ws · topics orders:{orderId} or orders:*TetraFi → You (push)Post-fill state transitions (Deposited → Filled → Attested → Claimed)Token auth + reconnect logic
Webhook via POST /api/v1/webhooksTetraFi → You (HTTP push)Same events, durable at-least-once deliveryPublic HTTPS endpoint + HMAC verification

See Webhooks & Events for WebSocket topic patterns, auth, and signature verification.

What the public WebSocket does NOT do for solvers. The public WS endpoint only accepts orders:* and prices:* topics - there are no RFQ or quote-streaming topics. You cannot stream quotes over WebSocket and you cannot receive RFQs over WebSocket via the self-serve public API. Solvers must expose POST /quotes over HTTPS to participate in auctions.

Exception - custom adapters. If your MM stack already exposes a native streaming API (e.g., a persistent WebSocket RFQ feed), TetraFi can write a bespoke adapter on our side that connects out to your server and translates your protocol to OIF internally. This is not self-serve - see Not OIF? Custom Adapter below.

Not OIF? Custom Adapter#

If you cannot host HTTP endpoints (your MM infra is streaming-native, uses a proprietary protocol, or you've already integrated with other intent networks via a different shape), TetraFi's aggregator supports pluggable SolverAdapter implementations. The team writes the adapter on our side - you keep your existing API.

Existing adapters in the aggregator include TetraFi-native, OIF (the contract this guide documents), Across, and bespoke LP adapters for streaming counterparties (Keyrock pattern). Adding a new one is a TetraFi engineering effort scoped during onboarding. Contact us to start the conversation - bring your protocol docs and a sandbox endpoint.

Troubleshooting#

Regulated Solver Landscape#

TetraFi's solver model is regulatory architecture, not just technical integration. Solvers are regulated market makers with existing compliance infrastructure.

Regulated Solver Landscape

FeatureAcheronHercleFlowdeskWintermuteKeyrock
Regulatory2 features
Primary Jurisdiction
Netherlands (AFM)EU + SwitzerlandFrance (AMF)UK (FCA)Multi-jurisdictional
License Type
MiCA CASPMiCA + FINMADASPFCA RegisteredMultiple
Integration2 features
API Type
CustomCustomCustomTurnkey APICustom
Supported Corridors
EVM L2EVM + TradEVM L2Broad EVMBroad EVM

Fill Mechanics#

orderId#

keccak256(abi.encode(standardOrder)) - deterministic hash of the typed struct

First-to-Fill#

fill() is idempotent - first caller wins, subsequent attempts return early. Race-to-fill model.

Compliance Onboarding#

Tier system. Regulated / Permissioned / Permissionless - gates corridor access + Travel Rule requirements. See Compliance Framework.

  1. Complete KYB verification via vendor adapter (Chainalysis or Elliptic)
  2. Receive ComplianceRegistry attestation on each chain you'll fill on
  3. Daily sanctions screening - attestation is continuously validated
  4. Post-trade Travel Rule data submission for applicable corridors
  5. Whitelist management - maintain approved corridor/token pairs