@stratabook/mcp 0.1.12 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,11 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import { DEFAULT_SLIPPAGE_BPS, StrataApiError, StrataClient, StrataPlatformClient, } from "@stratabook/sdk";
2
+ import { DEFAULT_MAXIMUM_TOLERANCE_BPS, StrataApiError, StrataClient, StrataPlatformClient, } from "@stratabook/sdk";
3
+ import { decideAutonomy, estimateBaseNotionalUsd, quoteNotionalUsd, MarketMetaResolver, } from "./autonomy.js";
3
4
  import * as z from "zod/v4";
4
5
  import { STRATA_AGENT_HARNESS, STRATA_AGENT_HARNESS_INSTRUCTIONS, STRATA_AGENT_HARNESS_URI, STRATA_ACTION_GRAPH_URI, } from "./generated-harness.js";
5
6
  import { SERVER_VERSION } from "./version.js";
6
7
  const REFRESH_INTERVAL_MS = 5_000;
8
+ export const STRATA_PLATFORM_GRAPH_URI = "strata://platform-graph/v2";
7
9
  export function capabilityAvailable(catalog, id) {
8
10
  return catalog.capabilities.some((capability) => capability.id === id
9
11
  && capability.default_enabled
@@ -31,6 +33,147 @@ export async function probeStrataMcpReadiness(options = {}) {
31
33
  harness_version: STRATA_AGENT_HARNESS.harness_version,
32
34
  };
33
35
  }
36
+ /** Map tool arguments onto one order-control operation, or a tool error. */
37
+ function orderOperationFromArgs(args) {
38
+ let request;
39
+ if (args.action === "place") {
40
+ if (args.clientOrderId === undefined
41
+ || args.side === undefined
42
+ || args.orderType === undefined
43
+ || args.limitPriceAtoms === undefined
44
+ || args.sizeAtoms === undefined) {
45
+ return toolError("invalid_request", "Place requires clientOrderId, side, orderType, limitPriceAtoms, and sizeAtoms.", false);
46
+ }
47
+ request = {
48
+ action: "place",
49
+ ownerWallet: args.ownerWallet,
50
+ sessionPublicKey: args.sessionPublicKey,
51
+ ...(args.accountSequence === undefined ? {} : { accountSequence: args.accountSequence }),
52
+ clientOrderId: args.clientOrderId,
53
+ side: args.side,
54
+ orderType: args.orderType,
55
+ limitPriceAtoms: args.limitPriceAtoms,
56
+ sizeAtoms: args.sizeAtoms,
57
+ };
58
+ }
59
+ else if (args.action === "cancel") {
60
+ if (args.orderId === undefined) {
61
+ return toolError("invalid_request", "Cancel requires orderId.", false);
62
+ }
63
+ request = {
64
+ action: "cancel",
65
+ ownerWallet: args.ownerWallet,
66
+ sessionPublicKey: args.sessionPublicKey,
67
+ orderId: args.orderId,
68
+ };
69
+ }
70
+ else if (args.action === "cancel_all") {
71
+ request = {
72
+ action: "cancel_all",
73
+ ownerWallet: args.ownerWallet,
74
+ sessionPublicKey: args.sessionPublicKey,
75
+ };
76
+ }
77
+ else if (args.action === "replace") {
78
+ if (args.orderId === undefined
79
+ || args.clientOrderId === undefined
80
+ || args.side === undefined
81
+ || args.orderType === undefined
82
+ || args.limitPriceAtoms === undefined
83
+ || args.sizeAtoms === undefined) {
84
+ return toolError("invalid_request", "Replace requires orderId, clientOrderId, side, orderType, limitPriceAtoms, and sizeAtoms.", false);
85
+ }
86
+ request = {
87
+ action: "replace",
88
+ ownerWallet: args.ownerWallet,
89
+ sessionPublicKey: args.sessionPublicKey,
90
+ orderId: args.orderId,
91
+ ...(args.accountSequence === undefined ? {} : { accountSequence: args.accountSequence }),
92
+ clientOrderId: args.clientOrderId,
93
+ side: args.side,
94
+ orderType: args.orderType,
95
+ limitPriceAtoms: args.limitPriceAtoms,
96
+ sizeAtoms: args.sizeAtoms,
97
+ };
98
+ }
99
+ else {
100
+ if (args.operations === undefined) {
101
+ return toolError("invalid_request", "Batch requires operations.", false);
102
+ }
103
+ const operations = [];
104
+ for (const operation of args.operations) {
105
+ if (operation.action === "cancel") {
106
+ if (operation.orderId === undefined) {
107
+ return toolError("invalid_request", "Batch cancel requires orderId.", false);
108
+ }
109
+ operations.push({ action: "cancel", orderId: operation.orderId });
110
+ continue;
111
+ }
112
+ if (operation.clientOrderId === undefined
113
+ || operation.side === undefined
114
+ || operation.orderType === undefined
115
+ || operation.limitPriceAtoms === undefined
116
+ || operation.sizeAtoms === undefined
117
+ || (operation.action === "replace" && operation.orderId === undefined)) {
118
+ return toolError("invalid_request", `Batch ${operation.action} has incomplete fields.`, false);
119
+ }
120
+ const place = {
121
+ ...(operation.accountSequence === undefined
122
+ ? {}
123
+ : { accountSequence: operation.accountSequence }),
124
+ clientOrderId: operation.clientOrderId,
125
+ side: operation.side,
126
+ orderType: operation.orderType,
127
+ limitPriceAtoms: operation.limitPriceAtoms,
128
+ sizeAtoms: operation.sizeAtoms,
129
+ };
130
+ operations.push(operation.action === "replace"
131
+ ? { action: "replace", orderId: operation.orderId, ...place }
132
+ : { action: "place", ...place });
133
+ }
134
+ request = {
135
+ action: "batch",
136
+ ownerWallet: args.ownerWallet,
137
+ sessionPublicKey: args.sessionPublicKey,
138
+ operations,
139
+ };
140
+ }
141
+ return request;
142
+ }
143
+ const PLATFORM_MARKET_PAGE_LIMIT = 100;
144
+ const PLATFORM_MARKET_MAX_PAGES = 20;
145
+ /**
146
+ * Every live platform market keyed by label, so the tool can hand agents the
147
+ * opaque `market_id` (and asset ids) that every by-market tool takes. A
148
+ * platform read failure leaves the list unidentified rather than failing it.
149
+ */
150
+ async function platformMarketIdentities(platformClient) {
151
+ const identities = new Map();
152
+ try {
153
+ let cursor;
154
+ for (let page = 0; page < PLATFORM_MARKET_MAX_PAGES; page += 1) {
155
+ const response = await platformClient.markets.list(cursor === undefined
156
+ ? { limit: PLATFORM_MARKET_PAGE_LIMIT }
157
+ : { limit: PLATFORM_MARKET_PAGE_LIMIT, cursor });
158
+ for (const market of response.markets) {
159
+ identities.set(market.label, {
160
+ market_id: market.market_id,
161
+ base_asset_id: market.base_asset_id,
162
+ quote_asset_id: market.quote_asset_id,
163
+ status: market.status,
164
+ available_actions: market.available_actions,
165
+ });
166
+ }
167
+ if (!response.page.has_more || response.page.next_cursor === null)
168
+ break;
169
+ cursor = response.page.next_cursor;
170
+ }
171
+ }
172
+ catch {
173
+ // Identity is a convenience layered on the Sonar list; never fail the list for it.
174
+ }
175
+ return identities;
176
+ }
34
177
  export async function createStrataMcpServer(options = {}) {
35
178
  const client = strataClient(options);
36
179
  const platformClient = options.platformClient ?? new StrataPlatformClient({
@@ -60,6 +203,19 @@ export async function createStrataMcpServer(options = {}) {
60
203
  },
61
204
  ],
62
205
  }));
206
+ server.registerResource("strata_platform_graph", STRATA_PLATFORM_GRAPH_URI, {
207
+ title: "Strata Platform Graph",
208
+ description: "Complete customer-safe entity, operation, and workflow graph with live capability gates.",
209
+ mimeType: "application/json",
210
+ }, async () => ({
211
+ contents: [
212
+ {
213
+ uri: STRATA_PLATFORM_GRAPH_URI,
214
+ mimeType: "application/json",
215
+ text: JSON.stringify(await platformClient.discovery.graph()),
216
+ },
217
+ ],
218
+ }));
63
219
  server.registerResource("strata_action_graph", STRATA_ACTION_GRAPH_URI, {
64
220
  title: "Strata Action Graph",
65
221
  description: "Live executable topology for discovery, quoting, external signing, and submission.",
@@ -83,38 +239,780 @@ export async function createStrataMcpServer(options = {}) {
83
239
  .max(2_000)
84
240
  .describe("The user's concrete Strata market or quote objective."),
85
241
  },
86
- }, async ({ objective }) => ({
87
- description: "Capability-gated Strata objective",
88
- messages: [
89
- {
90
- role: "user",
91
- content: {
92
- type: "text",
93
- text: `${STRATA_AGENT_HARNESS_INSTRUCTIONS}\n\nObjective: ${objective.trim()}`,
94
- },
95
- },
96
- ],
97
- }));
98
- server.registerTool("strata_capabilities", {
99
- title: "Strata capabilities",
100
- description: "See which Strata features are currently available to MCP clients.",
242
+ }, async ({ objective }) => ({
243
+ description: "Capability-gated Strata objective",
244
+ messages: [
245
+ {
246
+ role: "user",
247
+ content: {
248
+ type: "text",
249
+ text: `${STRATA_AGENT_HARNESS_INSTRUCTIONS}\n\nObjective: ${objective.trim()}`,
250
+ },
251
+ },
252
+ ],
253
+ }));
254
+ server.registerTool("strata_capabilities", {
255
+ title: "Strata capabilities",
256
+ description: "See which Strata features are currently available to MCP clients.",
257
+ annotations: {
258
+ readOnlyHint: true,
259
+ destructiveHint: false,
260
+ idempotentHint: true,
261
+ openWorldHint: true,
262
+ },
263
+ }, async () => toolResult(await client.capabilities(), "Current Strata capabilities."));
264
+ server.registerTool("strata_action_graph", {
265
+ title: "Strata action graph",
266
+ description: "Discover live operations, required capabilities, transition conditions, and external signing boundaries.",
267
+ annotations: {
268
+ readOnlyHint: true,
269
+ destructiveHint: false,
270
+ idempotentHint: true,
271
+ openWorldHint: true,
272
+ },
273
+ }, async () => toolResult(await client.actionGraph(), "Current Strata action graph."));
274
+ server.registerTool("strata_platform_graph", {
275
+ title: "Strata platform graph",
276
+ description: "Discover every public module, entity relationship, operation binding, workflow, and live availability gate.",
277
+ annotations: {
278
+ readOnlyHint: true,
279
+ destructiveHint: false,
280
+ idempotentHint: true,
281
+ openWorldHint: true,
282
+ },
283
+ }, async () => {
284
+ const graph = await platformClient.discovery.graph();
285
+ const liveOperations = graph.operations.filter((operation) => operation.available).length;
286
+ return toolResult(graph, `${liveOperations} of ${graph.operations.length} mapped Strata operations are currently live.`);
287
+ });
288
+ server.registerTool("strata_market_making_status", {
289
+ title: "Read Strata maker status",
290
+ description: "A maker's products, live exposure, health, and kill state in one market — public by wallet address, no signature.",
291
+ inputSchema: {
292
+ marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
293
+ walletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
294
+ },
295
+ annotations: {
296
+ readOnlyHint: true,
297
+ destructiveHint: false,
298
+ idempotentHint: true,
299
+ openWorldHint: true,
300
+ },
301
+ }, async ({ marketId, walletAddress }) => {
302
+ const response = await platformClient.marketMaking.status(marketId, walletAddress);
303
+ return toolResult(response, `${response.active_products} active maker products; reconcile intent, Strand, Current, signed-quote, and dead-man state before changing exposure.`);
304
+ });
305
+ server.registerTool("strata_market_making_reputation", {
306
+ title: "Read Strata maker reputation",
307
+ description: "A maker's reliability, participation, tier, and signed-quote eligibility in one market — public by wallet address, no signature.",
308
+ inputSchema: {
309
+ marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
310
+ walletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
311
+ },
312
+ annotations: {
313
+ readOnlyHint: true,
314
+ destructiveHint: false,
315
+ idempotentHint: true,
316
+ openWorldHint: true,
317
+ },
318
+ }, async ({ marketId, walletAddress }) => toolResult(await platformClient.marketMaking.reputation(marketId, walletAddress), "Maker reputation record. Use tier_progress and signed_quote_stream_eligible before choosing a maker transport."));
319
+ server.registerTool("strata_status", {
320
+ title: "Strata status",
321
+ description: "Read product-level readiness and the number of currently live mapped operations.",
322
+ annotations: {
323
+ readOnlyHint: true,
324
+ destructiveHint: false,
325
+ idempotentHint: true,
326
+ openWorldHint: true,
327
+ },
328
+ }, async () => {
329
+ const status = await platformClient.discovery.status();
330
+ return toolResult(status, `Strata is ${status.status}; ${status.available_operations} mapped operations are live.`);
331
+ });
332
+ server.registerTool("strata_candles", {
333
+ title: "Strata candles",
334
+ description: "Read bounded time-bucketed candles for one opaque Strata market ID.",
335
+ inputSchema: {
336
+ marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
337
+ fromMs: z.number().int().nonnegative(),
338
+ toMs: z.number().int().positive(),
339
+ resolutionSeconds: z.number().int().min(60).max(86_400).optional().default(300),
340
+ },
341
+ annotations: {
342
+ readOnlyHint: true,
343
+ destructiveHint: false,
344
+ idempotentHint: true,
345
+ openWorldHint: true,
346
+ },
347
+ }, async ({ marketId, fromMs, toMs, resolutionSeconds }) => {
348
+ const candles = await platformClient.marketData.candles(marketId, {
349
+ fromMs,
350
+ toMs,
351
+ resolutionSeconds,
352
+ });
353
+ return toolResult(candles, `${candles.candles.length} Strata candles returned.`);
354
+ });
355
+ server.registerTool("strata_marks", {
356
+ title: "Strata mark",
357
+ description: "Read the current customer-facing reference price for one opaque market ID.",
358
+ inputSchema: {
359
+ marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
360
+ },
361
+ annotations: {
362
+ readOnlyHint: true,
363
+ destructiveHint: false,
364
+ idempotentHint: true,
365
+ openWorldHint: true,
366
+ },
367
+ }, async ({ marketId }) => {
368
+ const mark = await platformClient.marketData.mark(marketId);
369
+ return toolResult(mark, mark.stale ? "Strata mark is stale." : "Current Strata mark.");
370
+ });
371
+ server.registerTool("strata_book", {
372
+ title: "Strata order book",
373
+ description: "Read the executable order book for one opaque market ID: bids and asks, one size per price "
374
+ + "level. Top of book is the best bid and ask.",
375
+ inputSchema: {
376
+ marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
377
+ depth: z
378
+ .number()
379
+ .int()
380
+ .min(1)
381
+ .max(2_000)
382
+ .optional()
383
+ .describe("Price levels per side (default server depth; max 2000)."),
384
+ },
385
+ annotations: {
386
+ readOnlyHint: true,
387
+ destructiveHint: false,
388
+ idempotentHint: true,
389
+ openWorldHint: true,
390
+ },
391
+ }, async ({ marketId, depth }) => {
392
+ const book = await platformClient.books.snapshot(marketId, depth === undefined ? {} : { depth });
393
+ return toolResult(book, `Book for ${marketId}: ${book.bids.length} bid / ${book.asks.length} ask levels at sequence ${book.sequence}.`);
394
+ });
395
+ server.registerTool("strata_bbo", {
396
+ title: "Strata best bid/ask",
397
+ description: "Read the current best bid and best ask (top of book) for one opaque market ID.",
398
+ inputSchema: {
399
+ marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
400
+ },
401
+ annotations: {
402
+ readOnlyHint: true,
403
+ destructiveHint: false,
404
+ idempotentHint: true,
405
+ openWorldHint: true,
406
+ },
407
+ }, async ({ marketId }) => {
408
+ const bbo = await platformClient.books.bestBidAsk(marketId);
409
+ return toolResult(bbo, `BBO for ${marketId}: bid ${bbo.best_bid?.price_atoms ?? "—"} / ask ${bbo.best_ask?.price_atoms ?? "—"}.`);
410
+ });
411
+ server.registerTool("strata_trades", {
412
+ title: "Strata recent trades",
413
+ description: "Read recent anonymized prints for one opaque market ID: price, size, side, and time.",
414
+ inputSchema: {
415
+ marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
416
+ limit: z
417
+ .number()
418
+ .int()
419
+ .min(1)
420
+ .max(500)
421
+ .optional()
422
+ .describe("Most recent prints to return (default server limit; max 500)."),
423
+ },
424
+ annotations: {
425
+ readOnlyHint: true,
426
+ destructiveHint: false,
427
+ idempotentHint: true,
428
+ openWorldHint: true,
429
+ },
430
+ }, async ({ marketId, limit }) => {
431
+ const trades = await platformClient.books.trades(marketId, limit === undefined ? {} : { limit });
432
+ return toolResult(trades, `${trades.trades.length} recent prints for ${marketId}.`);
433
+ });
434
+ server.registerTool("strata_execution_status", {
435
+ title: "Strata execution status",
436
+ description: "Recover prepared state or a restart-durable confirmed execution receipt.",
437
+ inputSchema: {
438
+ marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
439
+ executionId: z.string().regex(/^se_[0-9a-f]{32}$/),
440
+ },
441
+ annotations: {
442
+ readOnlyHint: true,
443
+ destructiveHint: false,
444
+ idempotentHint: true,
445
+ openWorldHint: true,
446
+ },
447
+ }, async ({ marketId, executionId }) => {
448
+ const receipt = await platformClient.executions.status(marketId, executionId);
449
+ return toolResult(receipt, `Execution is ${receipt.status}.`);
450
+ });
451
+ server.registerTool("strata_twaps", {
452
+ title: "Strata TWAPs",
453
+ description: "Read sanitized progress and terminal receipts for wallet-owned TWAP schedules.",
454
+ inputSchema: {
455
+ marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
456
+ walletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
457
+ },
458
+ annotations: {
459
+ readOnlyHint: true,
460
+ destructiveHint: false,
461
+ idempotentHint: true,
462
+ openWorldHint: true,
463
+ },
464
+ }, async ({ marketId, walletAddress }) => {
465
+ const response = await platformClient.algos.twaps(marketId, walletAddress);
466
+ return toolResult(response, `${response.twaps.length} TWAP schedules returned.`);
467
+ });
468
+ server.registerTool("strata_twap_challenge", {
469
+ title: "Prepare a Strata TWAP authorization",
470
+ description: "Request exact external-signing bytes for a bounded TWAP schedule.",
471
+ inputSchema: {
472
+ marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
473
+ ownerWallet: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
474
+ sessionPublicKey: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
475
+ side: z.enum(["buy", "sell"]),
476
+ totalSizeAtoms: z.string().regex(/^[1-9][0-9]*$/),
477
+ slicesTotal: z.number().int().min(2).max(120),
478
+ maximumToleranceBps: z.number().int().min(1).max(1_000),
479
+ intervalSlots: z.number().int().min(25).max(4_500),
480
+ limitPriceAtoms: z.string().regex(/^[1-9][0-9]*$/),
481
+ },
482
+ annotations: {
483
+ readOnlyHint: false,
484
+ destructiveHint: false,
485
+ idempotentHint: false,
486
+ openWorldHint: true,
487
+ },
488
+ }, async (input) => {
489
+ const response = await platformClient.algos.challenge(input.marketId, {
490
+ action: "place",
491
+ ownerWallet: input.ownerWallet,
492
+ sessionPublicKey: input.sessionPublicKey,
493
+ side: input.side,
494
+ totalSizeAtoms: input.totalSizeAtoms,
495
+ slicesTotal: input.slicesTotal,
496
+ maximumToleranceBps: input.maximumToleranceBps,
497
+ intervalSlots: input.intervalSlots,
498
+ limitPriceAtoms: input.limitPriceAtoms,
499
+ });
500
+ return toolResult(response, "Sign the returned authorization payload externally.");
501
+ });
502
+ server.registerTool("strata_twap_cancel", {
503
+ title: "Prepare Strata TWAP cancellation",
504
+ description: "Request exact external-signing bytes to cancel one active owned TWAP.",
505
+ inputSchema: {
506
+ marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
507
+ ownerWallet: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
508
+ sessionPublicKey: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
509
+ twapId: z.string().regex(/^twap_[0-9a-f]{32}$/),
510
+ },
511
+ annotations: {
512
+ readOnlyHint: false,
513
+ destructiveHint: true,
514
+ idempotentHint: false,
515
+ openWorldHint: true,
516
+ },
517
+ }, async (input) => {
518
+ const response = await platformClient.algos.challenge(input.marketId, {
519
+ action: "cancel",
520
+ ownerWallet: input.ownerWallet,
521
+ sessionPublicKey: input.sessionPublicKey,
522
+ twapId: input.twapId,
523
+ });
524
+ return toolResult(response, "Sign the returned cancellation payload externally.");
525
+ });
526
+ server.registerTool("strata_twap_prepare", {
527
+ title: "Prepare Strata TWAP transaction",
528
+ description: "Prepare a canonical TWAP transaction. One signature: pass the action itself (place fields or twapId to cancel) and sign only the returned transaction. (A challengeId + authorizationSignature is still accepted.)",
529
+ inputSchema: {
530
+ marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
531
+ challengeId: z.string().regex(/^twc_[0-9a-f]{32}$/).optional(),
532
+ authorizationSignature: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{64,88}$/).optional(),
533
+ action: z.enum(["place", "cancel"]).optional(),
534
+ ownerWallet: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).optional(),
535
+ sessionPublicKey: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).optional(),
536
+ side: z.enum(["buy", "sell"]).optional(),
537
+ totalSizeAtoms: z.string().regex(/^[1-9][0-9]*$/).optional(),
538
+ slicesTotal: z.number().int().min(2).max(120).optional(),
539
+ maximumToleranceBps: z.number().int().min(1).max(1_000).optional(),
540
+ intervalSlots: z.number().int().min(25).max(4_500).optional(),
541
+ limitPriceAtoms: z.string().regex(/^[1-9][0-9]*$/).optional(),
542
+ twapId: z.string().regex(/^twap_[0-9a-f]{32}$/).optional(),
543
+ },
544
+ annotations: {
545
+ readOnlyHint: false,
546
+ destructiveHint: false,
547
+ idempotentHint: false,
548
+ openWorldHint: true,
549
+ },
550
+ }, async (input) => {
551
+ if (input.challengeId !== undefined || input.authorizationSignature !== undefined) {
552
+ if (input.challengeId === undefined || input.authorizationSignature === undefined) {
553
+ return toolError("invalid_request", "The two-step path needs both challengeId and authorizationSignature.", false);
554
+ }
555
+ const response = await platformClient.algos.prepare(input.marketId, {
556
+ challengeId: input.challengeId,
557
+ authorizationSignature: input.authorizationSignature,
558
+ });
559
+ return toolResult(response, "Verify and sign this canonical transaction externally.");
560
+ }
561
+ if (input.ownerWallet === undefined || input.sessionPublicKey === undefined || input.action === undefined) {
562
+ return toolError("invalid_request", "Pass action, ownerWallet, and sessionPublicKey (or a signed challenge).", false);
563
+ }
564
+ let operation;
565
+ if (input.action === "cancel") {
566
+ if (input.twapId === undefined)
567
+ return toolError("invalid_request", "Cancel requires twapId.", false);
568
+ operation = {
569
+ action: "cancel",
570
+ ownerWallet: input.ownerWallet,
571
+ sessionPublicKey: input.sessionPublicKey,
572
+ twapId: input.twapId,
573
+ };
574
+ }
575
+ else {
576
+ if (input.side === undefined
577
+ || input.totalSizeAtoms === undefined
578
+ || input.slicesTotal === undefined
579
+ || input.maximumToleranceBps === undefined
580
+ || input.intervalSlots === undefined
581
+ || input.limitPriceAtoms === undefined) {
582
+ return toolError("invalid_request", "Place requires side, totalSizeAtoms, slicesTotal, maximumToleranceBps, intervalSlots, and limitPriceAtoms.", false);
583
+ }
584
+ operation = {
585
+ action: "place",
586
+ ownerWallet: input.ownerWallet,
587
+ sessionPublicKey: input.sessionPublicKey,
588
+ side: input.side,
589
+ totalSizeAtoms: input.totalSizeAtoms,
590
+ slicesTotal: input.slicesTotal,
591
+ maximumToleranceBps: input.maximumToleranceBps,
592
+ intervalSlots: input.intervalSlots,
593
+ limitPriceAtoms: input.limitPriceAtoms,
594
+ };
595
+ }
596
+ const response = await platformClient.algos.prepare(input.marketId, { operation });
597
+ return toolResult(response, "One signature: verify this canonical transaction, then sign it externally with the session key and submit.");
598
+ });
599
+ server.registerTool("strata_twap_submit", {
600
+ title: "Submit Strata TWAP transaction",
601
+ description: "Submit the exact externally signed TWAP transaction idempotently.",
602
+ inputSchema: {
603
+ marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
604
+ twapControlId: z.string().regex(/^twctl_[0-9a-f]{32}$/),
605
+ signedTransactionBase64: z.string().min(4),
606
+ idempotencyKey: z.string().regex(/^[A-Za-z0-9._-]{1,64}$/),
607
+ },
608
+ annotations: {
609
+ readOnlyHint: false,
610
+ destructiveHint: true,
611
+ idempotentHint: true,
612
+ openWorldHint: true,
613
+ },
614
+ }, async ({ marketId, twapControlId, signedTransactionBase64, idempotencyKey }) => {
615
+ const response = await platformClient.algos.submit(marketId, {
616
+ twapControlId,
617
+ signedTransactionBase64,
618
+ idempotencyKey,
619
+ });
620
+ return toolResult(response, `TWAP action submitted as ${response.signature}.`);
621
+ });
622
+ server.registerTool("strata_portfolio", {
623
+ title: "Strata account",
624
+ description: "The whole account in one public read, by wallet address: balances (total / available / locked, exact USD), positions, open orders, and recent fills across every live market. No signature, no session key, no market selection.",
625
+ inputSchema: {
626
+ walletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
627
+ },
628
+ annotations: {
629
+ readOnlyHint: true,
630
+ destructiveHint: false,
631
+ idempotentHint: true,
632
+ openWorldHint: true,
633
+ },
634
+ }, async ({ walletAddress }) => {
635
+ const response = await platformClient.account.read(walletAddress);
636
+ const activity = `${response.open_orders.length} open orders, ${response.recent_fills.length} recent fills`
637
+ + (response.unavailable_market_ids.length > 0
638
+ ? ` (${response.unavailable_market_ids.length} markets unavailable)`
639
+ : "");
640
+ return toolResult(response, response.valuation_complete
641
+ ? `${response.balances.length} held assets; ${activity}; equity ${response.equity_usd_micros} USD micros at slot ${response.observed_slot}.`
642
+ : `${response.balances.length} held assets; ${activity}; ${response.unpriced_asset_ids.length} unpriced, USD totals unavailable.`);
643
+ });
644
+ server.registerTool("strata_portfolio_history", {
645
+ title: "Strata portfolio history",
646
+ description: "Read genuine stored account-equity history in exact USD micros.",
647
+ inputSchema: {
648
+ walletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
649
+ range: z.enum(["24h", "7d", "30d"]).optional().default("24h"),
650
+ },
651
+ annotations: {
652
+ readOnlyHint: true,
653
+ destructiveHint: false,
654
+ idempotentHint: true,
655
+ openWorldHint: true,
656
+ },
657
+ }, async ({ walletAddress, range }) => {
658
+ const response = await platformClient.account.portfolioHistory(walletAddress, range);
659
+ return toolResult(response, `${response.points.length} stored equity samples returned.`);
660
+ });
661
+ server.registerTool("strata_vault_status", {
662
+ title: "Strata Vault status",
663
+ description: "Read sealed owner state and optional external-session readiness without construction identifiers.",
664
+ inputSchema: {
665
+ walletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
666
+ sessionPublicKey: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).optional(),
667
+ },
668
+ annotations: {
669
+ readOnlyHint: true,
670
+ destructiveHint: false,
671
+ idempotentHint: true,
672
+ openWorldHint: true,
673
+ },
674
+ }, async ({ walletAddress, sessionPublicKey }) => {
675
+ const response = await platformClient.vault.status({ walletAddress, sessionPublicKey });
676
+ return toolResult(response, response.session === null
677
+ ? `Vault is ${response.state}; no session was requested.`
678
+ : `Vault is ${response.state}; requested session is ${response.session.state}.`);
679
+ });
680
+ server.registerTool("strata_vault_pause", {
681
+ title: "Prepare Strata Vault pause",
682
+ description: "Prepare an owner-authorized pause or resume transaction for external verification, signing, and broadcast.",
683
+ inputSchema: {
684
+ walletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
685
+ paused: z.boolean(),
686
+ },
687
+ annotations: {
688
+ readOnlyHint: false,
689
+ destructiveHint: true,
690
+ idempotentHint: false,
691
+ openWorldHint: true,
692
+ },
693
+ }, async ({ walletAddress, paused }) => {
694
+ const response = await platformClient.vault.preparePause({ walletAddress, paused });
695
+ return toolResult(response, `Verify this ${paused ? "pause" : "resume"} transaction, then owner-sign it and pass preparation_id + the signed transaction to strata_vault_submit (Strata pays the fee when sponsored is true).`);
696
+ });
697
+ server.registerTool("strata_vault_setup", {
698
+ title: "Prepare Strata Vault onboarding",
699
+ description: "One-signature onboarding: register an external session key for a wallet. Only the wallet and the session key are needed; one session then trades every market. Policy fields are optional. A first strata_vault_deposit that names the session key does this in the same transaction.",
700
+ inputSchema: {
701
+ walletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
702
+ sessionPublicKey: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
703
+ marketId: z.string().regex(/^market_[0-9a-f]{32}$/).optional(),
704
+ spendingLimits: z
705
+ .array(z.object({
706
+ assetId: z.string().regex(/^asset_[0-9a-f]{32}$/),
707
+ maximumPerExecutionAtoms: z.string().regex(/^[1-9][0-9]*$/).optional(),
708
+ }))
709
+ .max(4)
710
+ .optional(),
711
+ expiresAtMs: z.number().int().positive().optional(),
712
+ minimumIntervalSeconds: z.number().int().min(1).max(86_400).optional(),
713
+ maximumToleranceBps: z.number().int().min(1).max(1_000).optional(),
714
+ },
715
+ annotations: {
716
+ readOnlyHint: false,
717
+ destructiveHint: false,
718
+ idempotentHint: false,
719
+ openWorldHint: true,
720
+ },
721
+ }, async ({ walletAddress, sessionPublicKey, marketId, spendingLimits, expiresAtMs, minimumIntervalSeconds, maximumToleranceBps, }) => {
722
+ const response = await platformClient.vault.prepareSetup({
723
+ walletAddress,
724
+ sessionPublicKey,
725
+ marketId: marketId ?? null,
726
+ expiresAtMs: expiresAtMs ?? null,
727
+ minimumIntervalSeconds,
728
+ maximumToleranceBps,
729
+ spendingLimits: (spendingLimits ?? []).map((limit) => ({
730
+ assetId: limit.assetId,
731
+ maximumPerExecutionAtoms: limit.maximumPerExecutionAtoms ?? null,
732
+ })),
733
+ });
734
+ return toolResult(response, "Verify every echoed session policy field, then owner-sign it and pass preparation_id + the signed transaction to strata_vault_submit (Strata pays the fee when sponsored is true).");
735
+ });
736
+ server.registerTool("strata_vault_deposit", {
737
+ title: "Prepare Strata Vault deposit",
738
+ description: "Prepare an exact owner-funded Vault deposit using opaque market and asset IDs. Name sessionPublicKey and a first deposit also registers that session in the same transaction (one owner signature onboards and funds the wallet).",
739
+ inputSchema: {
740
+ walletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
741
+ marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
742
+ assetId: z.string().regex(/^asset_[0-9a-f]{32}$/),
743
+ amountAtoms: z.string().regex(/^[1-9][0-9]*$/),
744
+ sessionPublicKey: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).optional(),
745
+ },
746
+ annotations: {
747
+ readOnlyHint: false,
748
+ destructiveHint: false,
749
+ idempotentHint: false,
750
+ openWorldHint: true,
751
+ },
752
+ }, async ({ walletAddress, marketId, assetId, amountAtoms, sessionPublicKey }) => {
753
+ const response = await platformClient.vault.prepareDeposit({
754
+ walletAddress,
755
+ marketId,
756
+ assetId,
757
+ amountAtoms,
758
+ sessionPublicKey: sessionPublicKey ?? null,
759
+ });
760
+ return toolResult(response, response.registers_session
761
+ ? "This deposit also registers the session key. Verify the exact market, asset, amount, and session, then owner-sign it and pass preparation_id + the signed transaction to strata_vault_submit (Strata pays the fee when sponsored is true)."
762
+ : "Verify the exact market, asset, and amount, then owner-sign it and pass preparation_id + the signed transaction to strata_vault_submit (Strata pays the fee when sponsored is true).");
763
+ });
764
+ server.registerTool("strata_vault_withdraw", {
765
+ title: "Prepare Strata Vault withdrawal",
766
+ description: "Prepare an exact owner-authorized withdrawal to a destination wallet.",
767
+ inputSchema: {
768
+ walletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
769
+ marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
770
+ assetId: z.string().regex(/^asset_[0-9a-f]{32}$/),
771
+ destinationWalletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
772
+ amountAtoms: z.string().regex(/^[1-9][0-9]*$/),
773
+ },
774
+ annotations: {
775
+ readOnlyHint: false,
776
+ destructiveHint: true,
777
+ idempotentHint: false,
778
+ openWorldHint: true,
779
+ },
780
+ }, async ({ walletAddress, marketId, assetId, destinationWalletAddress, amountAtoms }) => {
781
+ const response = await platformClient.vault.prepareWithdrawal({
782
+ walletAddress,
783
+ marketId,
784
+ assetId,
785
+ destinationWalletAddress,
786
+ amountAtoms,
787
+ });
788
+ return toolResult(response, "Verify the exact destination and amount, then owner-sign it and pass preparation_id + the signed transaction to strata_vault_submit (Strata pays the fee when sponsored is true).");
789
+ });
790
+ server.registerTool("strata_vault_delegate", {
791
+ title: "Prepare Strata Vault session control",
792
+ description: "Prepare owner-authorized revocation of one externally held Vault session key.",
793
+ inputSchema: {
794
+ walletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
795
+ sessionPublicKey: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
796
+ action: z.literal("revoke"),
797
+ },
798
+ annotations: {
799
+ readOnlyHint: false,
800
+ destructiveHint: true,
801
+ idempotentHint: false,
802
+ openWorldHint: true,
803
+ },
804
+ }, async ({ walletAddress, sessionPublicKey, action }) => {
805
+ const response = await platformClient.vault.prepareDelegate({
806
+ walletAddress,
807
+ sessionPublicKey,
808
+ action,
809
+ });
810
+ return toolResult(response, "Verify both identities and the destructive action, then owner-sign it and pass preparation_id + the signed transaction to strata_vault_submit (Strata pays the fee when sponsored is true).");
811
+ });
812
+ server.registerTool("strata_vault_policy", {
813
+ title: "Prepare Strata Vault withdrawal policy",
814
+ description: "Prepare an owner-authorized blocked or restricted withdrawal access policy.",
815
+ inputSchema: {
816
+ walletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
817
+ mode: z.enum(["blocked", "restricted"]),
818
+ allowedWalletAddresses: z.array(z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/)).max(8).optional().default([]),
819
+ },
820
+ annotations: {
821
+ readOnlyHint: false,
822
+ destructiveHint: true,
823
+ idempotentHint: false,
824
+ openWorldHint: true,
825
+ },
826
+ }, async ({ walletAddress, mode, allowedWalletAddresses }) => {
827
+ const response = await platformClient.vault.preparePolicy({
828
+ walletAddress,
829
+ withdrawalAccess: { mode, allowedWalletAddresses },
830
+ });
831
+ return toolResult(response, "Verify the exact withdrawal access policy, then owner-sign it and pass preparation_id + the signed transaction to strata_vault_submit (Strata pays the fee when sponsored is true).");
832
+ });
833
+ server.registerTool("strata_vault_submit", {
834
+ title: "Submit a prepared Strata Vault transaction",
835
+ description: "Submit an owner-signed prepared Vault transaction (setup, deposit, withdrawal, session, "
836
+ + "policy, pause). Strata verifies it is exactly the prepared transaction, pays the network "
837
+ + "fee and any rent when the preparation was sponsored (owners without SOL; recovered later "
838
+ + "from their deposits as network_cost_atoms), and broadcasts it — the owner needs no SOL "
839
+ + "and no RPC. Idempotent per idempotencyKey; read the outcome with strata_vault_submission.",
840
+ inputSchema: {
841
+ preparationId: z.string().regex(/^vp_[0-9a-f]{32}$/).describe("preparation_id from the prepare response."),
842
+ signedTransactionBase64: z.string().min(1).describe("The prepared transaction with the owner's signature added, base64."),
843
+ idempotencyKey: z.string().min(1).max(64).regex(/^[A-Za-z0-9._-]+$/),
844
+ },
845
+ annotations: {
846
+ readOnlyHint: false,
847
+ destructiveHint: false,
848
+ idempotentHint: true,
849
+ openWorldHint: true,
850
+ },
851
+ }, async ({ preparationId, signedTransactionBase64, idempotencyKey }) => {
852
+ const response = await platformClient.vault.submit({
853
+ preparationId,
854
+ signedTransactionBase64,
855
+ idempotencyKey,
856
+ });
857
+ return toolResult(response, `Vault ${response.action} ${response.status}${response.sponsored ? " (Strata paid the fee)" : ""}: `
858
+ + `signature ${response.signature}. Poll strata_vault_submission until confirmed.`);
859
+ });
860
+ server.registerTool("strata_vault_submission", {
861
+ title: "Strata Vault submission status",
862
+ description: "Read the durable outcome of a submitted Vault transaction: submitted, confirmed, or failed.",
863
+ inputSchema: {
864
+ preparationId: z.string().regex(/^vp_[0-9a-f]{32}$/),
865
+ },
866
+ annotations: {
867
+ readOnlyHint: true,
868
+ destructiveHint: false,
869
+ idempotentHint: true,
870
+ openWorldHint: true,
871
+ },
872
+ }, async ({ preparationId }) => {
873
+ const response = await platformClient.vault.submission(preparationId);
874
+ return toolResult(response, `Vault ${response.action} is ${response.status}`
875
+ + `${response.failure_code ? ` (${response.failure_code})` : ""}.`);
876
+ });
877
+ server.registerTool("strata_rewards", {
878
+ title: "Strata rewards",
879
+ description: "Read the current rewards season, standings, and optional owner score.",
880
+ inputSchema: {
881
+ walletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).optional(),
882
+ limit: z.number().int().min(1).max(100).optional().default(25),
883
+ },
884
+ annotations: {
885
+ readOnlyHint: true,
886
+ destructiveHint: false,
887
+ idempotentHint: true,
888
+ openWorldHint: true,
889
+ },
890
+ }, async ({ walletAddress, limit }) => {
891
+ const response = await platformClient.rewards.read({ walletAddress, limit });
892
+ return toolResult(response, `${response.standings.length} reward standings returned.`);
893
+ });
894
+ server.registerTool("strata_referrals", {
895
+ title: "Strata referrals",
896
+ description: "Read an owner's referral state and exact claimable reward atoms.",
897
+ inputSchema: {
898
+ walletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
899
+ },
101
900
  annotations: {
102
901
  readOnlyHint: true,
103
902
  destructiveHint: false,
104
903
  idempotentHint: true,
105
904
  openWorldHint: true,
106
905
  },
107
- }, async () => toolResult(await client.capabilities(), "Current Strata capabilities."));
108
- server.registerTool("strata_action_graph", {
109
- title: "Strata action graph",
110
- description: "Discover live operations, required capabilities, transition conditions, and external signing boundaries.",
906
+ }, async ({ walletAddress }) => {
907
+ const response = await platformClient.referrals.read(walletAddress);
908
+ return toolResult(response, response.enabled ? "Referral state returned." : "Referrals are disabled.");
909
+ });
910
+ server.registerTool("strata_referral_link", {
911
+ title: "Link a Strata referral",
912
+ description: "Prepare or submit an externally authorized referral link for a new wallet.",
913
+ inputSchema: {
914
+ walletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
915
+ referralCode: z.string().trim().min(1).max(64).regex(/^[A-Za-z0-9_-]+$/),
916
+ authorizationSignature: z.string().regex(/^(?:0x)?[0-9a-fA-F]{128}$/).optional(),
917
+ },
918
+ annotations: {
919
+ readOnlyHint: false,
920
+ destructiveHint: false,
921
+ idempotentHint: false,
922
+ openWorldHint: true,
923
+ },
924
+ }, async ({ walletAddress, referralCode, authorizationSignature }) => {
925
+ if (authorizationSignature === undefined) {
926
+ const payload = platformClient.referrals.linkAuthorizationPayload(referralCode);
927
+ return toolResult({
928
+ wallet_address: walletAddress,
929
+ authorization_payload_base64: Buffer.from(payload).toString("base64"),
930
+ }, "Have the referred wallet sign this payload externally, then call again with its hex signature.");
931
+ }
932
+ const response = await platformClient.referrals.link({
933
+ walletAddress,
934
+ referralCode,
935
+ authorizationSignature,
936
+ });
937
+ return toolResult(response, "Referral link is pending the wallet's first fill.");
938
+ });
939
+ server.registerTool("strata_referral_claim", {
940
+ title: "Claim Strata referral rewards",
941
+ description: "Prepare or submit an externally authorized request for currently claimable referral rewards.",
942
+ inputSchema: {
943
+ walletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
944
+ payoutWalletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).optional(),
945
+ authorizationSignature: z.string().regex(/^(?:0x)?[0-9a-fA-F]{128}$/).optional(),
946
+ },
947
+ annotations: {
948
+ readOnlyHint: false,
949
+ destructiveHint: false,
950
+ idempotentHint: false,
951
+ openWorldHint: true,
952
+ },
953
+ }, async ({ walletAddress, payoutWalletAddress, authorizationSignature }) => {
954
+ const payout = payoutWalletAddress ?? walletAddress;
955
+ if (authorizationSignature === undefined) {
956
+ const payload = platformClient.referrals.claimAuthorizationPayload(payout);
957
+ return toolResult({
958
+ wallet_address: walletAddress,
959
+ payout_wallet_address: payout,
960
+ authorization_payload_base64: Buffer.from(payload).toString("base64"),
961
+ }, "Have the claiming wallet sign this payload externally, then call again with its hex signature.");
962
+ }
963
+ const response = await platformClient.referrals.claim({
964
+ walletAddress,
965
+ payoutWalletAddress: payout,
966
+ authorizationSignature,
967
+ });
968
+ return toolResult(response, `${response.claimable_atoms} referral reward atoms requested.`);
969
+ });
970
+ server.registerTool("strata_bugs", {
971
+ title: "Strata bug reports",
972
+ description: "Read an owner's redacted bug reports and confirmed points.",
973
+ inputSchema: {
974
+ walletAddress: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
975
+ },
111
976
  annotations: {
112
977
  readOnlyHint: true,
113
978
  destructiveHint: false,
114
979
  idempotentHint: true,
115
980
  openWorldHint: true,
116
981
  },
117
- }, async () => toolResult(await client.actionGraph(), "Current Strata action graph."));
982
+ }, async ({ walletAddress }) => {
983
+ const response = await platformClient.bugs.read(walletAddress);
984
+ return toolResult(response, `${response.reports.length} redacted bug reports returned.`);
985
+ });
986
+ server.registerTool("strata_bug_submit", {
987
+ title: "Submit Strata bug report",
988
+ description: "Prepare or submit a bug report. Omit authorizationSignature to receive the exact "
989
+ + "payload for the owner wallet to sign externally; provide that hex signature to submit.",
990
+ inputSchema: {
991
+ ownerWallet: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/),
992
+ message: z.string().trim().min(1).max(2_000),
993
+ authorizationSignature: z.string().regex(/^(?:0x)?[0-9a-fA-F]{128}$/).optional(),
994
+ },
995
+ annotations: {
996
+ readOnlyHint: false,
997
+ destructiveHint: false,
998
+ idempotentHint: false,
999
+ openWorldHint: true,
1000
+ },
1001
+ }, async ({ ownerWallet, message, authorizationSignature }) => {
1002
+ if (authorizationSignature === undefined) {
1003
+ const payload = platformClient.bugs.authorizationPayload(message);
1004
+ return toolResult({
1005
+ owner_wallet: ownerWallet,
1006
+ authorization_payload_base64: Buffer.from(payload).toString("base64"),
1007
+ }, "Sign this payload externally, then call strata_bug_submit again with the hex signature.");
1008
+ }
1009
+ const response = await platformClient.bugs.submit({
1010
+ ownerWallet,
1011
+ message,
1012
+ authorizationSignature,
1013
+ });
1014
+ return toolResult(response, `Bug report ${response.bug_id} is pending review.`);
1015
+ });
118
1016
  const markets = server.registerTool("strata_markets", {
119
1017
  title: "Strata markets",
120
1018
  description: "List Strata markets and their current Sonar quote availability.",
@@ -133,13 +1031,22 @@ export async function createStrataMcpServer(options = {}) {
133
1031
  },
134
1032
  }, async ({ includePaused }) => guardedTool(client, "markets.read", async () => {
135
1033
  const response = await client.markets();
136
- const output = includePaused
137
- ? response
138
- : {
139
- ...response,
140
- markets: response.markets.filter((market) => market.ready),
141
- };
142
- return toolResult(output, `${output.markets.length} Strata markets available.`);
1034
+ const visible = includePaused
1035
+ ? response.markets
1036
+ : response.markets.filter((market) => market.ready);
1037
+ const identities = await platformMarketIdentities(platformClient);
1038
+ const output = {
1039
+ ...response,
1040
+ markets: visible.map((market) => {
1041
+ const identity = identities.get(market.label);
1042
+ return identity === undefined ? market : { ...market, ...identity };
1043
+ }),
1044
+ };
1045
+ const identified = output.markets.filter((market) => "market_id" in market).length;
1046
+ return toolResult(output, `${output.markets.length} Strata markets available`
1047
+ + (identified > 0
1048
+ ? `; ${identified} carry a market_id — pass it as marketId to every by-market tool.`
1049
+ : "."));
143
1050
  }));
144
1051
  const quote = server.registerTool("strata_quote", {
145
1052
  title: "Sonar quote",
@@ -157,15 +1064,16 @@ export async function createStrataMcpServer(options = {}) {
157
1064
  .regex(/^[0-9]+$/)
158
1065
  .max(20)
159
1066
  .describe("Exact input amount in the input token's smallest atomic unit."),
160
- slippageBps: z
1067
+ maximumToleranceBps: z
161
1068
  .number()
162
1069
  .int()
163
1070
  .min(0)
164
1071
  .max(1_000)
165
1072
  .optional()
166
- .default(DEFAULT_SLIPPAGE_BPS)
167
- .describe("Optional maximum execution tolerance in basis points. "
168
- + "The default 0 requires exact quoted output."),
1073
+ .default(DEFAULT_MAXIMUM_TOLERANCE_BPS)
1074
+ .describe("The most you accept below the quoted output, in basis points (default 0: the "
1075
+ + "quoted output exactly). This is YOUR choice. It is not price impact — "
1076
+ + "price_impact_pct in the response is measured from the book and is not a setting."),
169
1077
  },
170
1078
  annotations: {
171
1079
  readOnlyHint: true,
@@ -173,18 +1081,89 @@ export async function createStrataMcpServer(options = {}) {
173
1081
  idempotentHint: false,
174
1082
  openWorldHint: true,
175
1083
  },
176
- }, async ({ market, side, amountInAtoms, slippageBps }) => guardedTool(client, "quotes.read", async () => {
1084
+ }, async ({ market, side, amountInAtoms, maximumToleranceBps }) => guardedTool(client, "quotes.read", async () => {
177
1085
  const request = {
178
1086
  market,
179
1087
  side,
180
1088
  amountInAtoms,
181
- slippageBps,
1089
+ maximumToleranceBps,
182
1090
  };
183
1091
  const response = await client.quote(request);
184
- return toolResult(response, `Sonar ${response.side} quote: ${response.amount_in_consumed_atoms} input atoms `
185
- + `for ${response.amount_out_atoms} user-net output atoms; user-net minimum `
186
- + `${response.minimum_output_atoms}; expires at ${response.expires_at_ms}.`);
1092
+ return toolResult(response, quoteSummary(response));
1093
+ }));
1094
+ const exactOutputQuote = server.registerTool("strata_exact_output_quote", {
1095
+ title: "Sonar exact-output quote",
1096
+ description: "Request a short-lived Sonar quote for an exact output amount (for example: buy "
1097
+ + "1 SOL). Strata inverts its best route and returns the input that delivers it as "
1098
+ + "amount_in_atoms; minimum_output_atoms is the requested amount lowered by the "
1099
+ + "optional maximumToleranceBps (default 0: exactly the requested amount or the "
1100
+ + "execution fails closed). Execute it with the same quote_id flow as strata_quote.",
1101
+ inputSchema: {
1102
+ market: z
1103
+ .string()
1104
+ .min(1)
1105
+ .max(128)
1106
+ .describe("Market label such as SOL/USDC, or its public market ID."),
1107
+ side: z.enum(["buy", "sell"]).describe("Buy or sell the market's base asset."),
1108
+ amountOutAtoms: z
1109
+ .string()
1110
+ .regex(/^[0-9]+$/)
1111
+ .max(20)
1112
+ .describe("Output amount to receive at least, in the output token's smallest atomic unit "
1113
+ + "(base atoms for a buy, quote atoms for a sell)."),
1114
+ maximumToleranceBps: z
1115
+ .number()
1116
+ .int()
1117
+ .min(0)
1118
+ .max(1_000)
1119
+ .optional()
1120
+ .default(DEFAULT_MAXIMUM_TOLERANCE_BPS)
1121
+ .describe("The most you accept below the quoted output, in basis points (default 0: the "
1122
+ + "quoted output exactly). This is YOUR choice. It is not price impact — "
1123
+ + "price_impact_pct in the response is measured from the book and is not a setting."),
1124
+ },
1125
+ annotations: {
1126
+ readOnlyHint: true,
1127
+ destructiveHint: false,
1128
+ idempotentHint: false,
1129
+ openWorldHint: true,
1130
+ },
1131
+ }, async ({ market, side, amountOutAtoms, maximumToleranceBps }) => guardedTool(client, "quotes.read", async () => {
1132
+ const request = {
1133
+ market,
1134
+ side,
1135
+ amountOutAtoms,
1136
+ maximumToleranceBps,
1137
+ };
1138
+ const response = await client.quote(request);
1139
+ return toolResult(response, quoteSummary(response));
187
1140
  }));
1141
+ server.registerTool("strata_swap_quote", {
1142
+ title: "Sonar asset swap quote",
1143
+ description: "Request short-lived exact-input customer economics between two opaque Strata asset IDs.",
1144
+ inputSchema: {
1145
+ inputAssetId: z.string().regex(/^asset_[0-9a-f]{32}$/),
1146
+ outputAssetId: z.string().regex(/^asset_[0-9a-f]{32}$/),
1147
+ amountInAtoms: z.string().regex(/^[1-9][0-9]*$/).max(20),
1148
+ maximumToleranceBps: z.number().int().min(0).max(1_000).optional().default(0),
1149
+ },
1150
+ annotations: {
1151
+ readOnlyHint: true,
1152
+ destructiveHint: false,
1153
+ idempotentHint: false,
1154
+ openWorldHint: true,
1155
+ },
1156
+ }, async ({ inputAssetId, outputAssetId, amountInAtoms, maximumToleranceBps }) => {
1157
+ const response = await platformClient.quotes.swap({
1158
+ inputAssetId,
1159
+ outputAssetId,
1160
+ amountInAtoms,
1161
+ maximumToleranceBps,
1162
+ });
1163
+ return toolResult(response, `Sonar swap quote: ${response.amount_in_consumed_atoms} input atoms for `
1164
+ + `${response.amount_out_atoms} user-net output atoms; minimum `
1165
+ + `${response.minimum_output_atoms}; expires at ${response.expires_at_ms}.`);
1166
+ });
188
1167
  const executionChallenge = server.registerTool("strata_execution_challenge", {
189
1168
  title: "Strata execution challenge",
190
1169
  description: "Request canonical quote-bound authorization bytes for the external signer configured by the agent owner.",
@@ -201,7 +1180,8 @@ export async function createStrataMcpServer(options = {}) {
201
1180
  .string()
202
1181
  .regex(/^[0-9]+$/)
203
1182
  .max(20)
204
- .describe("Current Vault account sequence as an unsigned decimal string."),
1183
+ .optional()
1184
+ .describe("Optional Vault market account sequence as an unsigned decimal string. Omit it and Strata resolves the next sequence from the Vault's confirmed market account."),
205
1185
  },
206
1186
  annotations: {
207
1187
  readOnlyHint: false,
@@ -215,26 +1195,32 @@ export async function createStrataMcpServer(options = {}) {
215
1195
  quoteId,
216
1196
  ownerWallet,
217
1197
  sessionPublicKey,
218
- accountSequence,
1198
+ ...(accountSequence === undefined ? {} : { accountSequence }),
219
1199
  };
220
1200
  const response = await client.executionChallenge(request);
221
1201
  return toolResult(response, `Authorization challenge ${response.challenge_id}; expires at ${response.expires_at_ms}.`);
222
1202
  }));
223
1203
  const executionPrepare = server.registerTool("strata_execution_prepare", {
224
1204
  title: "Prepare Strata execution",
225
- description: "Exchange an externally signed authorization challenge for a quote-bound partially signed transaction.",
1205
+ description: "Prepare a quote-bound partially signed transaction. One signature: pass quoteId + ownerWallet + sessionPublicKey and sign only the returned transaction. (A challengeId + authorizationSignature from strata_execution_challenge is still accepted.)",
226
1206
  inputSchema: {
227
1207
  market: z.string().min(1).max(128).describe("Market label or public market ID."),
1208
+ quoteId: z.string().regex(/^sq_[0-9a-f]{32}$/).optional().describe("Unexpired Sonar quote ID (direct, one-signature path)."),
1209
+ ownerWallet: z.string().min(32).max(44).optional(),
1210
+ sessionPublicKey: z.string().min(32).max(44).optional(),
1211
+ accountSequence: z.string().regex(/^[0-9]+$/).max(20).optional(),
228
1212
  challengeId: z
229
1213
  .string()
230
1214
  .regex(/^sc_[0-9a-f]{32}$/)
231
- .describe("Execution challenge ID returned by Strata."),
1215
+ .optional()
1216
+ .describe("Execution challenge ID returned by Strata (two-step path)."),
232
1217
  authorizationSignature: z
233
1218
  .string()
234
1219
  .min(1)
235
1220
  .max(128)
236
1221
  .regex(/^[1-9A-HJ-NP-Za-km-z]+$/)
237
- .describe("Base58 Ed25519 signature made externally over the challenge payload."),
1222
+ .optional()
1223
+ .describe("Base58 Ed25519 signature made externally over the challenge payload (two-step path)."),
238
1224
  },
239
1225
  annotations: {
240
1226
  readOnlyHint: false,
@@ -242,14 +1228,28 @@ export async function createStrataMcpServer(options = {}) {
242
1228
  idempotentHint: false,
243
1229
  openWorldHint: true,
244
1230
  },
245
- }, async ({ market, challengeId, authorizationSignature }) => guardedTool(client, "trade.prepare", async () => {
246
- const request = {
247
- market,
248
- challengeId,
249
- authorizationSignature,
250
- };
1231
+ }, async ({ market, quoteId, ownerWallet, sessionPublicKey, accountSequence, challengeId, authorizationSignature }) => guardedTool(client, "trade.prepare", async () => {
1232
+ let request;
1233
+ if (challengeId !== undefined || authorizationSignature !== undefined) {
1234
+ if (challengeId === undefined || authorizationSignature === undefined) {
1235
+ return toolError("invalid_request", "The two-step path needs both challengeId and authorizationSignature.", false);
1236
+ }
1237
+ request = { market, challengeId, authorizationSignature };
1238
+ }
1239
+ else {
1240
+ if (quoteId === undefined || ownerWallet === undefined || sessionPublicKey === undefined) {
1241
+ return toolError("invalid_request", "Pass quoteId, ownerWallet, and sessionPublicKey (or a signed challenge).", false);
1242
+ }
1243
+ request = {
1244
+ market,
1245
+ quoteId,
1246
+ ownerWallet,
1247
+ sessionPublicKey,
1248
+ ...(accountSequence === undefined ? {} : { accountSequence }),
1249
+ };
1250
+ }
251
1251
  const response = await client.executionPrepare(request);
252
- return toolResult(response, `Prepared execution ${response.execution_id}; externally verify and sign before ${response.expires_at_ms}.`);
1252
+ return toolResult(response, `Prepared execution ${response.execution_id}; verify it, then sign the transaction externally with the session key before ${response.expires_at_ms} and submit.`);
253
1253
  }));
254
1254
  const executionSubmit = server.registerTool("strata_execution_submit", {
255
1255
  title: "Submit Strata execution",
@@ -297,7 +1297,12 @@ export async function createStrataMcpServer(options = {}) {
297
1297
  action: z.enum(["place", "cancel", "cancel_all", "replace", "batch"]),
298
1298
  ownerWallet: z.string().min(32).max(44),
299
1299
  sessionPublicKey: z.string().min(32).max(44),
300
- accountSequence: z.string().regex(/^[0-9]+$/).max(20).optional(),
1300
+ accountSequence: z
1301
+ .string()
1302
+ .regex(/^[0-9]+$/)
1303
+ .max(20)
1304
+ .optional()
1305
+ .describe("Optional Vault market account sequence. Omit it and Strata resolves the next sequence from the Vault's confirmed market account."),
301
1306
  clientOrderId: z.string().min(1).max(64).regex(/^[A-Za-z0-9._-]+$/).optional(),
302
1307
  side: z.enum(["buy", "sell"]).optional(),
303
1308
  orderType: z.enum(["good_until_cancelled", "post_only"]).optional(),
@@ -322,124 +1327,45 @@ export async function createStrataMcpServer(options = {}) {
322
1327
  openWorldHint: true,
323
1328
  },
324
1329
  }, async (args) => guardedTool(client, "orders.prepare", async () => {
325
- let request;
326
- if (args.action === "place") {
327
- if (args.accountSequence === undefined
328
- || args.clientOrderId === undefined
329
- || args.side === undefined
330
- || args.orderType === undefined
331
- || args.limitPriceAtoms === undefined
332
- || args.sizeAtoms === undefined) {
333
- return toolError("invalid_request", "Place requires accountSequence, clientOrderId, side, orderType, limitPriceAtoms, and sizeAtoms.", false);
334
- }
335
- request = {
336
- action: "place",
337
- ownerWallet: args.ownerWallet,
338
- sessionPublicKey: args.sessionPublicKey,
339
- accountSequence: args.accountSequence,
340
- clientOrderId: args.clientOrderId,
341
- side: args.side,
342
- orderType: args.orderType,
343
- limitPriceAtoms: args.limitPriceAtoms,
344
- sizeAtoms: args.sizeAtoms,
345
- };
346
- }
347
- else if (args.action === "cancel") {
348
- if (args.orderId === undefined) {
349
- return toolError("invalid_request", "Cancel requires orderId.", false);
350
- }
351
- request = {
352
- action: "cancel",
353
- ownerWallet: args.ownerWallet,
354
- sessionPublicKey: args.sessionPublicKey,
355
- orderId: args.orderId,
356
- };
357
- }
358
- else if (args.action === "cancel_all") {
359
- request = {
360
- action: "cancel_all",
361
- ownerWallet: args.ownerWallet,
362
- sessionPublicKey: args.sessionPublicKey,
363
- };
364
- }
365
- else if (args.action === "replace") {
366
- if (args.orderId === undefined
367
- || args.accountSequence === undefined
368
- || args.clientOrderId === undefined
369
- || args.side === undefined
370
- || args.orderType === undefined
371
- || args.limitPriceAtoms === undefined
372
- || args.sizeAtoms === undefined) {
373
- return toolError("invalid_request", "Replace requires orderId, accountSequence, clientOrderId, side, orderType, limitPriceAtoms, and sizeAtoms.", false);
374
- }
375
- request = {
376
- action: "replace",
377
- ownerWallet: args.ownerWallet,
378
- sessionPublicKey: args.sessionPublicKey,
379
- orderId: args.orderId,
380
- accountSequence: args.accountSequence,
381
- clientOrderId: args.clientOrderId,
382
- side: args.side,
383
- orderType: args.orderType,
384
- limitPriceAtoms: args.limitPriceAtoms,
385
- sizeAtoms: args.sizeAtoms,
386
- };
387
- }
388
- else {
389
- if (args.operations === undefined) {
390
- return toolError("invalid_request", "Batch requires operations.", false);
391
- }
392
- const operations = [];
393
- for (const operation of args.operations) {
394
- if (operation.action === "cancel") {
395
- if (operation.orderId === undefined) {
396
- return toolError("invalid_request", "Batch cancel requires orderId.", false);
397
- }
398
- operations.push({ action: "cancel", orderId: operation.orderId });
399
- continue;
400
- }
401
- if (operation.accountSequence === undefined
402
- || operation.clientOrderId === undefined
403
- || operation.side === undefined
404
- || operation.orderType === undefined
405
- || operation.limitPriceAtoms === undefined
406
- || operation.sizeAtoms === undefined
407
- || (operation.action === "replace" && operation.orderId === undefined)) {
408
- return toolError("invalid_request", `Batch ${operation.action} has incomplete fields.`, false);
409
- }
410
- const place = {
411
- accountSequence: operation.accountSequence,
412
- clientOrderId: operation.clientOrderId,
413
- side: operation.side,
414
- orderType: operation.orderType,
415
- limitPriceAtoms: operation.limitPriceAtoms,
416
- sizeAtoms: operation.sizeAtoms,
417
- };
418
- operations.push(operation.action === "replace"
419
- ? { action: "replace", orderId: operation.orderId, ...place }
420
- : { action: "place", ...place });
421
- }
422
- request = {
423
- action: "batch",
424
- ownerWallet: args.ownerWallet,
425
- sessionPublicKey: args.sessionPublicKey,
426
- operations,
427
- };
428
- }
1330
+ const mapped = orderOperationFromArgs(args);
1331
+ if ("content" in mapped)
1332
+ return mapped;
1333
+ const request = mapped;
429
1334
  const response = await platformClient.orders.challenge(args.marketId, request);
430
1335
  return toolResult(response, `Order challenge ${response.challenge_id} binds ${response.order_ids.length} opaque order ID(s); expires at ${response.expires_at_ms}.`);
431
1336
  }));
432
1337
  const orderPrepare = server.registerTool("strata_order_prepare", {
433
1338
  title: "Prepare Strata order control",
434
- description: "Exchange an externally signed order challenge for an immutable partially signed transaction.",
1339
+ description: "Prepare an immutable partially signed order-control transaction. One signature: pass the operation itself (same fields as strata_order_challenge) and sign only the returned transaction with the session key. (A challengeId + authorizationSignature from strata_order_challenge is still accepted.)",
435
1340
  inputSchema: {
436
1341
  marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
437
- challengeId: z.string().regex(/^oc_[0-9a-f]{32}$/),
1342
+ challengeId: z.string().regex(/^oc_[0-9a-f]{32}$/).optional(),
438
1343
  authorizationSignature: z
439
1344
  .string()
440
1345
  .min(64)
441
1346
  .max(88)
442
- .regex(/^[1-9A-HJ-NP-Za-km-z]+$/),
1347
+ .regex(/^[1-9A-HJ-NP-Za-km-z]+$/)
1348
+ .optional(),
1349
+ action: z.enum(["place", "cancel", "cancel_all", "replace", "batch"]).optional(),
1350
+ ownerWallet: z.string().min(32).max(44).optional(),
1351
+ sessionPublicKey: z.string().min(32).max(44).optional(),
1352
+ accountSequence: z.string().regex(/^[0-9]+$/).max(20).optional(),
1353
+ clientOrderId: z.string().min(1).max(64).regex(/^[A-Za-z0-9._-]+$/).optional(),
1354
+ side: z.enum(["buy", "sell"]).optional(),
1355
+ orderType: z.enum(["good_until_cancelled", "post_only"]).optional(),
1356
+ limitPriceAtoms: z.string().regex(/^[1-9][0-9]*$/).max(20).optional(),
1357
+ sizeAtoms: z.string().regex(/^[1-9][0-9]*$/).max(20).optional(),
1358
+ orderId: z.string().regex(/^order_[0-9a-f]{32}$/).optional(),
1359
+ operations: z.array(z.object({
1360
+ action: z.enum(["place", "cancel", "replace"]),
1361
+ accountSequence: z.string().regex(/^[0-9]+$/).max(20).optional(),
1362
+ clientOrderId: z.string().min(1).max(64).regex(/^[A-Za-z0-9._-]+$/).optional(),
1363
+ side: z.enum(["buy", "sell"]).optional(),
1364
+ orderType: z.enum(["good_until_cancelled", "post_only"]).optional(),
1365
+ limitPriceAtoms: z.string().regex(/^[1-9][0-9]*$/).max(20).optional(),
1366
+ sizeAtoms: z.string().regex(/^[1-9][0-9]*$/).max(20).optional(),
1367
+ orderId: z.string().regex(/^order_[0-9a-f]{32}$/).optional(),
1368
+ }).strict()).min(1).max(6).optional(),
443
1369
  },
444
1370
  annotations: {
445
1371
  readOnlyHint: false,
@@ -447,12 +1373,30 @@ export async function createStrataMcpServer(options = {}) {
447
1373
  idempotentHint: false,
448
1374
  openWorldHint: true,
449
1375
  },
450
- }, async ({ marketId, challengeId, authorizationSignature }) => guardedTool(client, "orders.prepare", async () => {
451
- const response = await platformClient.orders.prepare(marketId, {
452
- challengeId,
453
- authorizationSignature,
1376
+ }, async (args) => guardedTool(client, "orders.prepare", async () => {
1377
+ if (args.challengeId !== undefined || args.authorizationSignature !== undefined) {
1378
+ if (args.challengeId === undefined || args.authorizationSignature === undefined) {
1379
+ return toolError("invalid_request", "The two-step path needs both challengeId and authorizationSignature.", false);
1380
+ }
1381
+ const response = await platformClient.orders.prepare(args.marketId, {
1382
+ challengeId: args.challengeId,
1383
+ authorizationSignature: args.authorizationSignature,
1384
+ });
1385
+ return toolResult(response, `Prepared ${response.action} control ${response.order_control_id}; externally verify and sign before ${response.expires_at_ms}.`);
1386
+ }
1387
+ if (args.action === undefined || args.ownerWallet === undefined || args.sessionPublicKey === undefined) {
1388
+ return toolError("invalid_request", "Pass action, ownerWallet, and sessionPublicKey (or a signed challenge).", false);
1389
+ }
1390
+ const mapped = orderOperationFromArgs({
1391
+ ...args,
1392
+ action: args.action,
1393
+ ownerWallet: args.ownerWallet,
1394
+ sessionPublicKey: args.sessionPublicKey,
454
1395
  });
455
- return toolResult(response, `Prepared ${response.action} control ${response.order_control_id}; externally verify and sign before ${response.expires_at_ms}.`);
1396
+ if ("content" in mapped)
1397
+ return mapped;
1398
+ const response = await platformClient.orders.prepare(args.marketId, { operation: mapped });
1399
+ return toolResult(response, `Prepared ${response.action} control ${response.order_control_id} — one signature: verify it, sign the transaction externally with the session key before ${response.expires_at_ms}, then submit.`);
456
1400
  }));
457
1401
  const orderSubmit = server.registerTool("strata_order_submit", {
458
1402
  title: "Submit Strata order control",
@@ -499,9 +1443,11 @@ export async function createStrataMcpServer(options = {}) {
499
1443
  const response = await platformClient.orders.status(marketId, { orderControlId, idempotencyKey });
500
1444
  return toolResult(response, `Order control ${response.order_control_id} is ${response.status}.`);
501
1445
  }));
1446
+ registerAutonomyTools(server, client, platformClient, options.sessionAutonomy, () => typeof Date !== "undefined" ? Date.now() : 0);
502
1447
  const handles = {
503
1448
  markets,
504
1449
  quote,
1450
+ exactOutputQuote,
505
1451
  executionChallenge,
506
1452
  executionPrepare,
507
1453
  executionSubmit,
@@ -533,9 +1479,257 @@ export async function createStrataMcpServer(options = {}) {
533
1479
  },
534
1480
  };
535
1481
  }
1482
+ function registerAutonomyTools(server, client, platformClient, autonomy, nowMs) {
1483
+ // Always present, always read-only: the agent may show the slider and offer
1484
+ // to change it, but nothing it calls can raise its own autonomy.
1485
+ server.registerTool("strata_autonomy", {
1486
+ title: "Strata session autonomy",
1487
+ description: "Read how much this MCP may finish by itself: the autonomy level (ask / limits / instant), "
1488
+ + "any USD ceilings, and how to change it. Read-only — the level is the user's, set out-of-band "
1489
+ + "(the Agents page or the MCP's own env), never by an agent.",
1490
+ inputSchema: {},
1491
+ annotations: {
1492
+ readOnlyHint: true,
1493
+ destructiveHint: false,
1494
+ idempotentHint: true,
1495
+ openWorldHint: false,
1496
+ },
1497
+ }, async () => {
1498
+ const howToChange = {
1499
+ level_env: "STRATA_AUTONOMY = ask | limits | instant",
1500
+ per_trade_env: "STRATA_AUTONOMY_MAX_USD_PER_TRADE",
1501
+ per_day_env: "STRATA_AUTONOMY_MAX_USD_PER_DAY",
1502
+ markets_env: "STRATA_AUTONOMY_MARKETS (comma-separated opaque market IDs)",
1503
+ session_env: "STRATA_SESSION_SECRET_KEY + STRATA_OWNER_WALLET (register the key on the Agents page)",
1504
+ agents_page: "https://stratabook.app/agents",
1505
+ note: "Only the user changes these; an agent can offer but never raise its own level.",
1506
+ };
1507
+ if (!autonomy) {
1508
+ return toolResult({ session_configured: false, level: "ask", how_to_change: howToChange }, "Autonomy: ask (no session key configured). I can prepare trades for you to sign, "
1509
+ + "but I cannot sign any myself. To let me trade unattended, register a Vault session "
1510
+ + "key on the Agents page and set STRATA_SESSION_SECRET_KEY (+ STRATA_OWNER_WALLET), "
1511
+ + "then choose STRATA_AUTONOMY=limits or instant.");
1512
+ }
1513
+ const { config } = autonomy;
1514
+ const spentToday = autonomy.dailyBudget.spentToday(nowMs());
1515
+ const state = {
1516
+ session_configured: true,
1517
+ wallet_address: autonomy.ownerWallet,
1518
+ session_public_key: autonomy.signer.publicKey,
1519
+ level: config.level,
1520
+ max_usd_per_trade: config.maxUsdPerTrade ?? null,
1521
+ max_usd_per_day: config.maxUsdPerDay ?? null,
1522
+ spent_today_usd: Number(spentToday.toFixed(2)),
1523
+ remaining_today_usd: config.maxUsdPerDay === undefined
1524
+ ? null
1525
+ : Number(Math.max(0, config.maxUsdPerDay - spentToday).toFixed(2)),
1526
+ allowed_market_ids: config.allowedMarketIds ?? null,
1527
+ how_to_change: howToChange,
1528
+ };
1529
+ const summary = config.level === "instant"
1530
+ ? "Autonomy: instant — I trade within your on-chain session caps without asking."
1531
+ : config.level === "limits"
1532
+ ? `Autonomy: limits — I trade instantly up to ${config.maxUsdPerTrade !== undefined ? "$" + config.maxUsdPerTrade + "/trade" : "no per-trade cap"}`
1533
+ + `${config.maxUsdPerDay !== undefined ? ", $" + config.maxUsdPerDay + "/day" : ""}; above that I stop and ask.`
1534
+ : "Autonomy: ask — I prepare trades but never sign them; you sign each one.";
1535
+ return toolResult(state, summary);
1536
+ });
1537
+ if (!autonomy)
1538
+ return;
1539
+ const resolver = new MarketMetaResolver(platformClient, async () => (await client.markets()).markets, nowMs);
1540
+ const markFor = async (marketId) => {
1541
+ const mark = await platformClient.marketData.mark(marketId);
1542
+ return {
1543
+ price_atoms_per_base_unit: mark.price_atoms_per_base_unit,
1544
+ quote_decimals: mark.quote_decimals,
1545
+ stale: mark.stale,
1546
+ };
1547
+ };
1548
+ const refuse = (reason, prepared, summary) => toolResult({ executed: false, reason, prepared }, summary);
1549
+ // ── one-shot immediate execution from a fresh Sonar quote ─────────────────
1550
+ server.registerTool("strata_execute_quote", {
1551
+ title: "Execute a Strata quote",
1552
+ description: "Take a fresh Sonar quote and, within the autonomy slider, sign it with the session key and "
1553
+ + "submit it in one call. Under \"ask\" (or over a \"limits\" ceiling) it does not sign — it returns "
1554
+ + "the quote and asks you to sign or raise the slider.",
1555
+ inputSchema: {
1556
+ market: z.string().min(2).max(64).describe("Market label such as SOL/USDC, or its public market ID."),
1557
+ side: z.enum(["buy", "sell"]),
1558
+ amountInAtoms: z.string().regex(/^[1-9][0-9]*$/).max(20),
1559
+ toleranceBps: z.number().int().min(0).max(1_000).optional(),
1560
+ idempotencyKey: z.string().min(1).max(64).regex(/^[A-Za-z0-9._-]+$/).optional(),
1561
+ },
1562
+ annotations: {
1563
+ readOnlyHint: false,
1564
+ destructiveHint: true,
1565
+ idempotentHint: false,
1566
+ openWorldHint: true,
1567
+ },
1568
+ }, async (args) => guardedTool(client, "trade.submit", async () => {
1569
+ const quote = await client.quote({
1570
+ market: args.market,
1571
+ side: args.side,
1572
+ amountInAtoms: args.amountInAtoms,
1573
+ ...(args.toleranceBps === undefined ? {} : { toleranceBps: args.toleranceBps }),
1574
+ });
1575
+ const sonar = (await client.markets()).markets.find((market) => market.market_pda === quote.market_id);
1576
+ const notional = sonar
1577
+ ? quoteNotionalUsd(quote.side, quote.amount_in_atoms, quote.minimum_output_atoms, sonar.quote_decimals)
1578
+ : null;
1579
+ const marketId = sonar ? await resolver.idForLabel(sonar.label) : null;
1580
+ const decision = decideAutonomy(autonomy, marketId ?? "", notional, nowMs());
1581
+ if (!decision.allow) {
1582
+ return refuse(decision.reason, quote, decision.reason);
1583
+ }
1584
+ const receipt = await client.executeQuote({
1585
+ quote,
1586
+ ownerWallet: autonomy.ownerWallet,
1587
+ signer: autonomy.signer,
1588
+ ...(args.idempotencyKey === undefined ? {} : { idempotencyKey: args.idempotencyKey }),
1589
+ });
1590
+ if (notional !== null)
1591
+ autonomy.dailyBudget.record(notional, nowMs());
1592
+ return toolResult({ executed: true, receipt, notional_usd: notional }, `Executed ${quote.side} on ${quote.market_id} as ${receipt.signature}.`);
1593
+ }));
1594
+ // ── one-shot order control (place / cancel / replace / batch) ─────────────
1595
+ server.registerTool("strata_order_execute", {
1596
+ title: "Execute a Strata order control",
1597
+ description: "Place, cancel, replace, or batch orders and, within the autonomy slider, sign with the session "
1598
+ + "key and submit in one call. Owner wallet and session key come from the configured session. "
1599
+ + "Under \"ask\" (or over a \"limits\" ceiling) it prepares the transaction and asks you to sign.",
1600
+ inputSchema: {
1601
+ marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
1602
+ action: z.enum(["place", "cancel", "cancel_all", "replace", "batch"]),
1603
+ clientOrderId: z.string().min(1).max(64).regex(/^[A-Za-z0-9._-]+$/).optional(),
1604
+ side: z.enum(["buy", "sell"]).optional(),
1605
+ orderType: z.enum(["good_until_cancelled", "post_only"]).optional(),
1606
+ limitPriceAtoms: z.string().regex(/^[1-9][0-9]*$/).max(20).optional(),
1607
+ sizeAtoms: z.string().regex(/^[1-9][0-9]*$/).max(20).optional(),
1608
+ orderId: z.string().regex(/^order_[0-9a-f]{32}$/).optional(),
1609
+ operations: z.array(z.object({
1610
+ action: z.enum(["place", "cancel", "replace"]),
1611
+ clientOrderId: z.string().min(1).max(64).regex(/^[A-Za-z0-9._-]+$/).optional(),
1612
+ side: z.enum(["buy", "sell"]).optional(),
1613
+ orderType: z.enum(["good_until_cancelled", "post_only"]).optional(),
1614
+ limitPriceAtoms: z.string().regex(/^[1-9][0-9]*$/).max(20).optional(),
1615
+ sizeAtoms: z.string().regex(/^[1-9][0-9]*$/).max(20).optional(),
1616
+ orderId: z.string().regex(/^order_[0-9a-f]{32}$/).optional(),
1617
+ }).strict()).min(1).max(6).optional(),
1618
+ idempotencyKey: z.string().min(1).max(64).regex(/^[A-Za-z0-9._-]+$/).optional(),
1619
+ },
1620
+ annotations: {
1621
+ readOnlyHint: false,
1622
+ destructiveHint: true,
1623
+ idempotentHint: false,
1624
+ openWorldHint: true,
1625
+ },
1626
+ }, async (args) => guardedTool(client, "orders.submit", async () => {
1627
+ const challenge = orderOperationFromArgs({
1628
+ ...args,
1629
+ ownerWallet: autonomy.ownerWallet,
1630
+ sessionPublicKey: autonomy.signer.publicKey,
1631
+ });
1632
+ if ("content" in challenge)
1633
+ return challenge;
1634
+ // A place/replace risks new base; a cancel reduces it (notional 0).
1635
+ const baseAtoms = (args.action === "place" || args.action === "replace") && args.sizeAtoms !== undefined
1636
+ ? BigInt(args.sizeAtoms)
1637
+ : 0n;
1638
+ const notional = await estimateBaseNotionalUsd(resolver, markFor, args.marketId, baseAtoms);
1639
+ const decision = decideAutonomy(autonomy, args.marketId, notional, nowMs());
1640
+ if (!decision.allow) {
1641
+ const prepared = await platformClient.orders.prepare(args.marketId, {
1642
+ operation: challenge,
1643
+ });
1644
+ return refuse(decision.reason, prepared, decision.reason);
1645
+ }
1646
+ const { sessionPublicKey: _session, ...operation } = challenge;
1647
+ const receipt = await platformClient.orders.execute(args.marketId, {
1648
+ operation: operation,
1649
+ signer: autonomy.signer,
1650
+ ...(args.idempotencyKey === undefined ? {} : { idempotencyKey: args.idempotencyKey }),
1651
+ });
1652
+ if (notional !== null)
1653
+ autonomy.dailyBudget.record(notional, nowMs());
1654
+ return toolResult({ executed: true, receipt, notional_usd: notional }, `Executed ${receipt.action} control ${receipt.order_control_id} as ${receipt.signature}.`);
1655
+ }));
1656
+ // ── one-shot TWAP (schedule / cancel) ─────────────────────────────────────
1657
+ server.registerTool("strata_twap_execute", {
1658
+ title: "Execute a Strata TWAP",
1659
+ description: "Schedule or cancel a TWAP and, within the autonomy slider, sign with the session key and submit "
1660
+ + "in one call. Owner wallet and session key come from the configured session. Under \"ask\" (or over "
1661
+ + "a \"limits\" ceiling) it prepares the transaction and asks you to sign.",
1662
+ inputSchema: {
1663
+ marketId: z.string().regex(/^market_[0-9a-f]{32}$/),
1664
+ action: z.enum(["place", "cancel"]),
1665
+ side: z.enum(["buy", "sell"]).optional(),
1666
+ totalSizeAtoms: z.string().regex(/^[1-9][0-9]*$/).optional(),
1667
+ slicesTotal: z.number().int().min(2).max(120).optional(),
1668
+ maximumToleranceBps: z.number().int().min(1).max(1_000).optional(),
1669
+ intervalSlots: z.number().int().min(25).max(4_500).optional(),
1670
+ limitPriceAtoms: z.string().regex(/^[1-9][0-9]*$/).optional(),
1671
+ twapId: z.string().regex(/^twap_[0-9a-f]{32}$/).optional(),
1672
+ idempotencyKey: z.string().min(1).max(64).regex(/^[A-Za-z0-9._-]+$/).optional(),
1673
+ },
1674
+ annotations: {
1675
+ readOnlyHint: false,
1676
+ destructiveHint: true,
1677
+ idempotentHint: false,
1678
+ openWorldHint: true,
1679
+ },
1680
+ }, async (args) => guardedTool(client, "algos.submit", async () => {
1681
+ let operation;
1682
+ if (args.action === "cancel") {
1683
+ if (args.twapId === undefined)
1684
+ return toolError("invalid_request", "Cancel requires twapId.", false);
1685
+ operation = { action: "cancel", ownerWallet: autonomy.ownerWallet, twapId: args.twapId };
1686
+ }
1687
+ else {
1688
+ if (args.side === undefined
1689
+ || args.totalSizeAtoms === undefined
1690
+ || args.slicesTotal === undefined
1691
+ || args.maximumToleranceBps === undefined
1692
+ || args.intervalSlots === undefined
1693
+ || args.limitPriceAtoms === undefined) {
1694
+ return toolError("invalid_request", "Place requires side, totalSizeAtoms, slicesTotal, maximumToleranceBps, intervalSlots, and limitPriceAtoms.", false);
1695
+ }
1696
+ operation = {
1697
+ action: "place",
1698
+ ownerWallet: autonomy.ownerWallet,
1699
+ side: args.side,
1700
+ totalSizeAtoms: args.totalSizeAtoms,
1701
+ slicesTotal: args.slicesTotal,
1702
+ maximumToleranceBps: args.maximumToleranceBps,
1703
+ intervalSlots: args.intervalSlots,
1704
+ limitPriceAtoms: args.limitPriceAtoms,
1705
+ };
1706
+ }
1707
+ const baseAtoms = args.action === "place" && args.totalSizeAtoms !== undefined ? BigInt(args.totalSizeAtoms) : 0n;
1708
+ const notional = await estimateBaseNotionalUsd(resolver, markFor, args.marketId, baseAtoms);
1709
+ const decision = decideAutonomy(autonomy, args.marketId, notional, nowMs());
1710
+ if (!decision.allow) {
1711
+ const prepared = await platformClient.algos.prepare(args.marketId, {
1712
+ operation: {
1713
+ ...operation,
1714
+ sessionPublicKey: autonomy.signer.publicKey,
1715
+ },
1716
+ });
1717
+ return refuse(decision.reason, prepared, decision.reason);
1718
+ }
1719
+ const receipt = await platformClient.algos.execute(args.marketId, {
1720
+ operation,
1721
+ signer: autonomy.signer,
1722
+ ...(args.idempotencyKey === undefined ? {} : { idempotencyKey: args.idempotencyKey }),
1723
+ });
1724
+ if (notional !== null)
1725
+ autonomy.dailyBudget.record(notional, nowMs());
1726
+ return toolResult({ executed: true, receipt, notional_usd: notional }, `Executed TWAP ${receipt.twap_control_id} as ${receipt.signature}.`);
1727
+ }));
1728
+ }
536
1729
  function applyCapabilityCatalog(handles, catalog) {
537
1730
  setToolEnabled(handles.markets, capabilityAvailable(catalog, "markets.read"));
538
1731
  setToolEnabled(handles.quote, capabilityAvailable(catalog, "quotes.read"));
1732
+ setToolEnabled(handles.exactOutputQuote, capabilityAvailable(catalog, "quotes.read"));
539
1733
  setToolEnabled(handles.executionChallenge, capabilityAvailable(catalog, "trade.prepare"));
540
1734
  setToolEnabled(handles.executionPrepare, capabilityAvailable(catalog, "trade.prepare"));
541
1735
  setToolEnabled(handles.executionSubmit, capabilityAvailable(catalog, "trade.submit"));
@@ -565,6 +1759,16 @@ async function guardedTool(client, capabilityId, operation) {
565
1759
  return toolError("request_failed", safeMessage(error), true);
566
1760
  }
567
1761
  }
1762
+ /**
1763
+ * One line that keeps the two numbers apart: price impact is measured from the
1764
+ * book; the tolerance is the caller's own floor.
1765
+ */
1766
+ function quoteSummary(response) {
1767
+ return (`Sonar ${response.side} quote: ${response.amount_in_consumed_atoms} input atoms for `
1768
+ + `${response.amount_out_atoms} user-net output atoms; price impact ${response.price_impact_pct}% `
1769
+ + `(measured from the book); your tolerance ${response.maximum_tolerance_bps} bps, so the `
1770
+ + `user-net floor is ${response.minimum_output_atoms}; expires at ${response.expires_at_ms}.`);
1771
+ }
568
1772
  function toolResult(value, summary) {
569
1773
  return {
570
1774
  content: [