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
| Name | Required | Description | Default |
|---|---|---|---|
| swap_handle | Yes | The swap_handle (RFQ id) from swap_quote. | |
| limit_price | No | Sealed reservation. SELL=floor, BUY=ceiling. Re-supply it here; never persisted. | |
| quote_id | No | Exact bid id from swap_status best_bid.quote_id (explicit-confirm path). | |
| client_request_id | No | Idempotency 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))), ); - src/lib/swap.ts:162-240 (handler)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).', }); } - src/lib/swap.ts:158-160 (schema)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; } - src/lib/swap.ts:72-84 (helper)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; }); } - src/lib/swap.ts:57-60 (helper)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; }