Skip to main content
Glama

swap_execute

Accepts a winning sealed bid for a swap and creates the trade. Provide limit_price or quote_id to confirm; returns trade_id for on-chain settlement via HTLC.

Instructions

Accept the winning sealed bid for a swap and create the trade. Real funds. Provide EITHER limit_price (auto-takes the best bid only if it meets your bound) OR quote_id (the exact bid you saw via swap_status). With neither, this refuses (CONFIRMATION_REQUIRED) rather than guess — restate the price to the user first.

USE WHEN: a swap has an acceptable bid and the user confirmed. DO NOT USE WHEN: you have not surfaced the price to the user, or you want maker-side quoting (use respond_rfq).

PARAM NOTES: limit_price is the sealed reservation (SELL=floor, BUY=ceiling) and must be re-supplied here — it is deliberately never stored. WARNING: accepted_amount may EXCEED your requested amount if a maker quoted a larger size (full-fill v1 accepts a bid whose amount covers the request) — always reconcile accepted_amount against what you asked before settling on-chain. On success returns trade_id; settle on-chain next via create_htlc. This does NOT lock funds itself (non-custodial).

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
swap_handleYesThe swap_handle (RFQ id) from swap_quote.
limit_priceNoSealed reservation. SELL=floor, BUY=ceiling. Re-supply it here; never persisted.
quote_idNoExact bid id from swap_status best_bid.quote_id (explicit-confirm path).
client_request_idNoIdempotency key. Same id within this session returns the first result instead of accepting twice. Best-effort.

Implementation Reference

  • src/index.ts:373-391 (registration)
    MCP tool registration for 'swap_execute' — defines the tool name, description, Zod schema params (swap_handle, limit_price, quote_id, client_request_id), and calls runSwapExecute with idempotency wrapping.
    // ─── swap_execute ────────────────────────────────────────────
    server.tool(
      'swap_execute',
      [
        'Accept the winning sealed bid for a swap and create the trade. Real funds. Provide EITHER limit_price (auto-takes the best bid only if it meets your bound) OR quote_id (the exact bid you saw via swap_status). With neither, this refuses (CONFIRMATION_REQUIRED) rather than guess — restate the price to the user first.',
        '',
        'USE WHEN: a swap has an acceptable bid and the user confirmed. DO NOT USE WHEN: you have not surfaced the price to the user, or you want maker-side quoting (use respond_rfq).',
        '',
        'PARAM NOTES: `limit_price` is the sealed reservation (SELL=floor, BUY=ceiling) and must be re-supplied here — it is deliberately never stored. WARNING: accepted_amount may EXCEED your requested amount if a maker quoted a larger size (full-fill v1 accepts a bid whose amount covers the request) — always reconcile accepted_amount against what you asked before settling on-chain. On success returns trade_id; settle on-chain next via create_htlc. This does NOT lock funds itself (non-custodial).',
      ].join('\n'),
      {
        swap_handle: z.string().describe('The swap_handle (RFQ id) from swap_quote.'),
        limit_price: z.string().optional().describe('Sealed reservation. SELL=floor, BUY=ceiling. Re-supply it here; never persisted.'),
        quote_id: z.string().optional().describe('Exact bid id from swap_status best_bid.quote_id (explicit-confirm path).'),
        client_request_id: z.string().optional().describe('Idempotency key. Same id within this session returns the first result instead of accepting twice. Best-effort.'),
      },
      wrapTool(async (a) => runSwapExecute(swapClient, a,
        (op) => idempotency.remember(idempotencyKey('swap_execute', a.client_request_id, { h: a.swap_handle, q: a.quote_id, l: a.limit_price }), op))),
    );
  • Core handler logic for swap_execute — fetches RFQ, validates open status, resolves either quote_id or limit_price auto-selection, checks limit satisfaction, accepts the quote via client.acceptQuote, and returns trade_id/rfq_id/price/amount.
    export async function runSwapExecute(
      client: SwapClient, args: SwapExecuteArgs, remember: Remember,
    ): Promise<ToolContent> {
      let rfq: SwapRfq | null;
      try {
        rfq = await client.getRFQ(args.swap_handle);
      } catch (err) {
        // Uniform not-found contract: a forbidden/unauthorized RFQ (exists but the
        // caller is not a participant) must NOT be distinguishable from a
        // non-existent one, or swap_handle becomes an existence/participant oracle.
        const msg = err instanceof Error ? err.message : String(err);
        if (/forbidden|not a participant|unauthor|\b401\b|\b403\b/i.test(msg)) {
          return okContent({ outcome: 'SWAP_NOT_FOUND', swap_handle: args.swap_handle,
            next: 'Verify the swap_handle, or open a fresh swap with swap_quote.' });
        }
        throw err;
      }
      if (!rfq) {
        return okContent({ outcome: 'SWAP_NOT_FOUND', swap_handle: args.swap_handle,
          next: 'Verify the swap_handle, or open a fresh swap with swap_quote.' });
      }
      if (!RFQ_OPEN_STATES.has(rfq.status)) {
        return okContent({ outcome: 'SWAP_NOT_OPEN', swap_handle: args.swap_handle, rfq_status: rfq.status,
          next: 'This swap can no longer be executed. Open a fresh swap with swap_quote.' });
      }
      if (args.quote_id && args.limit_price !== undefined) {
        return okContent({ outcome: 'INVALID_EXECUTION_PARAMS', swap_handle: args.swap_handle,
          next: 'Provide EXACTLY ONE of quote_id (take that exact bid) or limit_price (auto-take best within your bound) — not both. They are distinct confirmation modes; passing both is ambiguous on a real-funds accept.' });
      }
      if (!args.quote_id && args.limit_price === undefined) {
        return okContent({ outcome: 'CONFIRMATION_REQUIRED', swap_handle: args.swap_handle,
          next: 'Real funds. Re-call swap_execute with EITHER limit_price (auto-takes best bid iff it meets your bound) OR quote_id (from swap_status best_bid.quote_id) to confirm the exact price.' });
      }
      const quotes = await client.getQuotes(args.swap_handle);
    
      let chosen: SwapQuote | null;
      if (args.quote_id) {
        chosen = quotes.find(
          (x) => x.id === args.quote_id && x.status === SELECTABLE_QUOTE
            && isPositiveDecimal(x.price) && isPositiveDecimal(x.amount)
            && compareDecimal(x.amount, rfq.amount) >= 0,
        ) ?? null;
        if (!chosen) {
          return okContent({ outcome: 'QUOTE_NOT_AVAILABLE', swap_handle: args.swap_handle, quote_id: args.quote_id,
            next: 'That quote expired or was outbid. Re-check live bids with swap_status.' });
        }
      } else {
        // limit_price is defined here (the no-args case returned CONFIRMATION_REQUIRED above).
        if (!isPositiveDecimal(args.limit_price as string)) {
          return okContent({ outcome: 'INVALID_LIMIT_PRICE', swap_handle: args.swap_handle,
            limit_price: args.limit_price,
            next: 'limit_price must be a positive decimal string like "3450.00" — no commas, no scientific notation, no negative.' });
        }
        const best = selectBestBid(quotes, rfq.side, rfq.amount);
        if (!best) {
          return okContent({ outcome: 'NO_ACCEPTABLE_FILL', swap_handle: args.swap_handle, best_price: null,
            limit_price: args.limit_price, side: rfq.side, bids_seen: quotes.length,
            next: 'No eligible bids yet. swap_status to wait, or swap_cancel.' });
        }
        if (!limitSatisfied(best.price, args.limit_price as string, rfq.side)) {
          return okContent({ outcome: 'NO_ACCEPTABLE_FILL', swap_handle: args.swap_handle, best_price: best.price,
            limit_price: args.limit_price, side: rfq.side, bids_seen: quotes.length,
            next: 'Best bid does not meet your limit. swap_status to wait for better, or swap_cancel.' });
        }
        chosen = best;
      }
    
      if (!chosen) throw new Error('invariant: chosen must be set before accept');
      // Idempotency key is composed at the index.ts tool handler (Layer-1 pattern); this fn receives a pre-bound Remember.
      const accept = await remember(() => client.acceptQuote(chosen.id));
      return okContent({
        trade_id: accept.trade?.id ?? null,
        rfq_id: accept.rfqId,
        accepted_price: chosen.price,
        accepted_amount: chosen.amount,
        status: accept.status,
        next: 'Settle on-chain: create_htlc -> get_htlc -> withdraw_htlc (or refund_htlc after timelock).',
      });
    }
  • SwapExecuteArgs interface defining input shape: swap_handle (required), limit_price, quote_id, client_request_id (all optional).
    export interface SwapExecuteArgs {
      swap_handle: string; limit_price?: string; quote_id?: string; client_request_id?: string;
    }
  • selectBestBid helper — filters eligible PENDING quotes whose amount covers the request and returns the best price (max for SELL, min for BUY). Used by runSwapExecute to auto-select when limit_price is supplied.
    export function selectBestBid(quotes: SwapQuote[], side: Side, requestedAmount: string): SwapQuote | null {
      const eligible = quotes.filter(
        (x) => x.status === SELECTABLE_QUOTE
          && isPositiveDecimal(x.price) && isPositiveDecimal(x.amount)
          && compareDecimal(x.amount, requestedAmount) >= 0,
      );
      if (eligible.length === 0) return null;
      return eligible.reduce((best, x) => {
        const c = compareDecimal(x.price, best.price);
        if (side === 'SELL') return c > 0 ? x : best;
        return c < 0 ? x : best;
      });
    }
  • limitSatisfied helper — compares best price vs limit price directionally: SELL=floor (best >= limit), BUY=ceiling (best <= limit). Used to validate limit_price selections in runSwapExecute.
    export function limitSatisfied(bestPrice: string, limitPrice: string, side: Side): boolean {
      const cmp = compareDecimal(bestPrice, limitPrice);
      return side === 'SELL' ? cmp >= 0 : cmp <= 0;
    }
Behavior5/5

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

With no annotations, the description fully discloses behavioral traits: real funds, non-custodial (doesn't lock funds), acceptance behavior with missing parameters (CONFIRMATION_REQUIRED), warning about accepted_amount exceeding requested amount, and fact that limit_price is re-supplied and not stored.

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

Conciseness4/5

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

Description is well-structured with sections (description, use when, param notes, warnings) and front-loaded with the core action. Though slightly long, each sentence adds necessary value for a real-funds tool.

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

Completeness5/5

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

Despite no output schema, the description covers return value (trade_id) and next step (create_htlc). It also addresses edge cases (missing parameters, size exceeding request) and constraints (non-custodial). Fully sufficient for an agent to use correctly.

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?

Schema has 100% coverage with descriptions. The description adds significant meaning: limit_price is sealed reservation and must be re-supplied, quote_id is exact bid from swap_status, client_request_id is idempotency key. This goes beyond the schema's basic descriptions.

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?

Clearly states it accepts the winning sealed bid and creates a trade, differentiating from sibling tools like respond_rfq. The verb 'accept' and resource 'swap' are specific, and the description distinguishes from maker-side quoting.

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?

Explicitly provides USE WHEN (swap has acceptable bid and user confirmed) and DO NOT USE WHEN (price not surfaced or want maker-side quoting), with alternative tool named (respond_rfq). This gives clear decision criteria.

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