Skip to main content
Glama

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 ═══

  1. 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.

  2. 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.

  3. 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

TableJSON Schema
NameRequiredDescriptionDefault
baseTokenYesBase asset symbol from the supported list (ETH, BTC, SUI, USDC, USDT, WBTC, WETH). Case-insensitive but uppercase preferred.
baseChainNoChain 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).
quoteTokenYesQuote asset symbol from the supported list. Same rules as baseToken.
quoteChainNoChain the quote token settles on. Same inference rules as baseChain. Cross-environment pairs are allowed (e.g. baseChain="sui" + quoteChain="sepolia").
sideYesBUY = user wants to acquire baseToken; SELL = user wants to dispose of baseToken. Map "sell/swap/exchange/liquidate/convert/sat" → SELL, "buy/acquire/al" → BUY.
amountYesAmount 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.
expiresInNoRFQ expiration in seconds. Default 300 (5 min). "Urgent" → 60-120. "Take your time" → 600-1800. Hard cap 86400 (24 h).
isBlindNoGhost 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_idNoIdempotency 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);
      }),
    );
  • 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);
    }),
  • 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,
      );
  • 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.'),
  • 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 }>;
    }
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description fully discloses behavioral aspects: sealed-bid auction, non-custodial nature, cross-chain support, zero slippage, and pre-call steps. It also covers idempotency and the requirement to confirm before calling, ensuring the agent understands the tool's behavior comprehensively.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured with clear headings, examples, and logical flow. It is detailed but every section adds essential information for proper tool usage. The front-loading of the core purpose and the use of tables and examples make it efficient for an AI agent to parse.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Despite the detailed description, it lacks an explanation of the tool's return value. There is no output schema, and the description does not mention what the response contains (e.g., RFQ ID, status). For a complex tool with no output schema, this omission creates a completeness gap, though the rest of the context is thorough.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The description adds immense value beyond the input schema, with detailed intent mapping, chain inference, language-specific instructions, handling of edge cases like USD amounts, and explanations for each parameter. This helps the agent correctly populate parameters even from ambiguous user input.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: creating an RFQ for OTC swaps. It specifies the type of auction (sealed-bid), distinguishes from siblings like swap_execute, and provides explicit use and non-use cases, making the purpose unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides comprehensive usage guidelines, including explicit 'USE WHEN' and 'DO NOT USE WHEN' sections, alternatives like DEX aggregator, and detailed intent-to-parameter mapping with language support and chain inference rules. It also instructs when to ask for clarification, ensuring proper tool selection.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Hashlock-Tech/hashlock-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server