create_rfq
Create a sealed-bid Request for Quote for large OTC crypto swaps across Ethereum, Bitcoin, and Sui chains without information leakage.
Instructions
Trustless price discovery for OTC trades — sealed-bid auction with zero information leakage, no front-running, no MEV. Non-custodial, cross-chain (ETH/BTC/SUI). Agent-friendly: works with any MCP runtime.
Create a Request for Quote (RFQ) for an OTC swap — broadcast to market makers for sealed-bid quotes.
USE WHEN: user wants competitive quotes (not AMM curve fill) for size ≥ $10k, cross-chain swaps, privacy-sensitive orders, or expressed a "negotiate" / "best execution" / "large block" / "institutional" intent. DO NOT USE WHEN: sub-second execution is required, or pair is a long-tail memecoin with no market-maker coverage (prefer DEX aggregator).
═══ SUPPORTED CHAIN-QUALIFIED PAIRS ═══ ETH/sepolia, ETH/ethereum, BTC/bitcoin-signet, BTC/bitcoin, USDC/sepolia, USDC/ethereum, USDT/ethereum, WBTC/ethereum, WETH/ethereum, SUI/sui, SUI/sui-testnet. Cross-chain RFQs (e.g. SUI/sui ↔ ETH/sepolia) are first-class — set baseChain and quoteChain explicitly so the backend can disambiguate same-symbol-different-chain pairs.
═══ INTENT → PARAMS MAPPING ═══ Translate the user free-text intent into params using these rules. The user will rarely give a structured form; you are the compiler.
side: • "sell X / swap X for Y / exchange X to Y / liquidate X / convert X to Y / cash out X" → side=SELL, baseToken=X, quoteToken=Y • "buy X with Y / acquire X / pay Y for X / get X using Y" → side=BUY, baseToken=X, quoteToken=Y • Turkish: "sat / çıkar / boşalt" → SELL, "al / topla" → BUY, "X karşılığı Y" → SELL with X=base.
baseChain / quoteChain (CHAIN INFERENCE): • If the user names the chain explicitly ("Sepolia", "mainnet", "Sui testnet", "signet"), use it. • Otherwise apply per-token mainnet defaults: ETH/USDC/USDT/WBTC/WETH → "ethereum"; BTC → "bitcoin"; SUI → "sui". • If the user says "test" / "testnet" / "demo" / "test mode" / "sınama" globally, switch every leg to its testnet variant: ETH→sepolia, BTC→bitcoin-signet, SUI→sui-testnet, USDC→sepolia. • Cross-environment is allowed and common — if only ONE leg is qualified with a testnet hint (e.g. "sell SUI for Sepolia ETH"), keep the other leg on its mainnet default. Do NOT silently testnet-ify the unqualified leg. • If the chain is genuinely ambiguous after all rules (e.g. "sell ETH for USDC" — both could be mainnet or both Sepolia depending on user intent), ASK before calling. Do not gamble on real funds.
amount: • Pass the raw decimal string the user typed ("0.1", "1.5", "10"). Do NOT pre-convert to wei / satoshis / smallest unit — the backend handles decimals via the token registry. • If the user gives a USD-denominated value ("worth $10k of SUI"), do NOT call the tool — ask for the base-token amount or compute and confirm before submitting.
expiresIn (seconds): • Default 300 (5 min) when unspecified. • "Quick / urgent / hızlı / acele" → 60–120. • "Leave open / take your time / uzun süre" → 600–1800. • Hard cap 86400 (24 h).
isBlind (Ghost Auction mode): • Default false. Zero slippage: quote equals fill, regardless of mode. • Set true on intent words: "ghost", "blind", "anonymous", "hide identity", "private auction", "gizli", "kimliğimi gizle".
═══ REQUIRED BEFORE CALLING ═══
RESTATE the resolved deal in plain language back to the user, naming the chain on every leg ("SELL 0.1 SUI on Sui mainnet for ETH on Sepolia, public auction, expires in 5 min — confirm?"). Real funds. Do NOT submit on first inference unless the user has already explicitly accepted the structured form.
If you cannot resolve a leg's chain confidently, ASK ("Ethereum mainnet ETH or Sepolia testnet ETH?"). Never silently default when the user phrasing is ambiguous on chain.
If the user names a token outside the supported list, do NOT call this tool — explain and offer the closest supported pair.
═══ EXAMPLES ═══ User: "Hashlock'ta 0.1 SUI'mi Sepolia ETH'e karşı sat, 5 dakika" → { side: "SELL", baseToken: "SUI", baseChain: "sui", quoteToken: "ETH", quoteChain: "sepolia", amount: "0.1", expiresIn: 300, isBlind: false }
User: "sell 2 ETH for USDC, ghost auction" → { side: "SELL", baseToken: "ETH", baseChain: "ethereum", quoteToken: "USDC", quoteChain: "ethereum", amount: "2", isBlind: true }
User: "buy 0.05 BTC with USDT, take your time" → { side: "BUY", baseToken: "BTC", baseChain: "bitcoin", quoteToken: "USDT", quoteChain: "ethereum", amount: "0.05", expiresIn: 1200 }
User: "test mode — swap 1 SUI to ETH" → { side: "SELL", baseToken: "SUI", baseChain: "sui-testnet", quoteToken: "ETH", quoteChain: "sepolia", amount: "1" }
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| baseToken | Yes | Base asset symbol from the supported list (ETH, BTC, SUI, USDC, USDT, WBTC, WETH). Case-insensitive but uppercase preferred. | |
| baseChain | No | Chain the base token settles on. Inference defaults: ETH/USDC/USDT/WBTC/WETH→"ethereum", BTC→"bitcoin", SUI→"sui". Override to testnet ONLY on explicit user mention ("sepolia", "signet", "testnet", "test", "sınama"). Required for SUI legs (no legacy fallback). | |
| quoteToken | Yes | Quote asset symbol from the supported list. Same rules as baseToken. | |
| quoteChain | No | Chain the quote token settles on. Same inference rules as baseChain. Cross-environment pairs are allowed (e.g. baseChain="sui" + quoteChain="sepolia"). | |
| side | Yes | BUY = user wants to acquire baseToken; SELL = user wants to dispose of baseToken. Map "sell/swap/exchange/liquidate/convert/sat" → SELL, "buy/acquire/al" → BUY. | |
| amount | Yes | Amount of base token as a raw decimal string ("0.1", "1.5", "10"). Do NOT convert to wei/satoshis. Reject USD-denominated values — ask user for base-token amount instead. | |
| expiresIn | No | RFQ expiration in seconds. Default 300 (5 min). "Urgent" → 60-120. "Take your time" → 600-1800. Hard cap 86400 (24 h). | |
| isBlind | No | Ghost Auction mode — hides requester identity from bidders and losing counterparties. Default false. Set true on intent words: "ghost", "blind", "anonymous", "hide identity", "gizli". External brand: "Ghost Auction"; internal name retained for API/DB schema stability. | |
| client_request_id | No | Idempotency key. Retrying the SAME write with the SAME id within this MCP session returns the first result instead of triggering a second on-chain/backend side effect. Best-effort: not durable across MCP restarts. |
Implementation Reference
- src/index.ts:208-232 (registration)MCP server.tool() registration for 'create_rfq' — defines the tool name, description (CREATE_RFQ_DESCRIPTION), Zod input schema with 9 parameters (baseToken, baseChain, quoteToken, quoteChain, side, amount, expiresIn, isBlind, client_request_id), and the handler wrapped with wrapTool().
server.tool( 'create_rfq', CREATE_RFQ_DESCRIPTION, { baseToken: z.string().describe('Base asset symbol from the supported list (ETH, BTC, SUI, USDC, USDT, WBTC, WETH). Case-insensitive but uppercase preferred.'), baseChain: z.enum(['ethereum', 'sepolia', 'bitcoin', 'bitcoin-signet', 'sui', 'sui-testnet']).optional().describe('Chain the base token settles on. Inference defaults: ETH/USDC/USDT/WBTC/WETH→"ethereum", BTC→"bitcoin", SUI→"sui". Override to testnet ONLY on explicit user mention ("sepolia", "signet", "testnet", "test", "sınama"). Required for SUI legs (no legacy fallback).'), quoteToken: z.string().describe('Quote asset symbol from the supported list. Same rules as baseToken.'), quoteChain: z.enum(['ethereum', 'sepolia', 'bitcoin', 'bitcoin-signet', 'sui', 'sui-testnet']).optional().describe('Chain the quote token settles on. Same inference rules as baseChain. Cross-environment pairs are allowed (e.g. baseChain="sui" + quoteChain="sepolia").'), side: z.enum(['BUY', 'SELL']).describe('BUY = user wants to acquire baseToken; SELL = user wants to dispose of baseToken. Map "sell/swap/exchange/liquidate/convert/sat" → SELL, "buy/acquire/al" → BUY.'), amount: z.string().describe('Amount of base token as a raw decimal string ("0.1", "1.5", "10"). Do NOT convert to wei/satoshis. Reject USD-denominated values — ask user for base-token amount instead.'), expiresIn: z.number().optional().describe('RFQ expiration in seconds. Default 300 (5 min). "Urgent" → 60-120. "Take your time" → 600-1800. Hard cap 86400 (24 h).'), isBlind: z.boolean().optional().describe('Ghost Auction mode — hides requester identity from bidders and losing counterparties. Default false. Set true on intent words: "ghost", "blind", "anonymous", "hide identity", "gizli". External brand: "Ghost Auction"; internal name retained for API/DB schema stability.'), client_request_id: z.string().optional().describe('Idempotency key. Retrying the SAME write with the SAME id within this MCP session returns the first result instead of triggering a second on-chain/backend side effect. Best-effort: not durable across MCP restarts.'), }, wrapTool(async ({ baseToken, baseChain, quoteToken, quoteChain, side, amount, expiresIn, isBlind, client_request_id }) => { // TODO: SDK type def (CreateRFQInput) lags backend — baseChain/quoteChain // are accepted by the GraphQL `createRFQ` mutation but not yet typed in // @hashlock-tech/sdk@0.1.4. Cast to bypass DTS build; remove once SDK // bumps the input type. Tracked separately from the v2 positioning sweep. const input = { baseToken, baseChain, quoteToken, quoteChain, side, amount, expiresIn, isBlind } as Parameters<typeof hl.createRFQ>[0]; const result = await idempotency.remember(idempotencyKey('create_rfq', client_request_id, input), () => hl.createRFQ(input)); return okContent(result); }), ); - src/index.ts:222-231 (handler)The handler function for create_rfq — constructs the input object (with a cast to bypass SDK type lag), calls hl.createRFQ(input) through the idempotency guard with a scoped key, and returns the result via okContent().
wrapTool(async ({ baseToken, baseChain, quoteToken, quoteChain, side, amount, expiresIn, isBlind, client_request_id }) => { // TODO: SDK type def (CreateRFQInput) lags backend — baseChain/quoteChain // are accepted by the GraphQL `createRFQ` mutation but not yet typed in // @hashlock-tech/sdk@0.1.4. Cast to bypass DTS build; remove once SDK // bumps the input type. Tracked separately from the v2 positioning sweep. const input = { baseToken, baseChain, quoteToken, quoteChain, side, amount, expiresIn, isBlind } as Parameters<typeof hl.createRFQ>[0]; const result = await idempotency.remember(idempotencyKey('create_rfq', client_request_id, input), () => hl.createRFQ(input)); return okContent(result); }), - src/lib/swap.ts:116-131 (handler)runSwapQuote helper mirrors create_rfq EXACTLY by constructing the same rfqInput object (baseToken, baseChain, quoteToken, quoteChain, side, amount, expiresIn, isBlind) and calling client.createRFQ(rfqInput). This is the same underlying createRFQ call used by the swap flow.
export async function runSwapQuote( client: SwapClient, args: SwapQuoteArgs, deps: SwapDeps, ): Promise<ToolContent> { // Mirror create_rfq EXACTLY: same input object incl. baseChain/quoteChain. // limit_price is deliberately NOT part of this object (sealed reservation). const rfqInput = { baseToken: args.baseToken, baseChain: args.baseChain, quoteToken: args.quoteToken, quoteChain: args.quoteChain, side: args.side, amount: args.amount, expiresIn: args.expiresIn ?? 300, isBlind: args.private ?? true, }; const rfq = await deps.remember(() => client.createRFQ(rfqInput)); const quotes = await pollForQuotes( client, rfq.id, args.side, args.amount, args.max_wait_seconds ?? 20, deps.sleep, ); - src/index.ts:212-220 (schema)Zod validation schema for create_rfq parameters: baseToken (z.string), baseChain (enum of 6 chains, optional), quoteToken (z.string), quoteChain (enum, optional), side (BUY/SELL enum), amount (z.string), expiresIn (z.number, optional), isBlind (z.boolean, optional), client_request_id (z.string, optional).
baseToken: z.string().describe('Base asset symbol from the supported list (ETH, BTC, SUI, USDC, USDT, WBTC, WETH). Case-insensitive but uppercase preferred.'), baseChain: z.enum(['ethereum', 'sepolia', 'bitcoin', 'bitcoin-signet', 'sui', 'sui-testnet']).optional().describe('Chain the base token settles on. Inference defaults: ETH/USDC/USDT/WBTC/WETH→"ethereum", BTC→"bitcoin", SUI→"sui". Override to testnet ONLY on explicit user mention ("sepolia", "signet", "testnet", "test", "sınama"). Required for SUI legs (no legacy fallback).'), quoteToken: z.string().describe('Quote asset symbol from the supported list. Same rules as baseToken.'), quoteChain: z.enum(['ethereum', 'sepolia', 'bitcoin', 'bitcoin-signet', 'sui', 'sui-testnet']).optional().describe('Chain the quote token settles on. Same inference rules as baseChain. Cross-environment pairs are allowed (e.g. baseChain="sui" + quoteChain="sepolia").'), side: z.enum(['BUY', 'SELL']).describe('BUY = user wants to acquire baseToken; SELL = user wants to dispose of baseToken. Map "sell/swap/exchange/liquidate/convert/sat" → SELL, "buy/acquire/al" → BUY.'), amount: z.string().describe('Amount of base token as a raw decimal string ("0.1", "1.5", "10"). Do NOT convert to wei/satoshis. Reject USD-denominated values — ask user for base-token amount instead.'), expiresIn: z.number().optional().describe('RFQ expiration in seconds. Default 300 (5 min). "Urgent" → 60-120. "Take your time" → 600-1800. Hard cap 86400 (24 h).'), isBlind: z.boolean().optional().describe('Ghost Auction mode — hides requester identity from bidders and losing counterparties. Default false. Set true on intent words: "ghost", "blind", "anonymous", "hide identity", "gizli". External brand: "Ghost Auction"; internal name retained for API/DB schema stability.'), client_request_id: z.string().optional().describe('Idempotency key. Retrying the SAME write with the SAME id within this MCP session returns the first result instead of triggering a second on-chain/backend side effect. Best-effort: not durable across MCP restarts.'), - src/lib/swap.ts:19-25 (helper)SwapClient interface defining createRFQ(input: unknown) => Promise<{ id: string; status: string }> — the SDK/client contract that the create_rfq handler delegates to.
export interface SwapClient { createRFQ(input: unknown): Promise<{ id: string; status: string }>; getRFQ(id: string): Promise<SwapRfq | null>; getQuotes(rfqId: string): Promise<SwapQuote[]>; acceptQuote(quoteId: string): Promise<{ id: string; rfqId: string; status: string; trade?: { id: string; status: string } | null }>; cancelRFQ(id: string): Promise<{ id: string; status: string }>; }