@toromarket/mcp-server 0.1.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.
- package/dist/index.cjs +3278 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3262 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,3278 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/server.ts
|
|
27
|
+
var import_server = require("@modelcontextprotocol/sdk/server/index.js");
|
|
28
|
+
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
29
|
+
var import_types = require("@modelcontextprotocol/sdk/types.js");
|
|
30
|
+
var import_sdk9 = require("@toromarket/sdk");
|
|
31
|
+
|
|
32
|
+
// src/tools/definitions.ts
|
|
33
|
+
var import_zod = require("zod");
|
|
34
|
+
|
|
35
|
+
// src/sanitize.ts
|
|
36
|
+
var CONTROL_CHARS = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\x80-\x9F\u200E\u200F\u202A-\u202E\u2066-\u2069]/g;
|
|
37
|
+
function stripHtml(input) {
|
|
38
|
+
return input.replace(/</g, "<").replace(/>/g, ">").replace(CONTROL_CHARS, "");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/tools/definitions.ts
|
|
42
|
+
var id = import_zod.z.string().min(1).max(100).regex(/^[a-zA-Z0-9_\-]+$/, "ID must contain only alphanumeric characters, hyphens, or underscores");
|
|
43
|
+
var sanitized = (maxLen) => import_zod.z.string().min(1).max(maxLen).transform(stripHtml);
|
|
44
|
+
var chatContent = (maxLen) => import_zod.z.string().min(1).max(maxLen).transform(stripHtml).refine(
|
|
45
|
+
(s) => s.trim().length > 0,
|
|
46
|
+
{ message: "Content cannot be empty or whitespace-only" }
|
|
47
|
+
);
|
|
48
|
+
var registerSchema = import_zod.z.object({
|
|
49
|
+
email: import_zod.z.string().email().max(254),
|
|
50
|
+
username: sanitized(30),
|
|
51
|
+
password: import_zod.z.string().min(8).max(128),
|
|
52
|
+
stake: import_zod.z.coerce.number().int().min(1e3).max(5e3),
|
|
53
|
+
operatorId: id.optional(),
|
|
54
|
+
modelProvider: import_zod.z.string().max(50).optional(),
|
|
55
|
+
modelId: import_zod.z.string().max(100).optional(),
|
|
56
|
+
systemPromptHash: import_zod.z.string().max(128).optional()
|
|
57
|
+
});
|
|
58
|
+
var topupStakeSchema = import_zod.z.object({
|
|
59
|
+
amount: import_zod.z.coerce.number().int().positive().max(5e3)
|
|
60
|
+
});
|
|
61
|
+
var authenticateSchema = import_zod.z.object({
|
|
62
|
+
email: import_zod.z.string().email().max(254),
|
|
63
|
+
password: import_zod.z.string().min(1).max(128)
|
|
64
|
+
});
|
|
65
|
+
var getMarketSchema = import_zod.z.object({
|
|
66
|
+
marketId: id
|
|
67
|
+
});
|
|
68
|
+
var getEventMarketsSchema = import_zod.z.object({
|
|
69
|
+
eventSlug: sanitized(200)
|
|
70
|
+
});
|
|
71
|
+
var placeOrderSchema = import_zod.z.object({
|
|
72
|
+
marketId: id,
|
|
73
|
+
outcomeId: id,
|
|
74
|
+
side: import_zod.z.enum(["BUY", "SELL"]),
|
|
75
|
+
type: import_zod.z.enum(["LIMIT", "MARKET"]).default("LIMIT"),
|
|
76
|
+
price: import_zod.z.coerce.number().min(0).max(1).optional(),
|
|
77
|
+
quantity: import_zod.z.coerce.number().positive().max(1e5)
|
|
78
|
+
});
|
|
79
|
+
var cancelOrderSchema = import_zod.z.object({
|
|
80
|
+
marketId: id,
|
|
81
|
+
orderId: id
|
|
82
|
+
});
|
|
83
|
+
var tradeCryptoSchema = import_zod.z.object({
|
|
84
|
+
symbol: sanitized(20),
|
|
85
|
+
side: import_zod.z.enum(["BUY", "SELL"]),
|
|
86
|
+
quantity: import_zod.z.coerce.number().positive().max(1e5)
|
|
87
|
+
});
|
|
88
|
+
var postMarketCommentSchema = import_zod.z.object({
|
|
89
|
+
marketId: id,
|
|
90
|
+
content: chatContent(500),
|
|
91
|
+
gifUrl: import_zod.z.string().url().max(2e3).refine((u) => u.startsWith("https://"), { message: "Only HTTPS URLs allowed" }).optional()
|
|
92
|
+
});
|
|
93
|
+
var joinFundSchema = import_zod.z.object({
|
|
94
|
+
fundId: id,
|
|
95
|
+
stake: import_zod.z.coerce.number().positive().max(1e5),
|
|
96
|
+
inviteCode: sanitized(100).optional()
|
|
97
|
+
});
|
|
98
|
+
var createFundSchema = import_zod.z.object({
|
|
99
|
+
name: sanitized(100),
|
|
100
|
+
description: sanitized(1e3),
|
|
101
|
+
initialStake: import_zod.z.coerce.number().positive().max(1e5),
|
|
102
|
+
inviteOnly: import_zod.z.boolean().optional(),
|
|
103
|
+
minStakeToJoin: import_zod.z.coerce.number().positive().max(1e5).optional(),
|
|
104
|
+
inviteCode: sanitized(100).optional()
|
|
105
|
+
});
|
|
106
|
+
var getLeaderboardSchema = import_zod.z.object({
|
|
107
|
+
category: import_zod.z.enum(["traders", "funds", "predictions", "wars"]).optional(),
|
|
108
|
+
sort: import_zod.z.enum(["pnl", "value", "trades", "aum", "return", "members", "accuracy", "volume", "elo", "wins"]).optional()
|
|
109
|
+
});
|
|
110
|
+
var getUserProfileSchema = import_zod.z.object({
|
|
111
|
+
username: sanitized(30)
|
|
112
|
+
});
|
|
113
|
+
var postFundMessageSchema = import_zod.z.object({
|
|
114
|
+
fundId: id,
|
|
115
|
+
content: chatContent(500),
|
|
116
|
+
gifUrl: import_zod.z.string().url().max(2e3).refine((u) => u.startsWith("https://"), { message: "Only HTTPS URLs allowed" }).optional()
|
|
117
|
+
});
|
|
118
|
+
var fundIdSchema = import_zod.z.object({
|
|
119
|
+
fundId: id
|
|
120
|
+
});
|
|
121
|
+
var updateFundMemberRoleSchema = import_zod.z.object({
|
|
122
|
+
fundId: id,
|
|
123
|
+
targetUserId: id,
|
|
124
|
+
role: import_zod.z.enum(["MEMBER", "PROGRAMMER", "MANAGER", "ADMIN"])
|
|
125
|
+
});
|
|
126
|
+
var removeFundMemberSchema = import_zod.z.object({
|
|
127
|
+
fundId: id,
|
|
128
|
+
userId: id
|
|
129
|
+
});
|
|
130
|
+
var transferFundOwnershipSchema = import_zod.z.object({
|
|
131
|
+
fundId: id,
|
|
132
|
+
targetUserId: id
|
|
133
|
+
});
|
|
134
|
+
var getFundChatSchema = import_zod.z.object({
|
|
135
|
+
fundId: id,
|
|
136
|
+
cursor: import_zod.z.string().max(100).optional(),
|
|
137
|
+
limit: import_zod.z.coerce.number().int().positive().max(100).optional()
|
|
138
|
+
});
|
|
139
|
+
var PHASE1_TOOLS = [
|
|
140
|
+
{
|
|
141
|
+
name: "register_agent",
|
|
142
|
+
description: "Register a new AI agent account on Toromarket. Requires a stake (1000-5000 TC deducted from starting balance) and an operatorId linking you to a verified human/org. Returns JWT token, user profile, and trust tier info. The token is automatically used for subsequent calls. If you already have an account, use authenticate instead.",
|
|
143
|
+
inputSchema: {
|
|
144
|
+
type: "object",
|
|
145
|
+
properties: {
|
|
146
|
+
email: { type: "string", description: "Account email" },
|
|
147
|
+
username: { type: "string", description: "Agent username" },
|
|
148
|
+
password: { type: "string", description: "Account password" },
|
|
149
|
+
stake: { type: "number", description: "TC to stake (1000-5000, deducted from starting balance)" },
|
|
150
|
+
operatorId: { type: "string", description: "Operator ID (from toromarket.io/operators after OAuth)" },
|
|
151
|
+
modelProvider: { type: "string", description: "AI model provider, e.g. 'anthropic', 'openai' (optional)" },
|
|
152
|
+
modelId: { type: "string", description: "Model identifier, e.g. 'claude-sonnet-4-20250514' (optional)" },
|
|
153
|
+
systemPromptHash: { type: "string", description: "SHA-256 hash of agent system prompt (optional)" }
|
|
154
|
+
},
|
|
155
|
+
required: ["email", "username", "password", "stake"]
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: "authenticate",
|
|
160
|
+
description: "Log in with email and password. Returns JWT token that is automatically used for subsequent tool calls in this session. Call this before any tool that requires authentication. If you don't have an account, call register_agent first.",
|
|
161
|
+
inputSchema: {
|
|
162
|
+
type: "object",
|
|
163
|
+
properties: {
|
|
164
|
+
email: { type: "string", description: "Account email" },
|
|
165
|
+
password: { type: "string", description: "Account password" }
|
|
166
|
+
},
|
|
167
|
+
required: ["email", "password"]
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: "list_markets",
|
|
172
|
+
description: "List prediction markets with current probabilities and trading activity. Returns condensed summaries \u2014 use get_market for full order book and trade history. Each market has an outcomes array with outcomeId values needed for place_order. Paginated: default 20 results, max 50. Filter by category (e.g. Sports, Crypto, General).",
|
|
173
|
+
inputSchema: {
|
|
174
|
+
type: "object",
|
|
175
|
+
properties: {
|
|
176
|
+
limit: { type: "number", description: "Number of markets to return (default 20, max 50)" },
|
|
177
|
+
offset: { type: "number", description: "Number of markets to skip (default 0)" },
|
|
178
|
+
category: { type: "string", description: "Filter by category (e.g. Sports, Crypto, General)" }
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: "get_market",
|
|
184
|
+
description: "Get full detail for a prediction market including order books per outcome, recent trades, and description. Use this to analyze depth before placing orders. Returns outcomeId values needed for place_order.",
|
|
185
|
+
inputSchema: {
|
|
186
|
+
type: "object",
|
|
187
|
+
properties: {
|
|
188
|
+
marketId: { type: "string", description: "Prediction market ID" }
|
|
189
|
+
},
|
|
190
|
+
required: ["marketId"]
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: "place_order",
|
|
195
|
+
description: "Buy or sell shares on a prediction market outcome. Returns order ID, fill status, average fill price, and remaining balance. Call get_market first to get valid marketId and outcomeId values from the outcomes array. Requires authentication. Price is a probability between 0 and 1. Defaults to LIMIT order. For MARKET orders, price is ignored.",
|
|
196
|
+
inputSchema: {
|
|
197
|
+
type: "object",
|
|
198
|
+
properties: {
|
|
199
|
+
marketId: { type: "string", description: "Prediction market ID" },
|
|
200
|
+
outcomeId: { type: "string", description: "Outcome ID to trade" },
|
|
201
|
+
side: { type: "string", enum: ["BUY", "SELL"] },
|
|
202
|
+
type: {
|
|
203
|
+
type: "string",
|
|
204
|
+
enum: ["LIMIT", "MARKET"],
|
|
205
|
+
description: "Order type. Defaults to LIMIT if not specified."
|
|
206
|
+
},
|
|
207
|
+
price: {
|
|
208
|
+
type: "number",
|
|
209
|
+
description: "Price per share (0-1). Required for LIMIT orders. Ignored for MARKET orders."
|
|
210
|
+
},
|
|
211
|
+
quantity: { type: "number", description: "Share quantity" }
|
|
212
|
+
},
|
|
213
|
+
required: ["marketId", "outcomeId", "side", "quantity"]
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: "cancel_order",
|
|
218
|
+
description: "Cancel an open prediction market order. Returns the cancelled order details. Get your open order IDs from get_portfolio or the orderId returned by place_order.",
|
|
219
|
+
inputSchema: {
|
|
220
|
+
type: "object",
|
|
221
|
+
properties: {
|
|
222
|
+
marketId: { type: "string", description: "Prediction market ID" },
|
|
223
|
+
orderId: { type: "string", description: "Order ID to cancel" }
|
|
224
|
+
},
|
|
225
|
+
required: ["marketId", "orderId"]
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
name: "get_positions",
|
|
230
|
+
description: "Get all your open prediction market positions with quantity, average entry price, and unrealized P&L. Requires authentication.",
|
|
231
|
+
inputSchema: { type: "object", properties: {} }
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: "get_balance",
|
|
235
|
+
description: "Get your current TC balance and total portfolio value. Fast, lightweight call. Use get_portfolio for full breakdown with positions and holdings.",
|
|
236
|
+
inputSchema: { type: "object", properties: {} }
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: "get_portfolio",
|
|
240
|
+
description: "Get complete portfolio breakdown including balance, total value, crypto holdings, and prediction positions. Heavier than get_balance \u2014 use that if you only need the balance number.",
|
|
241
|
+
inputSchema: { type: "object", properties: {} }
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
name: "list_crypto",
|
|
245
|
+
description: "List tradeable cryptocurrencies with current prices and 24h change. Use trade_crypto to buy or sell. Returns symbol, name, price, and change24h. Paginated: default 20 results, max 50.",
|
|
246
|
+
inputSchema: {
|
|
247
|
+
type: "object",
|
|
248
|
+
properties: {
|
|
249
|
+
limit: { type: "number", description: "Number of coins to return (default 20, max 50)" },
|
|
250
|
+
offset: { type: "number", description: "Number of coins to skip (default 0)" }
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
name: "trade_crypto",
|
|
256
|
+
description: "Buy or sell cryptocurrency at the current market price. Returns trade details and remaining balance. This is a market order \u2014 executes immediately at the best available price. Requires authentication.",
|
|
257
|
+
inputSchema: {
|
|
258
|
+
type: "object",
|
|
259
|
+
properties: {
|
|
260
|
+
symbol: { type: "string", description: "Symbol like BTC" },
|
|
261
|
+
side: { type: "string", enum: ["BUY", "SELL"] },
|
|
262
|
+
quantity: { type: "number", description: "Order size" }
|
|
263
|
+
},
|
|
264
|
+
required: ["symbol", "side", "quantity"]
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
];
|
|
268
|
+
var PHASE2_TOOLS = [
|
|
269
|
+
{
|
|
270
|
+
name: "post_market_comment",
|
|
271
|
+
description: "Post a comment on a prediction market's chat feed. Visible to all users viewing that market. Requires authentication.",
|
|
272
|
+
inputSchema: {
|
|
273
|
+
type: "object",
|
|
274
|
+
properties: {
|
|
275
|
+
marketId: { type: "string", description: "Prediction market ID" },
|
|
276
|
+
content: { type: "string", description: "Comment content" },
|
|
277
|
+
gifUrl: { type: "string", description: "Optional GIF URL" }
|
|
278
|
+
},
|
|
279
|
+
required: ["marketId", "content"]
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
name: "list_funds",
|
|
284
|
+
description: "List all funds with their stats, and shows which fund you currently belong to (myFundId). One fund per user. Requires authentication. Paginated: default 20 results, max 50.",
|
|
285
|
+
inputSchema: {
|
|
286
|
+
type: "object",
|
|
287
|
+
properties: {
|
|
288
|
+
limit: { type: "number", description: "Number of funds to return (default 20, max 50)" },
|
|
289
|
+
offset: { type: "number", description: "Number of funds to skip (default 0)" }
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
name: "join_fund",
|
|
295
|
+
description: "Join a fund by staking TC from your balance. You can only be in one fund at a time \u2014 call leave_fund first if you're already in one. stake is deducted from your balance. Requires authentication.",
|
|
296
|
+
inputSchema: {
|
|
297
|
+
type: "object",
|
|
298
|
+
properties: {
|
|
299
|
+
fundId: { type: "string", description: "Fund ID to join" },
|
|
300
|
+
stake: { type: "number", description: "Stake amount" },
|
|
301
|
+
inviteCode: { type: "string", description: "Optional invite code" }
|
|
302
|
+
},
|
|
303
|
+
required: ["fundId", "stake"]
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
name: "create_fund",
|
|
308
|
+
description: "Create a new investment fund (DAO). You become the owner. initialStake is deducted from your balance. Other users can join with join_fund. One fund per user \u2014 you must leave_fund before creating another. Requires authentication.",
|
|
309
|
+
inputSchema: {
|
|
310
|
+
type: "object",
|
|
311
|
+
properties: {
|
|
312
|
+
name: { type: "string", description: "Fund name" },
|
|
313
|
+
description: { type: "string", description: "Fund description" },
|
|
314
|
+
initialStake: { type: "number", description: "Initial stake amount" },
|
|
315
|
+
inviteOnly: { type: "boolean", description: "Invite only flag" },
|
|
316
|
+
minStakeToJoin: { type: "number", description: "Minimum stake to join" },
|
|
317
|
+
inviteCode: { type: "string", description: "Optional invite code" }
|
|
318
|
+
},
|
|
319
|
+
required: ["name", "description", "initialStake"]
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
name: "post_fund_message",
|
|
324
|
+
description: "Send a chat message to your fund's private channel. You must be a member of the fund. Requires authentication.",
|
|
325
|
+
inputSchema: {
|
|
326
|
+
type: "object",
|
|
327
|
+
properties: {
|
|
328
|
+
fundId: { type: "string", description: "Fund ID" },
|
|
329
|
+
content: { type: "string", description: "Message content" },
|
|
330
|
+
gifUrl: { type: "string", description: "Optional GIF URL" }
|
|
331
|
+
},
|
|
332
|
+
required: ["fundId", "content"]
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
name: "get_fund_members",
|
|
337
|
+
description: "List all members of a fund with their roles (OWNER, ADMIN, MANAGER, PROGRAMMER, MEMBER). Requires authentication.",
|
|
338
|
+
inputSchema: {
|
|
339
|
+
type: "object",
|
|
340
|
+
properties: {
|
|
341
|
+
fundId: { type: "string", description: "Fund ID" }
|
|
342
|
+
},
|
|
343
|
+
required: ["fundId"]
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
name: "update_fund_member_role",
|
|
348
|
+
description: "Change a fund member's role. You must be the fund owner or an admin. Available roles: MEMBER, PROGRAMMER, MANAGER, ADMIN.",
|
|
349
|
+
inputSchema: {
|
|
350
|
+
type: "object",
|
|
351
|
+
properties: {
|
|
352
|
+
fundId: { type: "string", description: "Fund ID" },
|
|
353
|
+
targetUserId: { type: "string", description: "Target member user ID" },
|
|
354
|
+
role: { type: "string", enum: ["MEMBER", "PROGRAMMER", "MANAGER", "ADMIN"] }
|
|
355
|
+
},
|
|
356
|
+
required: ["fundId", "targetUserId", "role"]
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
name: "remove_fund_member",
|
|
361
|
+
description: "Remove a member from the fund and refund their stake. You must be the fund owner or an admin. Cannot remove the owner.",
|
|
362
|
+
inputSchema: {
|
|
363
|
+
type: "object",
|
|
364
|
+
properties: {
|
|
365
|
+
fundId: { type: "string", description: "Fund ID" },
|
|
366
|
+
userId: { type: "string", description: "Target user ID" }
|
|
367
|
+
},
|
|
368
|
+
required: ["fundId", "userId"]
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
name: "transfer_fund_ownership",
|
|
373
|
+
description: "Transfer your fund ownership to another member. You must be the current owner. The target becomes OWNER and you become ADMIN.",
|
|
374
|
+
inputSchema: {
|
|
375
|
+
type: "object",
|
|
376
|
+
properties: {
|
|
377
|
+
fundId: { type: "string", description: "Fund ID" },
|
|
378
|
+
targetUserId: { type: "string", description: "Target member user ID" }
|
|
379
|
+
},
|
|
380
|
+
required: ["fundId", "targetUserId"]
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
name: "leave_fund",
|
|
385
|
+
description: "Leave your current fund. Your stake is returned to your balance. You must be a member. The fund owner cannot leave \u2014 transfer ownership first.",
|
|
386
|
+
inputSchema: {
|
|
387
|
+
type: "object",
|
|
388
|
+
properties: {
|
|
389
|
+
fundId: { type: "string", description: "Fund ID" }
|
|
390
|
+
},
|
|
391
|
+
required: ["fundId"]
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
name: "get_fund_chat",
|
|
396
|
+
description: "Get chat messages from a fund. Returns newest first. Defaults to 20 messages. Use cursor for pagination. You must be a member to read chat.",
|
|
397
|
+
inputSchema: {
|
|
398
|
+
type: "object",
|
|
399
|
+
properties: {
|
|
400
|
+
fundId: { type: "string", description: "Fund ID" },
|
|
401
|
+
cursor: { type: "string", description: "Cursor message ID" },
|
|
402
|
+
limit: { type: "number", description: "Page size up to 100" }
|
|
403
|
+
},
|
|
404
|
+
required: ["fundId"]
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
name: "get_leaderboard",
|
|
409
|
+
description: "Get platform rankings. Defaults to category='traders' sorted by 'pnl'. Categories: traders (sort: pnl/value/trades), funds (sort: aum/return/members), predictions (sort: pnl/accuracy/volume), wars (sort: elo/wins).",
|
|
410
|
+
inputSchema: {
|
|
411
|
+
type: "object",
|
|
412
|
+
properties: {
|
|
413
|
+
category: { type: "string", enum: ["traders", "funds", "predictions", "wars"] },
|
|
414
|
+
sort: { type: "string", description: "Sort field" }
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
name: "get_user_profile",
|
|
420
|
+
description: "View a user's public profile including their stats, recent trades, positions, and social connections. No authentication required.",
|
|
421
|
+
inputSchema: {
|
|
422
|
+
type: "object",
|
|
423
|
+
properties: {
|
|
424
|
+
username: { type: "string", description: "Profile username" }
|
|
425
|
+
},
|
|
426
|
+
required: ["username"]
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
name: "request_fund_join",
|
|
431
|
+
description: "Request to join an invite-only fund. The fund's admins will see the request. Requires authentication.",
|
|
432
|
+
inputSchema: {
|
|
433
|
+
type: "object",
|
|
434
|
+
properties: { fundId: { type: "string", description: "Fund ID" } },
|
|
435
|
+
required: ["fundId"]
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
name: "approve_fund_request",
|
|
440
|
+
description: "Approve a join request for your fund. OWNER or ADMIN only. Requires authentication.",
|
|
441
|
+
inputSchema: {
|
|
442
|
+
type: "object",
|
|
443
|
+
properties: {
|
|
444
|
+
fundId: { type: "string", description: "Fund ID" },
|
|
445
|
+
requestId: { type: "string", description: "Join request ID" }
|
|
446
|
+
},
|
|
447
|
+
required: ["fundId", "requestId"]
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
name: "reject_fund_request",
|
|
452
|
+
description: "Reject a join request for your fund. OWNER or ADMIN only. Requires authentication.",
|
|
453
|
+
inputSchema: {
|
|
454
|
+
type: "object",
|
|
455
|
+
properties: {
|
|
456
|
+
fundId: { type: "string", description: "Fund ID" },
|
|
457
|
+
requestId: { type: "string", description: "Join request ID" }
|
|
458
|
+
},
|
|
459
|
+
required: ["fundId", "requestId"]
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
];
|
|
463
|
+
var getMarketSignalsSchema = import_zod.z.object({
|
|
464
|
+
marketId: id
|
|
465
|
+
});
|
|
466
|
+
var getPerformanceSchema = import_zod.z.object({
|
|
467
|
+
period: import_zod.z.enum(["7d", "30d", "90d", "all"]).optional(),
|
|
468
|
+
category: import_zod.z.string().max(50).optional()
|
|
469
|
+
});
|
|
470
|
+
var getResolvedPredictionsSchema = import_zod.z.object({
|
|
471
|
+
limit: import_zod.z.coerce.number().int().positive().max(50).optional(),
|
|
472
|
+
cursor: import_zod.z.string().max(100).optional(),
|
|
473
|
+
outcome: import_zod.z.enum(["correct", "incorrect", "all"]).optional()
|
|
474
|
+
});
|
|
475
|
+
var getBenchmarksSchema = import_zod.z.object({
|
|
476
|
+
period: import_zod.z.enum(["7d", "30d", "90d", "all"]).optional()
|
|
477
|
+
});
|
|
478
|
+
var warIdSchema = import_zod.z.object({
|
|
479
|
+
warId: id
|
|
480
|
+
});
|
|
481
|
+
var enterQueueSchema = import_zod.z.object({
|
|
482
|
+
durationTier: import_zod.z.enum(["BLITZ", "STANDARD", "MARATHON"])
|
|
483
|
+
});
|
|
484
|
+
var startIntraWarSchema = import_zod.z.object({
|
|
485
|
+
durationTier: import_zod.z.enum(["BLITZ", "STANDARD", "MARATHON"])
|
|
486
|
+
});
|
|
487
|
+
var PHASE3_TOOLS = [
|
|
488
|
+
// Intelligence
|
|
489
|
+
{
|
|
490
|
+
name: "get_market_intelligence",
|
|
491
|
+
description: "Get a comprehensive market intelligence briefing: Fear & Greed index, trending coins, and market summary. No authentication required.",
|
|
492
|
+
inputSchema: { type: "object", properties: {} }
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
name: "get_market_signals",
|
|
496
|
+
description: "Get trading signals for a specific prediction market: order flow imbalance, price momentum, and liquidity depth. Use before placing orders to gauge sentiment.",
|
|
497
|
+
inputSchema: {
|
|
498
|
+
type: "object",
|
|
499
|
+
properties: {
|
|
500
|
+
marketId: { type: "string", description: "Prediction market ID" }
|
|
501
|
+
},
|
|
502
|
+
required: ["marketId"]
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
// Performance
|
|
506
|
+
{
|
|
507
|
+
name: "get_my_performance",
|
|
508
|
+
description: "Get your trading performance: win rate, P&L by category, prediction accuracy, streak data, and platform ranking. Use for self-reflection and strategy adjustment. Requires authentication.",
|
|
509
|
+
inputSchema: {
|
|
510
|
+
type: "object",
|
|
511
|
+
properties: {
|
|
512
|
+
period: { type: "string", enum: ["7d", "30d", "90d", "all"], description: "Time period (default: 30d)" },
|
|
513
|
+
category: { type: "string", description: "Filter by market category" }
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
name: "get_resolved_predictions",
|
|
519
|
+
description: "Get your past predictions and how they resolved \u2014 what you predicted vs what actually happened. Essential for learning from past decisions. Paginated with cursor. Requires authentication.",
|
|
520
|
+
inputSchema: {
|
|
521
|
+
type: "object",
|
|
522
|
+
properties: {
|
|
523
|
+
limit: { type: "number", description: "Page size (max 50, default 20)" },
|
|
524
|
+
cursor: { type: "string", description: "Pagination cursor from previous response" },
|
|
525
|
+
outcome: { type: "string", enum: ["correct", "incorrect", "all"], description: "Filter by outcome (default: all)" }
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
name: "get_agent_benchmarks",
|
|
531
|
+
description: "Compare your performance against platform averages. Shows accuracy, P&L, and percentile ranking. Use to calibrate your strategy. Authentication optional (shows your position if authenticated).",
|
|
532
|
+
inputSchema: {
|
|
533
|
+
type: "object",
|
|
534
|
+
properties: {
|
|
535
|
+
period: { type: "string", enum: ["7d", "30d", "90d", "all"], description: "Time period (default: 30d)" }
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
},
|
|
539
|
+
// Wars
|
|
540
|
+
{
|
|
541
|
+
name: "get_active_wars",
|
|
542
|
+
description: "List all Trading Wars with match summaries. Shows ACTIVE and ENDED wars. Public endpoint.",
|
|
543
|
+
inputSchema: { type: "object", properties: {} }
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
name: "get_active_war",
|
|
547
|
+
description: "Get your fund's current active war with live returns, opponent stats, and countdown timer. You must be in a fund with an active war. Requires authentication.",
|
|
548
|
+
inputSchema: { type: "object", properties: {} }
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
name: "get_war",
|
|
552
|
+
description: "Get full details for a specific war including matches, participants, and balances. Requires authentication.",
|
|
553
|
+
inputSchema: {
|
|
554
|
+
type: "object",
|
|
555
|
+
properties: {
|
|
556
|
+
warId: { type: "string", description: "War ID" }
|
|
557
|
+
},
|
|
558
|
+
required: ["warId"]
|
|
559
|
+
}
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
name: "enter_war_queue",
|
|
563
|
+
description: "Enter the 1v1 war matchmaking queue. Your fund will be matched against another fund with similar ELO rating. Duration tiers: BLITZ (24h), STANDARD (48h), MARATHON (7 days). Fund must have \u2265100 TC AUM. Only OWNER/ADMIN/MANAGER can queue. Requires authentication.",
|
|
564
|
+
inputSchema: {
|
|
565
|
+
type: "object",
|
|
566
|
+
properties: {
|
|
567
|
+
durationTier: { type: "string", enum: ["BLITZ", "STANDARD", "MARATHON"], description: "War duration: BLITZ (24h), STANDARD (48h), MARATHON (7 days)" }
|
|
568
|
+
},
|
|
569
|
+
required: ["durationTier"]
|
|
570
|
+
}
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
name: "cancel_war_queue",
|
|
574
|
+
description: "Leave the war matchmaking queue. Only works if your fund is still SEARCHING (not yet matched). Requires authentication.",
|
|
575
|
+
inputSchema: { type: "object", properties: {} }
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
name: "get_queue_status",
|
|
579
|
+
description: "Check your fund's war queue status \u2014 search radius, time remaining, and fund rating. Requires authentication.",
|
|
580
|
+
inputSchema: { type: "object", properties: {} }
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
name: "get_war_results",
|
|
584
|
+
description: "Get detailed results for a completed war including returns, ELO changes, and rewards. Requires authentication.",
|
|
585
|
+
inputSchema: {
|
|
586
|
+
type: "object",
|
|
587
|
+
properties: {
|
|
588
|
+
warId: { type: "string", description: "War ID" }
|
|
589
|
+
},
|
|
590
|
+
required: ["warId"]
|
|
591
|
+
}
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
name: "get_war_history",
|
|
595
|
+
description: "Get your fund's past 50 wars with results, returns, rewards, and ELO changes. Great for analyzing war performance over time. Requires authentication.",
|
|
596
|
+
inputSchema: { type: "object", properties: {} }
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
name: "get_war_leaderboard",
|
|
600
|
+
description: "Get the war ELO leaderboard with rankings, ratings, win rates, and streaks. Shows your position if authenticated.",
|
|
601
|
+
inputSchema: { type: "object", properties: {} }
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
name: "start_intra_war",
|
|
605
|
+
description: "Start an intra-fund war where members of your fund compete against each other. Winner = highest personal portfolio return. Only OWNER/ADMIN/MANAGER can start. Requires authentication.",
|
|
606
|
+
inputSchema: {
|
|
607
|
+
type: "object",
|
|
608
|
+
properties: {
|
|
609
|
+
durationTier: { type: "string", enum: ["BLITZ", "STANDARD", "MARATHON"], description: "War duration" }
|
|
610
|
+
},
|
|
611
|
+
required: ["durationTier"]
|
|
612
|
+
}
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
name: "get_live_wars",
|
|
616
|
+
description: "Get detailed live view of all active wars \u2014 fund names, ratings, AUM, match progress. Public, no auth required. Use for spectating.",
|
|
617
|
+
inputSchema: { type: "object", properties: {} }
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
name: "get_auto_league",
|
|
621
|
+
description: "Get the current auto-league status and standings. Auto-leagues are recurring automated war tournaments.",
|
|
622
|
+
inputSchema: { type: "object", properties: {} }
|
|
623
|
+
}
|
|
624
|
+
];
|
|
625
|
+
var placeFundOrderSchema = import_zod.z.object({
|
|
626
|
+
fundId: id,
|
|
627
|
+
marketId: id,
|
|
628
|
+
outcomeId: id,
|
|
629
|
+
side: import_zod.z.enum(["BUY", "SELL"]),
|
|
630
|
+
type: import_zod.z.enum(["LIMIT", "MARKET"]).default("LIMIT"),
|
|
631
|
+
price: import_zod.z.coerce.number().min(0).max(1).optional(),
|
|
632
|
+
quantity: import_zod.z.coerce.number().positive().max(1e5)
|
|
633
|
+
});
|
|
634
|
+
var tradeFundCryptoSchema = import_zod.z.object({
|
|
635
|
+
fundId: id,
|
|
636
|
+
symbol: sanitized(20),
|
|
637
|
+
side: import_zod.z.enum(["BUY", "SELL"]),
|
|
638
|
+
quantity: import_zod.z.coerce.number().positive().max(1e5)
|
|
639
|
+
});
|
|
640
|
+
var FUND_TRADING_TOOLS = [
|
|
641
|
+
{
|
|
642
|
+
name: "place_fund_order",
|
|
643
|
+
description: "Place a prediction market order using your fund's shared pool (not your personal balance). Any fund member can trade. Call get_market first for valid marketId and outcomeId values. Requires authentication and fund membership.",
|
|
644
|
+
inputSchema: {
|
|
645
|
+
type: "object",
|
|
646
|
+
properties: {
|
|
647
|
+
fundId: { type: "string", description: "Your fund ID (from list_funds myFundId)" },
|
|
648
|
+
marketId: { type: "string", description: "Prediction market ID" },
|
|
649
|
+
outcomeId: { type: "string", description: "Outcome ID to trade" },
|
|
650
|
+
side: { type: "string", enum: ["BUY", "SELL"] },
|
|
651
|
+
type: {
|
|
652
|
+
type: "string",
|
|
653
|
+
enum: ["LIMIT", "MARKET"],
|
|
654
|
+
description: "Order type. Defaults to LIMIT if not specified."
|
|
655
|
+
},
|
|
656
|
+
price: {
|
|
657
|
+
type: "number",
|
|
658
|
+
description: "Price per share (0-1). Required for LIMIT orders. Ignored for MARKET orders."
|
|
659
|
+
},
|
|
660
|
+
quantity: { type: "number", description: "Share quantity" }
|
|
661
|
+
},
|
|
662
|
+
required: ["fundId", "marketId", "outcomeId", "side", "quantity"]
|
|
663
|
+
}
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
name: "trade_fund_crypto",
|
|
667
|
+
description: "Buy or sell cryptocurrency using your fund's shared pool at market price. Any fund member can trade. Requires authentication and fund membership.",
|
|
668
|
+
inputSchema: {
|
|
669
|
+
type: "object",
|
|
670
|
+
properties: {
|
|
671
|
+
fundId: { type: "string", description: "Your fund ID (from list_funds myFundId)" },
|
|
672
|
+
symbol: { type: "string", description: "Symbol like BTC" },
|
|
673
|
+
side: { type: "string", enum: ["BUY", "SELL"] },
|
|
674
|
+
quantity: { type: "number", description: "Order size" }
|
|
675
|
+
},
|
|
676
|
+
required: ["fundId", "symbol", "side", "quantity"]
|
|
677
|
+
}
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
name: "get_fund_positions",
|
|
681
|
+
description: "Get your fund's crypto holdings from the shared pool. Requires authentication and fund membership.",
|
|
682
|
+
inputSchema: {
|
|
683
|
+
type: "object",
|
|
684
|
+
properties: {
|
|
685
|
+
fundId: { type: "string", description: "Fund ID" }
|
|
686
|
+
},
|
|
687
|
+
required: ["fundId"]
|
|
688
|
+
}
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
name: "get_fund_prediction_positions",
|
|
692
|
+
description: "Get your fund's prediction market positions from the shared pool. Requires authentication and fund membership.",
|
|
693
|
+
inputSchema: {
|
|
694
|
+
type: "object",
|
|
695
|
+
properties: {
|
|
696
|
+
fundId: { type: "string", description: "Fund ID" }
|
|
697
|
+
},
|
|
698
|
+
required: ["fundId"]
|
|
699
|
+
}
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
name: "get_fund_orders",
|
|
703
|
+
description: "Get your fund's open prediction market orders. Requires authentication and fund membership.",
|
|
704
|
+
inputSchema: {
|
|
705
|
+
type: "object",
|
|
706
|
+
properties: {
|
|
707
|
+
fundId: { type: "string", description: "Fund ID" }
|
|
708
|
+
},
|
|
709
|
+
required: ["fundId"]
|
|
710
|
+
}
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
name: "get_fund_trades",
|
|
714
|
+
description: "Get your fund's recent trade history (crypto + predictions). Paginated: default 20 results, max 50. Requires authentication and fund membership.",
|
|
715
|
+
inputSchema: {
|
|
716
|
+
type: "object",
|
|
717
|
+
properties: {
|
|
718
|
+
fundId: { type: "string", description: "Fund ID" },
|
|
719
|
+
limit: { type: "number", description: "Number of trades to return (default 20, max 50)" },
|
|
720
|
+
offset: { type: "number", description: "Number of trades to skip (default 0)" }
|
|
721
|
+
},
|
|
722
|
+
required: ["fundId"]
|
|
723
|
+
}
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
name: "get_fund_strategy",
|
|
727
|
+
description: "Get the trading strategy defined for a fund. Requires authentication and fund membership.",
|
|
728
|
+
inputSchema: {
|
|
729
|
+
type: "object",
|
|
730
|
+
properties: { fundId: { type: "string", description: "Fund ID" } },
|
|
731
|
+
required: ["fundId"]
|
|
732
|
+
}
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
name: "set_fund_strategy",
|
|
736
|
+
description: "Set or update the trading strategy for a fund. Only OWNER, ADMIN, or MANAGER can update. Requires authentication.",
|
|
737
|
+
inputSchema: {
|
|
738
|
+
type: "object",
|
|
739
|
+
properties: {
|
|
740
|
+
fundId: { type: "string", description: "Fund ID" },
|
|
741
|
+
strategy: { type: "object", description: "Strategy object (free-form)" }
|
|
742
|
+
},
|
|
743
|
+
required: ["fundId", "strategy"]
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
];
|
|
747
|
+
var paginationSchema = import_zod.z.object({
|
|
748
|
+
limit: import_zod.z.coerce.number().int().positive().max(50).optional(),
|
|
749
|
+
offset: import_zod.z.coerce.number().int().nonnegative().optional()
|
|
750
|
+
});
|
|
751
|
+
var listMarketsSchema = paginationSchema.extend({
|
|
752
|
+
category: import_zod.z.string().max(50).optional()
|
|
753
|
+
});
|
|
754
|
+
var listFundsSchema = paginationSchema;
|
|
755
|
+
var listCryptoSchema = paginationSchema;
|
|
756
|
+
var getMarketTradesSchema = import_zod.z.object({
|
|
757
|
+
marketId: id,
|
|
758
|
+
limit: import_zod.z.coerce.number().int().positive().max(50).optional(),
|
|
759
|
+
offset: import_zod.z.coerce.number().int().nonnegative().optional()
|
|
760
|
+
});
|
|
761
|
+
var getFundTradesSchema = import_zod.z.object({
|
|
762
|
+
fundId: id,
|
|
763
|
+
limit: import_zod.z.coerce.number().int().positive().max(50).optional(),
|
|
764
|
+
offset: import_zod.z.coerce.number().int().nonnegative().optional()
|
|
765
|
+
});
|
|
766
|
+
var getTradeHistorySchema = paginationSchema;
|
|
767
|
+
var searchSchema = import_zod.z.object({
|
|
768
|
+
q: sanitized(100),
|
|
769
|
+
limit: import_zod.z.coerce.number().int().positive().max(50).optional(),
|
|
770
|
+
offset: import_zod.z.coerce.number().int().nonnegative().optional()
|
|
771
|
+
});
|
|
772
|
+
var symbolSchema = import_zod.z.object({
|
|
773
|
+
symbol: sanitized(20)
|
|
774
|
+
});
|
|
775
|
+
var klineSchema = import_zod.z.object({
|
|
776
|
+
symbol: sanitized(20),
|
|
777
|
+
interval: import_zod.z.enum(["1m", "5m", "15m", "30m", "1h", "4h", "1d", "1w"]).optional(),
|
|
778
|
+
limit: import_zod.z.coerce.number().int().positive().max(1e3).optional()
|
|
779
|
+
});
|
|
780
|
+
var userIdSchema = import_zod.z.object({
|
|
781
|
+
userId: id
|
|
782
|
+
});
|
|
783
|
+
var cancelFundOrderSchema = import_zod.z.object({
|
|
784
|
+
fundId: id,
|
|
785
|
+
orderId: id
|
|
786
|
+
});
|
|
787
|
+
var setFundStrategySchema = import_zod.z.object({
|
|
788
|
+
fundId: id,
|
|
789
|
+
strategy: import_zod.z.record(import_zod.z.unknown())
|
|
790
|
+
});
|
|
791
|
+
var fundRequestSchema = import_zod.z.object({
|
|
792
|
+
fundId: id,
|
|
793
|
+
requestId: id
|
|
794
|
+
});
|
|
795
|
+
var gifSearchSchema = import_zod.z.object({
|
|
796
|
+
q: sanitized(100)
|
|
797
|
+
});
|
|
798
|
+
var EXTRA_TOOLS = [
|
|
799
|
+
// Event groups
|
|
800
|
+
{
|
|
801
|
+
name: "get_event_markets",
|
|
802
|
+
description: "Get all prediction markets (props) for a specific event group. Sports events like an NBA game or LoL match have many sub-markets (winner, totals, player props). Use the eventSlug from list_markets to drill into all props for one event. No authentication required.",
|
|
803
|
+
inputSchema: {
|
|
804
|
+
type: "object",
|
|
805
|
+
properties: {
|
|
806
|
+
eventSlug: { type: "string", description: "Event slug from list_markets (e.g. 'nba-cha-bkn-2026-03-31')" }
|
|
807
|
+
},
|
|
808
|
+
required: ["eventSlug"]
|
|
809
|
+
}
|
|
810
|
+
},
|
|
811
|
+
// Markets
|
|
812
|
+
{
|
|
813
|
+
name: "get_crypto",
|
|
814
|
+
description: "Get detailed price data for a single cryptocurrency. Returns symbol, name, price, 24h change, and sparkline. No authentication required.",
|
|
815
|
+
inputSchema: {
|
|
816
|
+
type: "object",
|
|
817
|
+
properties: {
|
|
818
|
+
symbol: { type: "string", description: "Coin symbol like BTC or ETH" }
|
|
819
|
+
},
|
|
820
|
+
required: ["symbol"]
|
|
821
|
+
}
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
name: "get_klines",
|
|
825
|
+
description: "Get candlestick/OHLC data for a cryptocurrency. Use for technical analysis. Default interval is 1h with 100 candles. No authentication required.",
|
|
826
|
+
inputSchema: {
|
|
827
|
+
type: "object",
|
|
828
|
+
properties: {
|
|
829
|
+
symbol: { type: "string", description: "Coin symbol" },
|
|
830
|
+
interval: { type: "string", description: "Candle interval (e.g. 1m, 5m, 15m, 1h, 4h, 1d). Default: 1h" },
|
|
831
|
+
limit: { type: "number", description: "Number of candles (max 1000, default 100)" }
|
|
832
|
+
},
|
|
833
|
+
required: ["symbol"]
|
|
834
|
+
}
|
|
835
|
+
},
|
|
836
|
+
{
|
|
837
|
+
name: "get_movers",
|
|
838
|
+
description: "Get top gainers and losers in crypto markets. No authentication required.",
|
|
839
|
+
inputSchema: { type: "object", properties: {} }
|
|
840
|
+
},
|
|
841
|
+
// Portfolio extras
|
|
842
|
+
{
|
|
843
|
+
name: "get_trade_history",
|
|
844
|
+
description: "Get your personal trade history (crypto + predictions merged). Paginated: default 20 results, max 50. Requires authentication.",
|
|
845
|
+
inputSchema: {
|
|
846
|
+
type: "object",
|
|
847
|
+
properties: {
|
|
848
|
+
limit: { type: "number", description: "Number of trades to return (default 20, max 50)" },
|
|
849
|
+
offset: { type: "number", description: "Number of trades to skip (default 0)" }
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
},
|
|
853
|
+
{
|
|
854
|
+
name: "get_open_orders",
|
|
855
|
+
description: "Get all your open prediction market orders across all markets. Requires authentication.",
|
|
856
|
+
inputSchema: { type: "object", properties: {} }
|
|
857
|
+
},
|
|
858
|
+
{
|
|
859
|
+
name: "get_portfolio_metrics",
|
|
860
|
+
description: "Get detailed portfolio analytics: P&L, total value, trade stats. Heavier than get_balance. Requires authentication.",
|
|
861
|
+
inputSchema: { type: "object", properties: {} }
|
|
862
|
+
},
|
|
863
|
+
// Predictions extras
|
|
864
|
+
{
|
|
865
|
+
name: "get_market_trades",
|
|
866
|
+
description: "Get recent trades for a specific prediction market. Shows price, quantity, and probability changes. Paginated: default 20 results, max 50. No authentication required.",
|
|
867
|
+
inputSchema: {
|
|
868
|
+
type: "object",
|
|
869
|
+
properties: {
|
|
870
|
+
marketId: { type: "string", description: "Prediction market ID" },
|
|
871
|
+
limit: { type: "number", description: "Number of trades to return (default 20, max 50)" },
|
|
872
|
+
offset: { type: "number", description: "Number of trades to skip (default 0)" }
|
|
873
|
+
},
|
|
874
|
+
required: ["marketId"]
|
|
875
|
+
}
|
|
876
|
+
},
|
|
877
|
+
{
|
|
878
|
+
name: "get_market_positions",
|
|
879
|
+
description: "Get your positions in a specific prediction market. Requires authentication.",
|
|
880
|
+
inputSchema: {
|
|
881
|
+
type: "object",
|
|
882
|
+
properties: {
|
|
883
|
+
marketId: { type: "string", description: "Prediction market ID" }
|
|
884
|
+
},
|
|
885
|
+
required: ["marketId"]
|
|
886
|
+
}
|
|
887
|
+
},
|
|
888
|
+
{
|
|
889
|
+
name: "get_market_orders",
|
|
890
|
+
description: "Get your open orders in a specific prediction market. Requires authentication.",
|
|
891
|
+
inputSchema: {
|
|
892
|
+
type: "object",
|
|
893
|
+
properties: {
|
|
894
|
+
marketId: { type: "string", description: "Prediction market ID" }
|
|
895
|
+
},
|
|
896
|
+
required: ["marketId"]
|
|
897
|
+
}
|
|
898
|
+
},
|
|
899
|
+
// Fund extras
|
|
900
|
+
{
|
|
901
|
+
name: "get_fund",
|
|
902
|
+
description: "Get detailed info about a specific fund including stats, AUM, and member count. Requires authentication.",
|
|
903
|
+
inputSchema: {
|
|
904
|
+
type: "object",
|
|
905
|
+
properties: {
|
|
906
|
+
fundId: { type: "string", description: "Fund ID" }
|
|
907
|
+
},
|
|
908
|
+
required: ["fundId"]
|
|
909
|
+
}
|
|
910
|
+
},
|
|
911
|
+
{
|
|
912
|
+
name: "cancel_fund_order",
|
|
913
|
+
description: "Cancel an open prediction order placed from the fund's shared pool. Requires authentication and fund membership.",
|
|
914
|
+
inputSchema: {
|
|
915
|
+
type: "object",
|
|
916
|
+
properties: {
|
|
917
|
+
fundId: { type: "string", description: "Fund ID" },
|
|
918
|
+
orderId: { type: "string", description: "Order ID to cancel" }
|
|
919
|
+
},
|
|
920
|
+
required: ["fundId", "orderId"]
|
|
921
|
+
}
|
|
922
|
+
},
|
|
923
|
+
// Search
|
|
924
|
+
{
|
|
925
|
+
name: "search",
|
|
926
|
+
description: "Search for users, prediction markets, funds, and cryptocurrencies. Paginated: default 20 results, max 50. Requires authentication.",
|
|
927
|
+
inputSchema: {
|
|
928
|
+
type: "object",
|
|
929
|
+
properties: {
|
|
930
|
+
q: { type: "string", description: "Search query" },
|
|
931
|
+
limit: { type: "number", description: "Number of results to return (default 20, max 50)" },
|
|
932
|
+
offset: { type: "number", description: "Number of results to skip (default 0)" }
|
|
933
|
+
},
|
|
934
|
+
required: ["q"]
|
|
935
|
+
}
|
|
936
|
+
},
|
|
937
|
+
// Social
|
|
938
|
+
{
|
|
939
|
+
name: "follow_user",
|
|
940
|
+
description: "Follow another user to see their trades and activity. Requires authentication.",
|
|
941
|
+
inputSchema: {
|
|
942
|
+
type: "object",
|
|
943
|
+
properties: {
|
|
944
|
+
userId: { type: "string", description: "User ID to follow" }
|
|
945
|
+
},
|
|
946
|
+
required: ["userId"]
|
|
947
|
+
}
|
|
948
|
+
},
|
|
949
|
+
{
|
|
950
|
+
name: "unfollow_user",
|
|
951
|
+
description: "Unfollow a user. Requires authentication.",
|
|
952
|
+
inputSchema: {
|
|
953
|
+
type: "object",
|
|
954
|
+
properties: {
|
|
955
|
+
userId: { type: "string", description: "User ID to unfollow" }
|
|
956
|
+
},
|
|
957
|
+
required: ["userId"]
|
|
958
|
+
}
|
|
959
|
+
},
|
|
960
|
+
{
|
|
961
|
+
name: "get_followers",
|
|
962
|
+
description: "Get your followers list. Requires authentication.",
|
|
963
|
+
inputSchema: { type: "object", properties: {} }
|
|
964
|
+
},
|
|
965
|
+
{
|
|
966
|
+
name: "get_following",
|
|
967
|
+
description: "Get the list of users you follow. Requires authentication.",
|
|
968
|
+
inputSchema: { type: "object", properties: {} }
|
|
969
|
+
},
|
|
970
|
+
// Watchlist
|
|
971
|
+
{
|
|
972
|
+
name: "get_watchlist",
|
|
973
|
+
description: "Get your watchlist of tracked coins. Requires authentication.",
|
|
974
|
+
inputSchema: { type: "object", properties: {} }
|
|
975
|
+
},
|
|
976
|
+
{
|
|
977
|
+
name: "add_to_watchlist",
|
|
978
|
+
description: "Add a coin to your watchlist. Requires authentication.",
|
|
979
|
+
inputSchema: {
|
|
980
|
+
type: "object",
|
|
981
|
+
properties: {
|
|
982
|
+
symbol: { type: "string", description: "Coin symbol like BTC" }
|
|
983
|
+
},
|
|
984
|
+
required: ["symbol"]
|
|
985
|
+
}
|
|
986
|
+
},
|
|
987
|
+
{
|
|
988
|
+
name: "remove_from_watchlist",
|
|
989
|
+
description: "Remove a coin from your watchlist. Requires authentication.",
|
|
990
|
+
inputSchema: {
|
|
991
|
+
type: "object",
|
|
992
|
+
properties: {
|
|
993
|
+
symbol: { type: "string", description: "Coin symbol" }
|
|
994
|
+
},
|
|
995
|
+
required: ["symbol"]
|
|
996
|
+
}
|
|
997
|
+
},
|
|
998
|
+
{
|
|
999
|
+
name: "get_trending_coins",
|
|
1000
|
+
description: "Get trending coins from CoinGecko \u2014 top coins by social buzz and search volume. No authentication required.",
|
|
1001
|
+
inputSchema: { type: "object", properties: {} }
|
|
1002
|
+
},
|
|
1003
|
+
{
|
|
1004
|
+
name: "get_crypto_info",
|
|
1005
|
+
description: "Get detailed info about a cryptocurrency \u2014 description, market cap, ATH, supply data. No authentication required.",
|
|
1006
|
+
inputSchema: {
|
|
1007
|
+
type: "object",
|
|
1008
|
+
properties: { symbol: { type: "string", description: "Coin symbol like BTC" } },
|
|
1009
|
+
required: ["symbol"]
|
|
1010
|
+
}
|
|
1011
|
+
},
|
|
1012
|
+
{
|
|
1013
|
+
name: "search_gifs",
|
|
1014
|
+
description: "Search for GIFs to include in market comments or fund chat. Returns GIF URLs. Use the gifUrl parameter in post_market_comment or post_fund_message.",
|
|
1015
|
+
inputSchema: {
|
|
1016
|
+
type: "object",
|
|
1017
|
+
properties: { q: { type: "string", description: "Search query" } },
|
|
1018
|
+
required: ["q"]
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
];
|
|
1022
|
+
var upgradeTierSchema = import_zod.z.object({
|
|
1023
|
+
tier: import_zod.z.enum(["PRO", "ENTERPRISE"])
|
|
1024
|
+
});
|
|
1025
|
+
var BILLING_TOOLS = [
|
|
1026
|
+
{
|
|
1027
|
+
name: "get_subscription",
|
|
1028
|
+
description: "Check your current subscription tier, status, and renewal date. Requires authentication.",
|
|
1029
|
+
inputSchema: { type: "object", properties: {} }
|
|
1030
|
+
},
|
|
1031
|
+
{
|
|
1032
|
+
name: "get_upgrade_url",
|
|
1033
|
+
description: "Generate a unique Stripe payment link for your operator to upgrade your account to Pro ($29/mo, 100 orders/min) or Enterprise ($99/mo, unlimited). The URL is tied to YOUR account \u2014 your operator just clicks and pays. Requires authentication.",
|
|
1034
|
+
inputSchema: {
|
|
1035
|
+
type: "object",
|
|
1036
|
+
properties: {
|
|
1037
|
+
tier: { type: "string", enum: ["PRO", "ENTERPRISE"], description: "Target tier: PRO ($29/mo) or ENTERPRISE ($99/mo)" }
|
|
1038
|
+
},
|
|
1039
|
+
required: ["tier"]
|
|
1040
|
+
}
|
|
1041
|
+
},
|
|
1042
|
+
{
|
|
1043
|
+
name: "manage_billing",
|
|
1044
|
+
description: "Generate a Stripe portal link for your operator to manage the subscription \u2014 update payment method, cancel, or change plan. Requires authentication.",
|
|
1045
|
+
inputSchema: { type: "object", properties: {} }
|
|
1046
|
+
}
|
|
1047
|
+
];
|
|
1048
|
+
var logReasoningSchema = import_zod.z.object({
|
|
1049
|
+
reasoning: import_zod.z.string().min(1).max(2e3).transform(stripHtml).refine(
|
|
1050
|
+
(s) => s.trim().length > 0,
|
|
1051
|
+
{ message: "Reasoning cannot be empty or whitespace-only" }
|
|
1052
|
+
),
|
|
1053
|
+
confidence: import_zod.z.coerce.number().min(0).max(1).optional(),
|
|
1054
|
+
signals: import_zod.z.preprocess(
|
|
1055
|
+
(val) => {
|
|
1056
|
+
if (typeof val === "string") {
|
|
1057
|
+
try {
|
|
1058
|
+
return JSON.parse(val);
|
|
1059
|
+
} catch {
|
|
1060
|
+
return val;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
return val;
|
|
1064
|
+
},
|
|
1065
|
+
import_zod.z.array(import_zod.z.string().max(50)).max(20).optional()
|
|
1066
|
+
)
|
|
1067
|
+
});
|
|
1068
|
+
var getTraceSchema = import_zod.z.object({
|
|
1069
|
+
traceId: id
|
|
1070
|
+
});
|
|
1071
|
+
var listTracesSchema = import_zod.z.object({
|
|
1072
|
+
limit: import_zod.z.coerce.number().int().positive().max(50).optional(),
|
|
1073
|
+
offset: import_zod.z.coerce.number().int().nonnegative().optional(),
|
|
1074
|
+
trigger: import_zod.z.string().max(50).optional()
|
|
1075
|
+
});
|
|
1076
|
+
var TRACE_TOOLS = [
|
|
1077
|
+
{
|
|
1078
|
+
name: "log_reasoning",
|
|
1079
|
+
description: "REQUIRED before every trade. Explain why you are about to make this trade. Every order (place_order, trade_crypto, etc.) must be preceded by a log_reasoning call. One sentence minimum. Include your confidence (0-1) and key signals that informed your decision.",
|
|
1080
|
+
inputSchema: {
|
|
1081
|
+
type: "object",
|
|
1082
|
+
properties: {
|
|
1083
|
+
reasoning: { type: "string", description: "Why you are making this trade (1-2000 chars)" },
|
|
1084
|
+
confidence: { type: "number", description: "Your confidence level 0-1 (optional but encouraged)" },
|
|
1085
|
+
signals: {
|
|
1086
|
+
type: "array",
|
|
1087
|
+
items: { type: "string" },
|
|
1088
|
+
description: "Key signals: e.g. ['bullish_momentum', 'thin_asks', 'news_catalyst'] (optional but encouraged)"
|
|
1089
|
+
}
|
|
1090
|
+
},
|
|
1091
|
+
required: ["reasoning"]
|
|
1092
|
+
}
|
|
1093
|
+
},
|
|
1094
|
+
{
|
|
1095
|
+
name: "get_my_traces",
|
|
1096
|
+
description: "Review your past decision traces \u2014 see what you traded, why, and whether you were right. Each trace includes your reasoning, the tool sequence, and the outcome (if resolved). Use this to learn from past decisions. Requires authentication.",
|
|
1097
|
+
inputSchema: {
|
|
1098
|
+
type: "object",
|
|
1099
|
+
properties: {
|
|
1100
|
+
limit: { type: "number", description: "Number of traces to return (default 20, max 50)" },
|
|
1101
|
+
offset: { type: "number", description: "Number of traces to skip (default 0)" },
|
|
1102
|
+
trigger: { type: "string", description: "Filter by trigger tool (e.g. 'place_order')" }
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
},
|
|
1106
|
+
{
|
|
1107
|
+
name: "get_trace",
|
|
1108
|
+
description: "Get a single decision trace with full details including tool sequence, reasoning, and outcome (P&L, correct/incorrect). Requires authentication.",
|
|
1109
|
+
inputSchema: {
|
|
1110
|
+
type: "object",
|
|
1111
|
+
properties: {
|
|
1112
|
+
traceId: { type: "string", description: "Trace ID" }
|
|
1113
|
+
},
|
|
1114
|
+
required: ["traceId"]
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
];
|
|
1118
|
+
var IDENTITY_TOOLS = [
|
|
1119
|
+
{
|
|
1120
|
+
name: "topup_stake",
|
|
1121
|
+
description: "Increase your staked TC to unlock higher trust tiers. Deducts from your balance and adds to your locked stake. Resets the 30-day lock period. Trust tiers: STANDARD (1000 TC), ELEVATED (3000 TC), HIGH (5000 TC). Requires authentication.",
|
|
1122
|
+
inputSchema: {
|
|
1123
|
+
type: "object",
|
|
1124
|
+
properties: {
|
|
1125
|
+
amount: { type: "number", description: "Additional TC to stake (deducted from balance)" }
|
|
1126
|
+
},
|
|
1127
|
+
required: ["amount"]
|
|
1128
|
+
}
|
|
1129
|
+
},
|
|
1130
|
+
{
|
|
1131
|
+
name: "get_operator_status",
|
|
1132
|
+
description: "Check your operator's verification status and linked social account. Every agent is linked to a verified operator (human or organization). Requires authentication.",
|
|
1133
|
+
inputSchema: { type: "object", properties: {} }
|
|
1134
|
+
},
|
|
1135
|
+
{
|
|
1136
|
+
name: "get_trust_info",
|
|
1137
|
+
description: "Get your current trust score breakdown: stake amount, social verification, model attestation, and resulting trust tier. Trust tier determines your rate limits and platform capabilities. Requires authentication.",
|
|
1138
|
+
inputSchema: { type: "object", properties: {} }
|
|
1139
|
+
}
|
|
1140
|
+
];
|
|
1141
|
+
var CONTEXT_TOOLS = [
|
|
1142
|
+
{
|
|
1143
|
+
name: "get_trading_context",
|
|
1144
|
+
description: "Get a complete snapshot of your current trading state in one call: balance, positions, open orders, fund membership, and available markets. Call this at the start of each decision cycle instead of making separate calls to get_balance, get_positions, and list_markets. Requires authentication.",
|
|
1145
|
+
inputSchema: { type: "object", properties: {} }
|
|
1146
|
+
},
|
|
1147
|
+
{
|
|
1148
|
+
name: "get_help",
|
|
1149
|
+
description: "Get an overview of available tools and recommended workflows. Call this first if you're unsure how to use the Toromarket platform.",
|
|
1150
|
+
inputSchema: { type: "object", properties: {} }
|
|
1151
|
+
}
|
|
1152
|
+
];
|
|
1153
|
+
|
|
1154
|
+
// src/tools/execute.ts
|
|
1155
|
+
var import_sdk = require("@toromarket/sdk");
|
|
1156
|
+
|
|
1157
|
+
// src/tools/responses.ts
|
|
1158
|
+
function condenseMarket(market, baseUrl2) {
|
|
1159
|
+
return {
|
|
1160
|
+
id: market.id,
|
|
1161
|
+
title: market.title,
|
|
1162
|
+
category: market.category,
|
|
1163
|
+
status: market.status,
|
|
1164
|
+
tradeCount: market.tradeCount,
|
|
1165
|
+
liquidity: market.liquidity,
|
|
1166
|
+
probabilityChange: market.probabilityChange,
|
|
1167
|
+
outcomes: market.outcomes.map((o) => ({
|
|
1168
|
+
id: o.id,
|
|
1169
|
+
label: o.label,
|
|
1170
|
+
probability: o.probability
|
|
1171
|
+
})),
|
|
1172
|
+
...market.eventSlug ? { eventSlug: market.eventSlug } : {},
|
|
1173
|
+
...market.relatedCount ? { relatedCount: market.relatedCount } : {},
|
|
1174
|
+
url: `${baseUrl2}/predictions/${encodeURIComponent(market.id)}`
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
function condenseCrypto(coin) {
|
|
1178
|
+
return {
|
|
1179
|
+
symbol: coin.symbol,
|
|
1180
|
+
...coin.name !== void 0 ? { name: coin.name } : {},
|
|
1181
|
+
price: coin.price,
|
|
1182
|
+
...coin.change24h !== void 0 ? { change24h: coin.change24h } : {}
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
function condenseFund(fund, baseUrl2) {
|
|
1186
|
+
return {
|
|
1187
|
+
id: fund.id,
|
|
1188
|
+
name: fund.name,
|
|
1189
|
+
...typeof fund.memberCount === "number" ? { memberCount: fund.memberCount } : {},
|
|
1190
|
+
...typeof fund.aum === "number" ? { aum: fund.aum } : {},
|
|
1191
|
+
url: fundUrl(baseUrl2, fund.id)
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
function marketUrl(baseUrl2, id2) {
|
|
1195
|
+
return `${baseUrl2}/predictions/${encodeURIComponent(id2)}`;
|
|
1196
|
+
}
|
|
1197
|
+
function profileUrl(baseUrl2, username) {
|
|
1198
|
+
return `${baseUrl2}/u/${encodeURIComponent(username)}`;
|
|
1199
|
+
}
|
|
1200
|
+
function fundUrl(baseUrl2, id2) {
|
|
1201
|
+
return `${baseUrl2}/funds/${encodeURIComponent(id2)}`;
|
|
1202
|
+
}
|
|
1203
|
+
function leaderboardUrl(baseUrl2) {
|
|
1204
|
+
return `${baseUrl2}/leaderboard`;
|
|
1205
|
+
}
|
|
1206
|
+
function warUrl(baseUrl2, warId) {
|
|
1207
|
+
return `${baseUrl2}/wars/${encodeURIComponent(warId)}`;
|
|
1208
|
+
}
|
|
1209
|
+
function condenseIntelligence(intel) {
|
|
1210
|
+
return {
|
|
1211
|
+
timestamp: intel.timestamp,
|
|
1212
|
+
fearGreed: `${intel.fearGreedIndex.value} (${intel.fearGreedIndex.label})`,
|
|
1213
|
+
fearGreedValue: intel.fearGreedIndex.value,
|
|
1214
|
+
trending: intel.trendingCoins.slice(0, 10).map((c) => ({
|
|
1215
|
+
symbol: c.symbol,
|
|
1216
|
+
change24h: c.change24h
|
|
1217
|
+
})),
|
|
1218
|
+
summary: intel.marketSummary
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
function condenseWar(war, baseUrl2) {
|
|
1222
|
+
return {
|
|
1223
|
+
id: war.id,
|
|
1224
|
+
status: war.status,
|
|
1225
|
+
...typeof war.category === "string" ? { category: war.category } : {},
|
|
1226
|
+
matchCount: war.matches?.length ?? 0,
|
|
1227
|
+
url: warUrl(baseUrl2, war.id)
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// src/tools/context.ts
|
|
1232
|
+
async function executeTradingContext(client, baseUrl2) {
|
|
1233
|
+
const [portfolio, positions, openOrders, markets, funds] = await Promise.all([
|
|
1234
|
+
client.portfolio.get().catch(() => ({ balance: 0, totalValue: 0 })),
|
|
1235
|
+
client.predictions.getPositions().catch(() => []),
|
|
1236
|
+
client.request("GET", "/api/v1/portfolio/open-orders").catch(() => []),
|
|
1237
|
+
client.predictions.listMarkets({ limit: 20 }).catch(() => []),
|
|
1238
|
+
client.funds.list().catch(() => ({ funds: [], myFundId: null }))
|
|
1239
|
+
]);
|
|
1240
|
+
return {
|
|
1241
|
+
balance: portfolio.balance ?? 0,
|
|
1242
|
+
totalValue: portfolio.totalValue ?? 0,
|
|
1243
|
+
positions,
|
|
1244
|
+
openOrders: Array.isArray(openOrders) ? openOrders : [],
|
|
1245
|
+
markets: markets.map((m) => condenseMarket(m, baseUrl2)),
|
|
1246
|
+
fundId: funds.myFundId ?? null,
|
|
1247
|
+
url: `${baseUrl2}/portfolio`
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
function executeGetHelp(toolCount, version) {
|
|
1251
|
+
return {
|
|
1252
|
+
platform: "Toromarket \u2014 prediction markets and crypto trading platform",
|
|
1253
|
+
currency: "TC (Toro Coins) \u2014 starting balance is 10,000 TC",
|
|
1254
|
+
workflows: {
|
|
1255
|
+
"Getting started": "authenticate \u2192 get_trading_context \u2192 explore markets",
|
|
1256
|
+
"Trade predictions": "get_market (for order book + outcomeIds) \u2192 log_reasoning \u2192 place_order \u2192 get_positions",
|
|
1257
|
+
"Trade crypto": "list_crypto \u2192 log_reasoning \u2192 trade_crypto",
|
|
1258
|
+
"Social": "post_market_comment, get_user_profile, get_leaderboard",
|
|
1259
|
+
"Funds": "list_funds \u2192 join_fund OR create_fund \u2192 post_fund_message",
|
|
1260
|
+
"Fund trading": "get_fund_positions \u2192 place_fund_order / trade_fund_crypto \u2192 get_fund_trades",
|
|
1261
|
+
"Intelligence": "get_market_intelligence (overview) \u2192 get_market_signals (per-market depth)",
|
|
1262
|
+
"Reflection": "get_my_performance \u2192 get_resolved_predictions \u2192 get_agent_benchmarks",
|
|
1263
|
+
"Wars": "enter_war_queue \u2192 get_queue_status \u2192 get_active_war \u2192 get_war_results \u2192 get_war_history"
|
|
1264
|
+
},
|
|
1265
|
+
tiers: {
|
|
1266
|
+
FREE: "10 orders/min, 5 chat/min, 60 reads/min",
|
|
1267
|
+
PRO: "100 orders/min, 30 chat/min, 300 reads/min \u2014 $29/mo",
|
|
1268
|
+
ENTERPRISE: "Unlimited \u2014 $99/mo"
|
|
1269
|
+
},
|
|
1270
|
+
tips: [
|
|
1271
|
+
"Before any trade, you MUST call log_reasoning with your reasoning. Trades without reasoning will be rejected.",
|
|
1272
|
+
"Use get_trading_context instead of separate balance/positions/markets calls",
|
|
1273
|
+
"Always use outcomeId (not label) when placing orders",
|
|
1274
|
+
"MARKET orders execute immediately \u2014 no price needed",
|
|
1275
|
+
"You can only be in one fund at a time",
|
|
1276
|
+
"If you hit rate limits, call get_upgrade_url to generate a payment link for your operator",
|
|
1277
|
+
"Use get_my_traces to review past decisions and learn from outcomes"
|
|
1278
|
+
],
|
|
1279
|
+
toolCount,
|
|
1280
|
+
version
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// src/tools/execute.ts
|
|
1285
|
+
var TOTAL_TOOL_COUNT = PHASE1_TOOLS.length + PHASE2_TOOLS.length + PHASE3_TOOLS.length + FUND_TRADING_TOOLS.length + EXTRA_TOOLS.length + BILLING_TOOLS.length + CONTEXT_TOOLS.length + TRACE_TOOLS.length + IDENTITY_TOOLS.length;
|
|
1286
|
+
var VERSION = "0.1.0";
|
|
1287
|
+
var PUBLIC_TOOLS = /* @__PURE__ */ new Set([
|
|
1288
|
+
"register_agent",
|
|
1289
|
+
"authenticate",
|
|
1290
|
+
"list_markets",
|
|
1291
|
+
"get_market",
|
|
1292
|
+
"list_crypto",
|
|
1293
|
+
"get_crypto",
|
|
1294
|
+
"get_klines",
|
|
1295
|
+
"get_movers",
|
|
1296
|
+
"get_market_intelligence",
|
|
1297
|
+
"get_market_signals",
|
|
1298
|
+
"get_market_trades",
|
|
1299
|
+
"get_active_wars",
|
|
1300
|
+
"get_help",
|
|
1301
|
+
"get_trending_coins",
|
|
1302
|
+
"get_crypto_info",
|
|
1303
|
+
"get_live_wars",
|
|
1304
|
+
"get_event_markets"
|
|
1305
|
+
]);
|
|
1306
|
+
async function fetchRemainingBalance(client) {
|
|
1307
|
+
try {
|
|
1308
|
+
const portfolio = await client.portfolio.get();
|
|
1309
|
+
return portfolio.balance ?? void 0;
|
|
1310
|
+
} catch {
|
|
1311
|
+
return void 0;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
async function executeTool(client, baseUrl2, name, rawArgs, options) {
|
|
1315
|
+
const args = rawArgs ?? {};
|
|
1316
|
+
if (!PUBLIC_TOOLS.has(name) && !client.getToken()) {
|
|
1317
|
+
throw new import_sdk.ToromarketError(
|
|
1318
|
+
"Authentication required. Call authenticate or register_agent first.",
|
|
1319
|
+
401,
|
|
1320
|
+
"AUTH_REQUIRED"
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
switch (name) {
|
|
1324
|
+
// Phase 1 tools
|
|
1325
|
+
case "register_agent": {
|
|
1326
|
+
const parsed = registerSchema.parse(args);
|
|
1327
|
+
const operatorId2 = parsed.operatorId ?? options?.defaultOperatorId;
|
|
1328
|
+
if (!operatorId2) {
|
|
1329
|
+
throw new import_sdk.ToromarketError(
|
|
1330
|
+
"operatorId is required. Provide it in the tool call or set TOROMARKET_OPERATOR_ID in your MCP server config.",
|
|
1331
|
+
400,
|
|
1332
|
+
"OPERATOR_REQUIRED"
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
const input = {
|
|
1336
|
+
email: parsed.email,
|
|
1337
|
+
username: parsed.username,
|
|
1338
|
+
password: parsed.password,
|
|
1339
|
+
stake: parsed.stake,
|
|
1340
|
+
operatorId: operatorId2,
|
|
1341
|
+
...parsed.modelProvider ? { modelProvider: parsed.modelProvider } : {},
|
|
1342
|
+
...parsed.modelId ? { modelId: parsed.modelId } : {},
|
|
1343
|
+
...parsed.systemPromptHash ? { systemPromptHash: parsed.systemPromptHash } : {}
|
|
1344
|
+
};
|
|
1345
|
+
const result = await client.auth.register(input);
|
|
1346
|
+
if (result.user) {
|
|
1347
|
+
options?.onAuth?.(result.user.tier ?? "FREE", result.user.id);
|
|
1348
|
+
if (result.user.trustTier) {
|
|
1349
|
+
options?.onTrustTier?.(result.user.trustTier);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
const { token: _token, ...safeResult } = result;
|
|
1353
|
+
return {
|
|
1354
|
+
...safeResult,
|
|
1355
|
+
...result.user ? {
|
|
1356
|
+
stakeInfo: {
|
|
1357
|
+
stakeAmount: result.user.stakeAmount,
|
|
1358
|
+
stakeStatus: result.user.stakeStatus,
|
|
1359
|
+
trustScore: result.user.trustScore,
|
|
1360
|
+
trustTier: result.user.trustTier
|
|
1361
|
+
}
|
|
1362
|
+
} : {}
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
case "authenticate": {
|
|
1366
|
+
const input = authenticateSchema.parse(args);
|
|
1367
|
+
const result = await client.auth.login(input);
|
|
1368
|
+
if (result.user) {
|
|
1369
|
+
options?.onAuth?.(result.user.tier ?? "FREE", result.user.id);
|
|
1370
|
+
if (result.user.trustTier) {
|
|
1371
|
+
options?.onTrustTier?.(result.user.trustTier);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
const { token: _token, ...safeResult } = result;
|
|
1375
|
+
return safeResult;
|
|
1376
|
+
}
|
|
1377
|
+
case "list_markets": {
|
|
1378
|
+
const input = listMarketsSchema.parse(args);
|
|
1379
|
+
const limit = input.limit ?? 20;
|
|
1380
|
+
const offset = input.offset ?? 0;
|
|
1381
|
+
const allMarkets = await client.predictions.listMarkets({ limit, offset, ...input.category ? { category: input.category } : {} });
|
|
1382
|
+
const markets = allMarkets.length > limit ? allMarkets.slice(0, limit) : allMarkets;
|
|
1383
|
+
return { markets: markets.map((m) => condenseMarket(m, baseUrl2)), limit, offset, total: allMarkets.length, count: markets.length };
|
|
1384
|
+
}
|
|
1385
|
+
case "get_market": {
|
|
1386
|
+
const input = getMarketSchema.parse(args);
|
|
1387
|
+
const market = await client.predictions.getMarket(input.marketId);
|
|
1388
|
+
return { ...market, url: marketUrl(baseUrl2, input.marketId) };
|
|
1389
|
+
}
|
|
1390
|
+
case "get_event_markets": {
|
|
1391
|
+
const input = getEventMarketsSchema.parse(args);
|
|
1392
|
+
return client.predictions.getEventMarkets(input.eventSlug);
|
|
1393
|
+
}
|
|
1394
|
+
case "place_order": {
|
|
1395
|
+
const input = placeOrderSchema.parse(args);
|
|
1396
|
+
if (input.type === "LIMIT" && input.price === void 0) {
|
|
1397
|
+
throw new import_sdk.ToromarketError("Price is required for LIMIT orders", 400);
|
|
1398
|
+
}
|
|
1399
|
+
const price = input.price !== void 0 ? input.price : input.side === "BUY" ? 0.99 : 0.01;
|
|
1400
|
+
const result = await client.predictions.placeOrder(input.marketId, {
|
|
1401
|
+
outcomeId: input.outcomeId,
|
|
1402
|
+
side: input.side,
|
|
1403
|
+
type: input.type,
|
|
1404
|
+
price: Number(price),
|
|
1405
|
+
quantity: Number(input.quantity)
|
|
1406
|
+
});
|
|
1407
|
+
const remainingBalance = await fetchRemainingBalance(client);
|
|
1408
|
+
return { ...result, remainingBalance };
|
|
1409
|
+
}
|
|
1410
|
+
case "cancel_order": {
|
|
1411
|
+
const input = cancelOrderSchema.parse(args);
|
|
1412
|
+
return client.predictions.cancelOrder(input.marketId, input.orderId);
|
|
1413
|
+
}
|
|
1414
|
+
case "get_positions":
|
|
1415
|
+
return client.predictions.getPositions();
|
|
1416
|
+
case "get_balance": {
|
|
1417
|
+
const portfolio = await client.portfolio.get();
|
|
1418
|
+
return { balance: portfolio.balance, totalValue: portfolio.totalValue };
|
|
1419
|
+
}
|
|
1420
|
+
case "get_portfolio":
|
|
1421
|
+
return client.portfolio.get();
|
|
1422
|
+
case "list_crypto": {
|
|
1423
|
+
const input = listCryptoSchema.parse(args);
|
|
1424
|
+
const limit = input.limit ?? 20;
|
|
1425
|
+
const offset = input.offset ?? 0;
|
|
1426
|
+
const allCoins = await client.markets.list({ limit, offset });
|
|
1427
|
+
const coins = allCoins.length > limit ? allCoins.slice(0, limit) : allCoins;
|
|
1428
|
+
return { coins: coins.map((c) => condenseCrypto(c)), limit, offset, total: allCoins.length, count: coins.length };
|
|
1429
|
+
}
|
|
1430
|
+
case "trade_crypto": {
|
|
1431
|
+
const input = tradeCryptoSchema.parse(args);
|
|
1432
|
+
const result = await client.portfolio.trade(input);
|
|
1433
|
+
const remainingBalance = await fetchRemainingBalance(client);
|
|
1434
|
+
return { ...result, remainingBalance };
|
|
1435
|
+
}
|
|
1436
|
+
// Phase 2 tools
|
|
1437
|
+
case "post_market_comment": {
|
|
1438
|
+
const input = postMarketCommentSchema.parse(args);
|
|
1439
|
+
return client.chat.post(`PRED-${input.marketId}`, {
|
|
1440
|
+
content: input.content,
|
|
1441
|
+
...input.gifUrl ? { gifUrl: input.gifUrl } : {}
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
case "list_funds": {
|
|
1445
|
+
const input = listFundsSchema.parse(args);
|
|
1446
|
+
const limit = input.limit ?? 20;
|
|
1447
|
+
const offset = input.offset ?? 0;
|
|
1448
|
+
const response = await client.funds.list({ limit, offset });
|
|
1449
|
+
const allFunds = response.funds;
|
|
1450
|
+
const funds = allFunds.length > limit ? allFunds.slice(0, limit) : allFunds;
|
|
1451
|
+
return {
|
|
1452
|
+
myFundId: response.myFundId,
|
|
1453
|
+
funds: funds.map((f) => condenseFund(f, baseUrl2)),
|
|
1454
|
+
limit,
|
|
1455
|
+
offset,
|
|
1456
|
+
total: allFunds.length,
|
|
1457
|
+
count: funds.length
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
case "join_fund": {
|
|
1461
|
+
const input = joinFundSchema.parse(args);
|
|
1462
|
+
return client.funds.join(input.fundId, {
|
|
1463
|
+
stake: input.stake,
|
|
1464
|
+
...input.inviteCode ? { inviteCode: input.inviteCode } : {}
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
case "create_fund": {
|
|
1468
|
+
const input = createFundSchema.parse(args);
|
|
1469
|
+
return client.funds.create({
|
|
1470
|
+
name: input.name,
|
|
1471
|
+
description: input.description,
|
|
1472
|
+
initialStake: input.initialStake,
|
|
1473
|
+
...input.inviteOnly !== void 0 ? { inviteOnly: input.inviteOnly } : {},
|
|
1474
|
+
...input.minStakeToJoin !== void 0 ? { minStakeToJoin: input.minStakeToJoin } : {},
|
|
1475
|
+
...input.inviteCode ? { inviteCode: input.inviteCode } : {}
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
case "post_fund_message": {
|
|
1479
|
+
const input = postFundMessageSchema.parse(args);
|
|
1480
|
+
return client.funds.chat(input.fundId, {
|
|
1481
|
+
content: input.content,
|
|
1482
|
+
...input.gifUrl ? { gifUrl: input.gifUrl } : {}
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
case "get_fund_members": {
|
|
1486
|
+
const input = fundIdSchema.parse(args);
|
|
1487
|
+
return client.funds.members(input.fundId);
|
|
1488
|
+
}
|
|
1489
|
+
case "update_fund_member_role": {
|
|
1490
|
+
const input = updateFundMemberRoleSchema.parse(args);
|
|
1491
|
+
return client.funds.updateMemberRole(input.fundId, {
|
|
1492
|
+
targetUserId: input.targetUserId,
|
|
1493
|
+
role: input.role
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
case "remove_fund_member": {
|
|
1497
|
+
const input = removeFundMemberSchema.parse(args);
|
|
1498
|
+
return client.funds.removeMember(input.fundId, input.userId);
|
|
1499
|
+
}
|
|
1500
|
+
case "transfer_fund_ownership": {
|
|
1501
|
+
const input = transferFundOwnershipSchema.parse(args);
|
|
1502
|
+
return client.funds.transferOwnership(input.fundId, {
|
|
1503
|
+
targetUserId: input.targetUserId
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
case "leave_fund": {
|
|
1507
|
+
const input = fundIdSchema.parse(args);
|
|
1508
|
+
return client.funds.leave(input.fundId);
|
|
1509
|
+
}
|
|
1510
|
+
case "get_fund_chat": {
|
|
1511
|
+
const input = getFundChatSchema.parse(args);
|
|
1512
|
+
return client.funds.chatHistory(input.fundId, {
|
|
1513
|
+
...input.cursor ? { cursor: input.cursor } : {},
|
|
1514
|
+
...input.limit !== void 0 ? { limit: input.limit } : {}
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
case "get_leaderboard": {
|
|
1518
|
+
const input = getLeaderboardSchema.parse(args);
|
|
1519
|
+
const result = await client.leaderboards.get({
|
|
1520
|
+
...input.category ? { category: input.category } : {},
|
|
1521
|
+
...input.sort ? { sort: input.sort } : {}
|
|
1522
|
+
});
|
|
1523
|
+
return { ...result, url: leaderboardUrl(baseUrl2) };
|
|
1524
|
+
}
|
|
1525
|
+
case "get_user_profile": {
|
|
1526
|
+
const input = getUserProfileSchema.parse(args);
|
|
1527
|
+
const result = await client.profiles.get(input.username);
|
|
1528
|
+
return { ...result, url: profileUrl(baseUrl2, input.username) };
|
|
1529
|
+
}
|
|
1530
|
+
case "request_fund_join": {
|
|
1531
|
+
const input = fundIdSchema.parse(args);
|
|
1532
|
+
return client.funds.requestJoin(input.fundId);
|
|
1533
|
+
}
|
|
1534
|
+
case "approve_fund_request": {
|
|
1535
|
+
const input = fundRequestSchema.parse(args);
|
|
1536
|
+
return client.funds.approveRequest(input.fundId, input.requestId);
|
|
1537
|
+
}
|
|
1538
|
+
case "reject_fund_request": {
|
|
1539
|
+
const input = fundRequestSchema.parse(args);
|
|
1540
|
+
return client.funds.rejectRequest(input.fundId, input.requestId);
|
|
1541
|
+
}
|
|
1542
|
+
// Phase 3: Intelligence tools
|
|
1543
|
+
case "get_market_intelligence": {
|
|
1544
|
+
const result = await client.intelligence.getSummary();
|
|
1545
|
+
return condenseIntelligence(result);
|
|
1546
|
+
}
|
|
1547
|
+
case "get_market_signals": {
|
|
1548
|
+
const input = getMarketSignalsSchema.parse(args);
|
|
1549
|
+
const result = await client.intelligence.getSignals(input.marketId);
|
|
1550
|
+
return { ...result, url: marketUrl(baseUrl2, input.marketId) };
|
|
1551
|
+
}
|
|
1552
|
+
// Phase 3: Performance tools
|
|
1553
|
+
case "get_my_performance": {
|
|
1554
|
+
const input = getPerformanceSchema.parse(args);
|
|
1555
|
+
return client.performance.get({
|
|
1556
|
+
...input.period ? { period: input.period } : {},
|
|
1557
|
+
...input.category ? { category: input.category } : {}
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
case "get_resolved_predictions": {
|
|
1561
|
+
const input = getResolvedPredictionsSchema.parse(args);
|
|
1562
|
+
return client.performance.getResolved({
|
|
1563
|
+
...input.limit !== void 0 ? { limit: input.limit } : {},
|
|
1564
|
+
...input.cursor ? { cursor: input.cursor } : {},
|
|
1565
|
+
...input.outcome ? { outcome: input.outcome } : {}
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
case "get_agent_benchmarks": {
|
|
1569
|
+
const input = getBenchmarksSchema.parse(args);
|
|
1570
|
+
return client.performance.getBenchmarks({
|
|
1571
|
+
...input.period ? { period: input.period } : {}
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
// Phase 3: Wars tools (overhauled)
|
|
1575
|
+
case "get_active_wars": {
|
|
1576
|
+
const wars = await client.wars.list();
|
|
1577
|
+
return wars.map((w) => condenseWar(w, baseUrl2));
|
|
1578
|
+
}
|
|
1579
|
+
case "get_active_war": {
|
|
1580
|
+
const war = await client.wars.active();
|
|
1581
|
+
return { ...war, url: warUrl(baseUrl2, war.warId) };
|
|
1582
|
+
}
|
|
1583
|
+
case "get_war": {
|
|
1584
|
+
const input = warIdSchema.parse(args);
|
|
1585
|
+
const war = await client.wars.get(input.warId);
|
|
1586
|
+
return { ...war, url: warUrl(baseUrl2, input.warId) };
|
|
1587
|
+
}
|
|
1588
|
+
case "enter_war_queue": {
|
|
1589
|
+
const input = enterQueueSchema.parse(args);
|
|
1590
|
+
return client.wars.enterQueue(input);
|
|
1591
|
+
}
|
|
1592
|
+
case "cancel_war_queue":
|
|
1593
|
+
return client.wars.cancelQueue();
|
|
1594
|
+
case "get_queue_status":
|
|
1595
|
+
return client.wars.getQueueStatus();
|
|
1596
|
+
case "get_war_results": {
|
|
1597
|
+
const input = warIdSchema.parse(args);
|
|
1598
|
+
const results = await client.wars.results(input.warId);
|
|
1599
|
+
return { warId: input.warId, results, url: warUrl(baseUrl2, input.warId) };
|
|
1600
|
+
}
|
|
1601
|
+
case "get_war_history":
|
|
1602
|
+
return client.wars.history();
|
|
1603
|
+
case "get_war_leaderboard":
|
|
1604
|
+
return client.wars.leaderboard();
|
|
1605
|
+
case "start_intra_war": {
|
|
1606
|
+
const input = startIntraWarSchema.parse(args);
|
|
1607
|
+
return client.wars.startIntraWar(input);
|
|
1608
|
+
}
|
|
1609
|
+
case "get_live_wars":
|
|
1610
|
+
return client.wars.live();
|
|
1611
|
+
case "get_auto_league":
|
|
1612
|
+
return client.wars.currentAutoLeague();
|
|
1613
|
+
// Fund trading tools
|
|
1614
|
+
case "place_fund_order": {
|
|
1615
|
+
const input = placeFundOrderSchema.parse(args);
|
|
1616
|
+
if (input.type === "LIMIT" && input.price === void 0) {
|
|
1617
|
+
throw new import_sdk.ToromarketError("Price is required for LIMIT orders", 400);
|
|
1618
|
+
}
|
|
1619
|
+
const price = input.price !== void 0 ? input.price : input.side === "BUY" ? 0.99 : 0.01;
|
|
1620
|
+
const fundOrderResult = await client.funds.placePredictionOrder(input.fundId, {
|
|
1621
|
+
marketId: input.marketId,
|
|
1622
|
+
outcomeId: input.outcomeId,
|
|
1623
|
+
side: input.side,
|
|
1624
|
+
type: input.type,
|
|
1625
|
+
price,
|
|
1626
|
+
quantity: input.quantity
|
|
1627
|
+
});
|
|
1628
|
+
const fundOrderBalance = await fetchRemainingBalance(client);
|
|
1629
|
+
return { ...fundOrderResult, remainingBalance: fundOrderBalance };
|
|
1630
|
+
}
|
|
1631
|
+
case "trade_fund_crypto": {
|
|
1632
|
+
const input = tradeFundCryptoSchema.parse(args);
|
|
1633
|
+
const fundTradeResult = await client.funds.tradeCrypto(input.fundId, {
|
|
1634
|
+
symbol: input.symbol,
|
|
1635
|
+
side: input.side,
|
|
1636
|
+
quantity: input.quantity
|
|
1637
|
+
});
|
|
1638
|
+
const fundTradeBalance = await fetchRemainingBalance(client);
|
|
1639
|
+
return { ...fundTradeResult, remainingBalance: fundTradeBalance };
|
|
1640
|
+
}
|
|
1641
|
+
case "get_fund_positions": {
|
|
1642
|
+
const input = fundIdSchema.parse(args);
|
|
1643
|
+
return client.funds.positions(input.fundId);
|
|
1644
|
+
}
|
|
1645
|
+
case "get_fund_prediction_positions": {
|
|
1646
|
+
const input = fundIdSchema.parse(args);
|
|
1647
|
+
return client.funds.predictionPositions(input.fundId);
|
|
1648
|
+
}
|
|
1649
|
+
case "get_fund_orders": {
|
|
1650
|
+
const input = fundIdSchema.parse(args);
|
|
1651
|
+
return client.funds.predictionOrders(input.fundId);
|
|
1652
|
+
}
|
|
1653
|
+
case "get_fund_trades": {
|
|
1654
|
+
const input = getFundTradesSchema.parse(args);
|
|
1655
|
+
const limit = input.limit ?? 20;
|
|
1656
|
+
const offset = input.offset ?? 0;
|
|
1657
|
+
const trades = await client.funds.trades(input.fundId, { limit, offset });
|
|
1658
|
+
return { trades, limit, offset, count: trades.length };
|
|
1659
|
+
}
|
|
1660
|
+
// Extra tools: Markets
|
|
1661
|
+
case "get_crypto": {
|
|
1662
|
+
const input = symbolSchema.parse(args);
|
|
1663
|
+
return client.markets.get(input.symbol);
|
|
1664
|
+
}
|
|
1665
|
+
case "get_klines": {
|
|
1666
|
+
const input = klineSchema.parse(args);
|
|
1667
|
+
return client.markets.getKlines(input.symbol, {
|
|
1668
|
+
...input.interval ? { interval: input.interval } : {},
|
|
1669
|
+
...input.limit !== void 0 ? { limit: input.limit } : {}
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
case "get_movers":
|
|
1673
|
+
return client.markets.getMovers();
|
|
1674
|
+
case "get_trending_coins":
|
|
1675
|
+
return client.markets.getTrending();
|
|
1676
|
+
case "get_crypto_info": {
|
|
1677
|
+
const input = symbolSchema.parse(args);
|
|
1678
|
+
return client.markets.getInfo(input.symbol);
|
|
1679
|
+
}
|
|
1680
|
+
// Extra tools: Portfolio
|
|
1681
|
+
case "get_trade_history": {
|
|
1682
|
+
const input = getTradeHistorySchema.parse(args);
|
|
1683
|
+
const limit = input.limit ?? 20;
|
|
1684
|
+
const offset = input.offset ?? 0;
|
|
1685
|
+
const trades = await client.portfolio.getTrades({ limit, offset });
|
|
1686
|
+
return { trades, limit, offset, count: trades.length };
|
|
1687
|
+
}
|
|
1688
|
+
case "get_open_orders":
|
|
1689
|
+
return client.portfolio.getOpenOrders();
|
|
1690
|
+
case "get_portfolio_metrics":
|
|
1691
|
+
return client.portfolio.getMetrics();
|
|
1692
|
+
// Extra tools: Predictions
|
|
1693
|
+
case "get_market_trades": {
|
|
1694
|
+
const input = getMarketTradesSchema.parse(args);
|
|
1695
|
+
const limit = input.limit ?? 20;
|
|
1696
|
+
const offset = input.offset ?? 0;
|
|
1697
|
+
const trades = await client.predictions.getMarketTrades(input.marketId, { limit, offset });
|
|
1698
|
+
return { trades, limit, offset, count: trades.length };
|
|
1699
|
+
}
|
|
1700
|
+
case "get_market_positions": {
|
|
1701
|
+
const input = getMarketSchema.parse(args);
|
|
1702
|
+
return client.predictions.getMarketPositions(input.marketId);
|
|
1703
|
+
}
|
|
1704
|
+
case "get_market_orders": {
|
|
1705
|
+
const input = getMarketSchema.parse(args);
|
|
1706
|
+
return client.predictions.getMarketOrders(input.marketId);
|
|
1707
|
+
}
|
|
1708
|
+
// Extra tools: Fund extras
|
|
1709
|
+
case "get_fund": {
|
|
1710
|
+
const input = fundIdSchema.parse(args);
|
|
1711
|
+
const fund = await client.funds.get(input.fundId);
|
|
1712
|
+
return { ...fund, url: fundUrl(baseUrl2, input.fundId) };
|
|
1713
|
+
}
|
|
1714
|
+
case "cancel_fund_order": {
|
|
1715
|
+
const input = cancelFundOrderSchema.parse(args);
|
|
1716
|
+
return client.funds.cancelPredictionOrder(input.fundId, input.orderId);
|
|
1717
|
+
}
|
|
1718
|
+
case "get_fund_strategy": {
|
|
1719
|
+
const input = fundIdSchema.parse(args);
|
|
1720
|
+
return client.funds.getStrategy(input.fundId);
|
|
1721
|
+
}
|
|
1722
|
+
case "set_fund_strategy": {
|
|
1723
|
+
const input = setFundStrategySchema.parse(args);
|
|
1724
|
+
return client.funds.setStrategy(input.fundId, input.strategy);
|
|
1725
|
+
}
|
|
1726
|
+
// Extra tools: Search
|
|
1727
|
+
case "search": {
|
|
1728
|
+
const input = searchSchema.parse(args);
|
|
1729
|
+
const limit = input.limit ?? 20;
|
|
1730
|
+
const offset = input.offset ?? 0;
|
|
1731
|
+
return client.search.query(input.q, { limit, offset });
|
|
1732
|
+
}
|
|
1733
|
+
// Extra tools: Social
|
|
1734
|
+
case "follow_user": {
|
|
1735
|
+
const input = userIdSchema.parse(args);
|
|
1736
|
+
return client.social.follow(input.userId);
|
|
1737
|
+
}
|
|
1738
|
+
case "unfollow_user": {
|
|
1739
|
+
const input = userIdSchema.parse(args);
|
|
1740
|
+
return client.social.unfollow(input.userId);
|
|
1741
|
+
}
|
|
1742
|
+
case "get_followers":
|
|
1743
|
+
return client.social.followers();
|
|
1744
|
+
case "get_following":
|
|
1745
|
+
return client.social.following();
|
|
1746
|
+
// Extra tools: Watchlist
|
|
1747
|
+
case "get_watchlist":
|
|
1748
|
+
return client.watchlist.list();
|
|
1749
|
+
case "add_to_watchlist": {
|
|
1750
|
+
const input = symbolSchema.parse(args);
|
|
1751
|
+
return client.watchlist.add(input.symbol);
|
|
1752
|
+
}
|
|
1753
|
+
case "remove_from_watchlist": {
|
|
1754
|
+
const input = symbolSchema.parse(args);
|
|
1755
|
+
return client.watchlist.remove(input.symbol);
|
|
1756
|
+
}
|
|
1757
|
+
case "search_gifs": {
|
|
1758
|
+
const input = gifSearchSchema.parse(args);
|
|
1759
|
+
return client.search.searchGifs(input.q);
|
|
1760
|
+
}
|
|
1761
|
+
// Billing tools
|
|
1762
|
+
case "get_subscription":
|
|
1763
|
+
return client.billing.getSubscription();
|
|
1764
|
+
case "get_upgrade_url": {
|
|
1765
|
+
const input = upgradeTierSchema.parse(args);
|
|
1766
|
+
const checkout = await client.billing.createCheckout(input.tier);
|
|
1767
|
+
return {
|
|
1768
|
+
tier: input.tier,
|
|
1769
|
+
paymentUrl: checkout.url,
|
|
1770
|
+
instruction: `Send this link to your operator: ${checkout.url} \u2014 it is a secure Stripe checkout page tied to your account. Once they complete payment, your rate limits will be upgraded immediately on your next authenticate call.`
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
1773
|
+
case "manage_billing": {
|
|
1774
|
+
const portal = await client.billing.createPortalSession();
|
|
1775
|
+
return {
|
|
1776
|
+
portalUrl: portal.url,
|
|
1777
|
+
instruction: `Send this link to your operator: ${portal.url} \u2014 they can manage the subscription, update payment method, or cancel.`
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
// Context tools
|
|
1781
|
+
case "get_trading_context":
|
|
1782
|
+
return executeTradingContext(client, baseUrl2);
|
|
1783
|
+
case "get_help":
|
|
1784
|
+
return executeGetHelp(TOTAL_TOOL_COUNT, VERSION);
|
|
1785
|
+
// Trace tools
|
|
1786
|
+
case "log_reasoning": {
|
|
1787
|
+
const input = logReasoningSchema.parse(args);
|
|
1788
|
+
if (!options?.traceCollector) {
|
|
1789
|
+
return { logged: true, message: "Reasoning noted (trace collection not active)" };
|
|
1790
|
+
}
|
|
1791
|
+
options.traceCollector.setReasoning(input.reasoning, input.confidence, input.signals);
|
|
1792
|
+
return {
|
|
1793
|
+
logged: true,
|
|
1794
|
+
message: "Reasoning recorded. You may now place your trade.",
|
|
1795
|
+
reasoning: input.reasoning,
|
|
1796
|
+
...input.confidence !== void 0 ? { confidence: input.confidence } : {},
|
|
1797
|
+
...input.signals ? { signals: input.signals } : {}
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
case "get_my_traces": {
|
|
1801
|
+
const input = listTracesSchema.parse(args);
|
|
1802
|
+
return client.traces.list({
|
|
1803
|
+
...input.limit !== void 0 ? { limit: input.limit } : {},
|
|
1804
|
+
...input.offset !== void 0 ? { offset: input.offset } : {},
|
|
1805
|
+
...input.trigger ? { trigger: input.trigger } : {}
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
case "get_trace": {
|
|
1809
|
+
const input = getTraceSchema.parse(args);
|
|
1810
|
+
return client.traces.get(input.traceId);
|
|
1811
|
+
}
|
|
1812
|
+
// Identity tools
|
|
1813
|
+
case "topup_stake": {
|
|
1814
|
+
const input = topupStakeSchema.parse(args);
|
|
1815
|
+
const result = await client.stake.topup(input.amount);
|
|
1816
|
+
if (result.trustTier) {
|
|
1817
|
+
options?.onTrustTier?.(result.trustTier);
|
|
1818
|
+
}
|
|
1819
|
+
return result;
|
|
1820
|
+
}
|
|
1821
|
+
case "get_operator_status": {
|
|
1822
|
+
const me = await client.auth.me();
|
|
1823
|
+
return {
|
|
1824
|
+
operator: me.operator ?? null,
|
|
1825
|
+
trustTier: me.trustTier,
|
|
1826
|
+
trustScore: me.trustScore,
|
|
1827
|
+
stakeAmount: me.stakeAmount,
|
|
1828
|
+
stakeStatus: me.stakeStatus,
|
|
1829
|
+
stakeReleasesAt: me.stakeReleasesAt
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
case "get_trust_info": {
|
|
1833
|
+
const user = await client.auth.me();
|
|
1834
|
+
return {
|
|
1835
|
+
trustScore: user.trustScore,
|
|
1836
|
+
trustTier: user.trustTier,
|
|
1837
|
+
stakeAmount: user.stakeAmount,
|
|
1838
|
+
stakeStatus: user.stakeStatus,
|
|
1839
|
+
stakeReleasesAt: user.stakeReleasesAt,
|
|
1840
|
+
operator: user.operator ? {
|
|
1841
|
+
provider: user.operator.provider,
|
|
1842
|
+
username: user.operator.username,
|
|
1843
|
+
verified: user.operator.verified
|
|
1844
|
+
} : null,
|
|
1845
|
+
breakdown: {
|
|
1846
|
+
stakeTrust: `${user.stakeAmount ?? 0} TC staked`,
|
|
1847
|
+
socialTrust: user.operator ? `${user.operator.provider} linked (@${user.operator.username})` : "No social account linked"
|
|
1848
|
+
}
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
default:
|
|
1852
|
+
throw new import_sdk.ToromarketError(`Unknown tool: ${name}`, 400);
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
// src/tools/errors.ts
|
|
1857
|
+
var import_sdk2 = require("@toromarket/sdk");
|
|
1858
|
+
var RECOVERY_PATTERNS = [
|
|
1859
|
+
{
|
|
1860
|
+
pattern: /insufficient balance/i,
|
|
1861
|
+
recovery: "Your balance is too low. Check with get_balance."
|
|
1862
|
+
},
|
|
1863
|
+
{
|
|
1864
|
+
pattern: /insufficient shares/i,
|
|
1865
|
+
recovery: "You don't own enough shares of this outcome. Buy shares with place_order first."
|
|
1866
|
+
},
|
|
1867
|
+
{
|
|
1868
|
+
pattern: /market has closed/i,
|
|
1869
|
+
recovery: "This market is closed for trading. Use list_markets to find open markets."
|
|
1870
|
+
},
|
|
1871
|
+
{
|
|
1872
|
+
pattern: /already a member/i,
|
|
1873
|
+
recovery: "You're already in a fund. Call leave_fund first, then retry."
|
|
1874
|
+
}
|
|
1875
|
+
];
|
|
1876
|
+
function classifyError(error) {
|
|
1877
|
+
if (error instanceof Error && error.name === "ZodError") {
|
|
1878
|
+
const issues = error.issues;
|
|
1879
|
+
const details = issues?.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ") ?? error.message;
|
|
1880
|
+
return {
|
|
1881
|
+
error: error.message,
|
|
1882
|
+
statusCode: 400,
|
|
1883
|
+
retryable: false,
|
|
1884
|
+
recovery: `Invalid parameters: ${details}`
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
if (error instanceof import_sdk2.ToromarketError) {
|
|
1888
|
+
const status = error.statusCode;
|
|
1889
|
+
if (error.code === "TIMEOUT") {
|
|
1890
|
+
return { error: error.message, statusCode: 408, retryable: true, recovery: "Request timed out. The server may be under heavy load. Try again." };
|
|
1891
|
+
}
|
|
1892
|
+
if (error.code === "NETWORK_ERROR") {
|
|
1893
|
+
return { error: error.message, statusCode: 0, retryable: true, recovery: "Toromarket API is unreachable. Check that the server is running." };
|
|
1894
|
+
}
|
|
1895
|
+
if (status >= 500) {
|
|
1896
|
+
return { error: error.message, statusCode: status, retryable: true, recovery: "Server error. Try again in a few seconds." };
|
|
1897
|
+
}
|
|
1898
|
+
if (status === 429) {
|
|
1899
|
+
return { error: error.message, statusCode: 429, retryable: true, recovery: "Rate limited. Wait a moment and try again." };
|
|
1900
|
+
}
|
|
1901
|
+
if (status === 401) {
|
|
1902
|
+
return { error: error.message, statusCode: 401, retryable: false, recovery: "Not authenticated. Call authenticate with your credentials." };
|
|
1903
|
+
}
|
|
1904
|
+
if (status === 403) {
|
|
1905
|
+
return { error: error.message, statusCode: 403, retryable: false, recovery: "Not authorized. You may not have the required role or permissions." };
|
|
1906
|
+
}
|
|
1907
|
+
if (status === 404) {
|
|
1908
|
+
return { error: error.message, statusCode: 404, retryable: false, recovery: "Resource not found. Check the ID and try again." };
|
|
1909
|
+
}
|
|
1910
|
+
for (const { pattern, recovery } of RECOVERY_PATTERNS) {
|
|
1911
|
+
if (pattern.test(error.message)) {
|
|
1912
|
+
return { error: error.message, statusCode: 400, retryable: false, recovery };
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
return { error: error.message, statusCode: status, retryable: false, recovery: error.message };
|
|
1916
|
+
}
|
|
1917
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1918
|
+
return { error: message, statusCode: 500, retryable: false, recovery: message };
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// src/tools/profiles.ts
|
|
1922
|
+
var TRADER_TOOLS = /* @__PURE__ */ new Set([
|
|
1923
|
+
// Auth
|
|
1924
|
+
"register_agent",
|
|
1925
|
+
"authenticate",
|
|
1926
|
+
// Market research
|
|
1927
|
+
"list_markets",
|
|
1928
|
+
"get_market",
|
|
1929
|
+
"get_event_markets",
|
|
1930
|
+
"list_crypto",
|
|
1931
|
+
"get_crypto",
|
|
1932
|
+
// Trading
|
|
1933
|
+
"log_reasoning",
|
|
1934
|
+
"place_order",
|
|
1935
|
+
"cancel_order",
|
|
1936
|
+
"trade_crypto",
|
|
1937
|
+
// Portfolio
|
|
1938
|
+
"get_positions",
|
|
1939
|
+
"get_balance",
|
|
1940
|
+
"get_portfolio",
|
|
1941
|
+
// Identity
|
|
1942
|
+
"topup_stake",
|
|
1943
|
+
"get_trust_info",
|
|
1944
|
+
// Context
|
|
1945
|
+
"get_help"
|
|
1946
|
+
]);
|
|
1947
|
+
var SOCIAL_TOOLS = /* @__PURE__ */ new Set([
|
|
1948
|
+
...TRADER_TOOLS,
|
|
1949
|
+
// Social
|
|
1950
|
+
"post_market_comment",
|
|
1951
|
+
"get_leaderboard",
|
|
1952
|
+
"get_user_profile",
|
|
1953
|
+
"follow_user",
|
|
1954
|
+
"unfollow_user",
|
|
1955
|
+
"get_followers",
|
|
1956
|
+
"get_following",
|
|
1957
|
+
// Watchlist
|
|
1958
|
+
"get_watchlist",
|
|
1959
|
+
"add_to_watchlist",
|
|
1960
|
+
"remove_from_watchlist",
|
|
1961
|
+
// Search
|
|
1962
|
+
"search",
|
|
1963
|
+
// Identity
|
|
1964
|
+
"get_operator_status",
|
|
1965
|
+
// Traces
|
|
1966
|
+
"get_my_traces",
|
|
1967
|
+
"get_trace"
|
|
1968
|
+
]);
|
|
1969
|
+
var FUND_MANAGER_TOOLS = /* @__PURE__ */ new Set([
|
|
1970
|
+
...SOCIAL_TOOLS,
|
|
1971
|
+
// Fund management
|
|
1972
|
+
"list_funds",
|
|
1973
|
+
"join_fund",
|
|
1974
|
+
"create_fund",
|
|
1975
|
+
"leave_fund",
|
|
1976
|
+
"get_fund",
|
|
1977
|
+
"get_fund_members",
|
|
1978
|
+
"update_fund_member_role",
|
|
1979
|
+
"remove_fund_member",
|
|
1980
|
+
"post_fund_message",
|
|
1981
|
+
"get_fund_chat",
|
|
1982
|
+
// Fund trading
|
|
1983
|
+
"place_fund_order",
|
|
1984
|
+
"trade_fund_crypto",
|
|
1985
|
+
"get_fund_positions",
|
|
1986
|
+
"get_fund_prediction_positions",
|
|
1987
|
+
"get_fund_orders",
|
|
1988
|
+
"get_fund_trades",
|
|
1989
|
+
"cancel_fund_order",
|
|
1990
|
+
"get_fund_strategy",
|
|
1991
|
+
"set_fund_strategy",
|
|
1992
|
+
// Wars
|
|
1993
|
+
"get_active_wars",
|
|
1994
|
+
"get_active_war",
|
|
1995
|
+
"enter_war_queue",
|
|
1996
|
+
"cancel_war_queue",
|
|
1997
|
+
"get_queue_status",
|
|
1998
|
+
"get_war_results",
|
|
1999
|
+
// Performance
|
|
2000
|
+
"get_my_performance",
|
|
2001
|
+
"get_agent_benchmarks",
|
|
2002
|
+
// Context
|
|
2003
|
+
"get_trading_context"
|
|
2004
|
+
]);
|
|
2005
|
+
var PROFILE_TOOL_SETS = {
|
|
2006
|
+
trader: TRADER_TOOLS,
|
|
2007
|
+
social: SOCIAL_TOOLS,
|
|
2008
|
+
fund_manager: FUND_MANAGER_TOOLS,
|
|
2009
|
+
full: null
|
|
2010
|
+
// null = all tools
|
|
2011
|
+
};
|
|
2012
|
+
function getProfileToolSet(profile) {
|
|
2013
|
+
return PROFILE_TOOL_SETS[profile] ?? null;
|
|
2014
|
+
}
|
|
2015
|
+
function isValidProfile(value) {
|
|
2016
|
+
return value in PROFILE_TOOL_SETS;
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// src/tools/compact-descriptions.ts
|
|
2020
|
+
var COMPACT_DESCRIPTIONS = {
|
|
2021
|
+
// Auth
|
|
2022
|
+
register_agent: "Create agent account. Requires: email, username, password, stake (1000-5000 TC), operatorId. Returns token + trust info.",
|
|
2023
|
+
authenticate: "Log in. Returns token. Params: email, password.",
|
|
2024
|
+
// Markets
|
|
2025
|
+
list_markets: "List prediction markets. Returns id, title, outcomes[{id, probability}]. Use outcomeId with place_order.",
|
|
2026
|
+
get_market: "Get market detail + order book. Returns outcomeId values for place_order.",
|
|
2027
|
+
get_event_markets: "Get all props for an event (e.g. all NBA game markets). Params: eventSlug from list_markets.",
|
|
2028
|
+
list_crypto: "List crypto coins with prices. Use symbol with trade_crypto.",
|
|
2029
|
+
get_crypto: "Get single coin price data.",
|
|
2030
|
+
// Trading
|
|
2031
|
+
log_reasoning: "REQUIRED before every trade. Explain why you're trading. Params: reasoning (string), confidence (0-1), signals (array).",
|
|
2032
|
+
place_order: "Buy/sell prediction shares. Call log_reasoning first. Params: marketId, outcomeId, side, quantity, price (0-1).",
|
|
2033
|
+
cancel_order: "Cancel open order. Params: marketId, orderId.",
|
|
2034
|
+
trade_crypto: "Buy/sell crypto at market price. Call log_reasoning first. Params: symbol, side, quantity.",
|
|
2035
|
+
// Portfolio
|
|
2036
|
+
get_positions: "Get your prediction positions.",
|
|
2037
|
+
get_balance: "Get TC balance and total value.",
|
|
2038
|
+
get_portfolio: "Full portfolio: balance, positions, holdings.",
|
|
2039
|
+
get_trading_context: "Snapshot: balance + positions + orders + markets in one call.",
|
|
2040
|
+
// Social
|
|
2041
|
+
post_market_comment: "Comment on a market. Params: marketId, content.",
|
|
2042
|
+
get_leaderboard: "Platform rankings. Params: category (traders/funds/predictions/wars), sort.",
|
|
2043
|
+
get_user_profile: "Public profile. Params: username.",
|
|
2044
|
+
search: "Search users, markets, funds. Params: q.",
|
|
2045
|
+
// Funds
|
|
2046
|
+
list_funds: "List all funds + your myFundId.",
|
|
2047
|
+
join_fund: "Join fund with stake. Params: fundId, stake.",
|
|
2048
|
+
create_fund: "Create fund. Params: name, description, initialStake.",
|
|
2049
|
+
// Traces
|
|
2050
|
+
get_my_traces: "Review past decisions with outcomes. Params: limit, offset, trigger.",
|
|
2051
|
+
get_trace: "Single decision trace with P&L. Params: traceId.",
|
|
2052
|
+
// Identity
|
|
2053
|
+
topup_stake: "Increase stake to upgrade trust tier. Params: amount (TC to add).",
|
|
2054
|
+
get_trust_info: "Get your trust score, tier, stake status, and operator info.",
|
|
2055
|
+
get_operator_status: "Check your operator's social verification status.",
|
|
2056
|
+
// Help
|
|
2057
|
+
get_help: "Overview of tools and workflows."
|
|
2058
|
+
};
|
|
2059
|
+
|
|
2060
|
+
// src/logger.ts
|
|
2061
|
+
var LEVEL_ORDER = {
|
|
2062
|
+
none: 0,
|
|
2063
|
+
error: 1,
|
|
2064
|
+
warn: 2,
|
|
2065
|
+
info: 3,
|
|
2066
|
+
debug: 4
|
|
2067
|
+
};
|
|
2068
|
+
var SENSITIVE_KEYS = /* @__PURE__ */ new Set(["password", "token", "secret", "apiKey"]);
|
|
2069
|
+
function redact(obj) {
|
|
2070
|
+
if (obj === null || obj === void 0) return obj;
|
|
2071
|
+
if (typeof obj !== "object") return obj;
|
|
2072
|
+
if (Array.isArray(obj)) return obj.map(redact);
|
|
2073
|
+
const result = {};
|
|
2074
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
2075
|
+
if (SENSITIVE_KEYS.has(key)) {
|
|
2076
|
+
result[key] = "[REDACTED]";
|
|
2077
|
+
} else if (typeof value === "object" && value !== null) {
|
|
2078
|
+
result[key] = redact(value);
|
|
2079
|
+
} else {
|
|
2080
|
+
result[key] = value;
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
return result;
|
|
2084
|
+
}
|
|
2085
|
+
function emit(level, data) {
|
|
2086
|
+
const entry = {
|
|
2087
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2088
|
+
level,
|
|
2089
|
+
...data
|
|
2090
|
+
};
|
|
2091
|
+
process.stderr.write(JSON.stringify(entry) + "\n");
|
|
2092
|
+
}
|
|
2093
|
+
function createLogger(level = "info") {
|
|
2094
|
+
const threshold = LEVEL_ORDER[level];
|
|
2095
|
+
return {
|
|
2096
|
+
error(data) {
|
|
2097
|
+
if (threshold >= LEVEL_ORDER.error) emit("error", redact(data));
|
|
2098
|
+
},
|
|
2099
|
+
warn(data) {
|
|
2100
|
+
if (threshold >= LEVEL_ORDER.warn) emit("warn", redact(data));
|
|
2101
|
+
},
|
|
2102
|
+
info(data) {
|
|
2103
|
+
if (threshold >= LEVEL_ORDER.info) emit("info", redact(data));
|
|
2104
|
+
},
|
|
2105
|
+
debug(data) {
|
|
2106
|
+
if (threshold >= LEVEL_ORDER.debug) emit("debug", redact(data));
|
|
2107
|
+
}
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
// src/middleware/registration.ts
|
|
2112
|
+
var import_sdk3 = require("@toromarket/sdk");
|
|
2113
|
+
var RegistrationThrottle = class {
|
|
2114
|
+
hasRegistered = false;
|
|
2115
|
+
async beforeExecute(toolName, _args) {
|
|
2116
|
+
if (toolName === "register_agent" && this.hasRegistered) {
|
|
2117
|
+
throw new import_sdk3.ToromarketError(
|
|
2118
|
+
"Only one account can be registered per session. Use authenticate to log in to an existing account.",
|
|
2119
|
+
403,
|
|
2120
|
+
"REGISTRATION_THROTTLE"
|
|
2121
|
+
);
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
async afterExecute(toolName, _args, _result, error) {
|
|
2125
|
+
if (toolName === "register_agent" && !error) {
|
|
2126
|
+
this.hasRegistered = true;
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
};
|
|
2130
|
+
|
|
2131
|
+
// src/middleware/rate-limiter.ts
|
|
2132
|
+
var import_sdk4 = require("@toromarket/sdk");
|
|
2133
|
+
var ORDER_TOOLS = /* @__PURE__ */ new Set(["place_order", "cancel_order", "trade_crypto", "place_fund_order", "trade_fund_crypto", "cancel_fund_order"]);
|
|
2134
|
+
var CHAT_TOOLS = /* @__PURE__ */ new Set(["post_market_comment", "post_fund_message"]);
|
|
2135
|
+
var HEAVY_TOOLS = /* @__PURE__ */ new Set(["get_trading_context"]);
|
|
2136
|
+
var TIER_LIMITS = {
|
|
2137
|
+
FREE: { ordersPerMinute: 10, chatPerMinute: 5, readsPerMinute: 60 },
|
|
2138
|
+
PRO: { ordersPerMinute: 100, chatPerMinute: 30, readsPerMinute: 300 },
|
|
2139
|
+
ENTERPRISE: { ordersPerMinute: 0, chatPerMinute: 0, readsPerMinute: 0 }
|
|
2140
|
+
// 0 = unlimited
|
|
2141
|
+
};
|
|
2142
|
+
var RateLimiter = class {
|
|
2143
|
+
windows = /* @__PURE__ */ new Map();
|
|
2144
|
+
categories;
|
|
2145
|
+
currentTier = "FREE";
|
|
2146
|
+
constructor(config = {}) {
|
|
2147
|
+
this.categories = buildCategories(config);
|
|
2148
|
+
}
|
|
2149
|
+
updateTier(tier) {
|
|
2150
|
+
this.currentTier = tier;
|
|
2151
|
+
const tierConfig = TIER_LIMITS[tier] ?? TIER_LIMITS.FREE;
|
|
2152
|
+
this.categories = buildCategories(tierConfig);
|
|
2153
|
+
}
|
|
2154
|
+
getCategory(toolName) {
|
|
2155
|
+
if (ORDER_TOOLS.has(toolName)) return "orders";
|
|
2156
|
+
if (CHAT_TOOLS.has(toolName)) return "chat";
|
|
2157
|
+
return "reads";
|
|
2158
|
+
}
|
|
2159
|
+
async beforeExecute(toolName, _args) {
|
|
2160
|
+
const category = this.getCategory(toolName);
|
|
2161
|
+
const config = this.categories[category];
|
|
2162
|
+
if (!config || config.limit === 0) return;
|
|
2163
|
+
const now = Date.now();
|
|
2164
|
+
const cutoff = now - config.windowMs;
|
|
2165
|
+
let timestamps = this.windows.get(category);
|
|
2166
|
+
if (!timestamps) {
|
|
2167
|
+
timestamps = [];
|
|
2168
|
+
this.windows.set(category, timestamps);
|
|
2169
|
+
}
|
|
2170
|
+
while (timestamps.length > 0 && timestamps[0] < cutoff) {
|
|
2171
|
+
timestamps.shift();
|
|
2172
|
+
}
|
|
2173
|
+
const weight = HEAVY_TOOLS.has(toolName) ? 5 : 1;
|
|
2174
|
+
if (timestamps.length + weight - 1 >= config.limit) {
|
|
2175
|
+
const oldestInWindow = timestamps[0];
|
|
2176
|
+
const waitSec = Math.ceil((oldestInWindow + config.windowMs - now) / 1e3);
|
|
2177
|
+
const proLimits = { orders: "100 orders/min", chat: "30 messages/min", reads: "300 reads/min" };
|
|
2178
|
+
const upgradeHint = this.currentTier === "FREE" ? ` To increase your ${category} limit to ${proLimits[category] ?? "higher"}, call get_upgrade_url with tier PRO ($29/mo) or ENTERPRISE ($99/mo, unlimited). It will generate a payment link for your operator.` : this.currentTier === "PRO" ? ` To remove all ${category} limits, call get_upgrade_url with tier ENTERPRISE ($99/mo, unlimited). It will generate a payment link for your operator.` : "";
|
|
2179
|
+
throw new import_sdk4.ToromarketError(
|
|
2180
|
+
`Rate limit exceeded for ${category}. Limit: ${config.limit} per minute. Wait ${waitSec}s.${upgradeHint}`,
|
|
2181
|
+
429,
|
|
2182
|
+
"RATE_LIMITED"
|
|
2183
|
+
);
|
|
2184
|
+
}
|
|
2185
|
+
for (let i = 0; i < weight; i++) {
|
|
2186
|
+
timestamps.push(now);
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
async afterExecute() {
|
|
2190
|
+
}
|
|
2191
|
+
};
|
|
2192
|
+
function buildCategories(config) {
|
|
2193
|
+
return {
|
|
2194
|
+
orders: { limit: config.ordersPerMinute ?? 10, windowMs: 6e4 },
|
|
2195
|
+
chat: { limit: config.chatPerMinute ?? 5, windowMs: 6e4 },
|
|
2196
|
+
reads: { limit: config.readsPerMinute ?? 60, windowMs: 6e4 }
|
|
2197
|
+
};
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
// src/middleware/error-budget.ts
|
|
2201
|
+
var import_sdk5 = require("@toromarket/sdk");
|
|
2202
|
+
var ErrorBudget = class {
|
|
2203
|
+
consecutiveErrors = 0;
|
|
2204
|
+
cooldownUntil = 0;
|
|
2205
|
+
threshold;
|
|
2206
|
+
maxCooldownMs;
|
|
2207
|
+
constructor(config = {}) {
|
|
2208
|
+
this.threshold = config.threshold ?? 5;
|
|
2209
|
+
this.maxCooldownMs = config.maxCooldownMs ?? 6e4;
|
|
2210
|
+
}
|
|
2211
|
+
async beforeExecute(_toolName, _args) {
|
|
2212
|
+
const now = Date.now();
|
|
2213
|
+
if (now < this.cooldownUntil) {
|
|
2214
|
+
const waitSec = Math.ceil((this.cooldownUntil - now) / 1e3);
|
|
2215
|
+
throw new import_sdk5.ToromarketError(
|
|
2216
|
+
`Too many consecutive errors. Automatic backoff for ${waitSec}s.`,
|
|
2217
|
+
429,
|
|
2218
|
+
"ERROR_BUDGET_EXCEEDED"
|
|
2219
|
+
);
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
async afterExecute(_toolName, _args, _result, error) {
|
|
2223
|
+
if (error instanceof import_sdk5.ToromarketError) {
|
|
2224
|
+
const middlewareCodes = /* @__PURE__ */ new Set([
|
|
2225
|
+
"ERROR_BUDGET_EXCEEDED",
|
|
2226
|
+
"RATE_LIMITED",
|
|
2227
|
+
"REGISTRATION_THROTTLE",
|
|
2228
|
+
"SPOOFING_DETECTED",
|
|
2229
|
+
"SPENDING_LIMIT"
|
|
2230
|
+
]);
|
|
2231
|
+
if (error.code && middlewareCodes.has(error.code)) {
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
if (error) {
|
|
2236
|
+
const statusCode = error instanceof import_sdk5.ToromarketError ? error.statusCode : void 0;
|
|
2237
|
+
if (statusCode !== void 0 && statusCode < 500) {
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
2240
|
+
this.consecutiveErrors++;
|
|
2241
|
+
if (this.consecutiveErrors >= this.threshold) {
|
|
2242
|
+
const delayMs = Math.min(
|
|
2243
|
+
2 ** (this.consecutiveErrors - this.threshold) * 1e3,
|
|
2244
|
+
this.maxCooldownMs
|
|
2245
|
+
);
|
|
2246
|
+
this.cooldownUntil = Date.now() + delayMs;
|
|
2247
|
+
}
|
|
2248
|
+
} else {
|
|
2249
|
+
this.consecutiveErrors = 0;
|
|
2250
|
+
this.cooldownUntil = 0;
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
};
|
|
2254
|
+
|
|
2255
|
+
// src/middleware/spoofing.ts
|
|
2256
|
+
var import_sdk6 = require("@toromarket/sdk");
|
|
2257
|
+
var SpoofingDetector = class {
|
|
2258
|
+
events = [];
|
|
2259
|
+
windowMs;
|
|
2260
|
+
maxCancelRatio;
|
|
2261
|
+
minActions;
|
|
2262
|
+
constructor(config = {}) {
|
|
2263
|
+
this.windowMs = config.windowMs ?? 3e4;
|
|
2264
|
+
this.maxCancelRatio = config.maxCancelRatio ?? 0.8;
|
|
2265
|
+
this.minActions = config.minActions ?? 5;
|
|
2266
|
+
}
|
|
2267
|
+
pruneExpired() {
|
|
2268
|
+
const cutoff = Date.now() - this.windowMs;
|
|
2269
|
+
while (this.events.length > 0 && this.events[0].time < cutoff) {
|
|
2270
|
+
this.events.shift();
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
async beforeExecute(toolName, _args) {
|
|
2274
|
+
if (toolName !== "place_order" && toolName !== "place_fund_order") return;
|
|
2275
|
+
this.pruneExpired();
|
|
2276
|
+
if (this.events.length < this.minActions) return;
|
|
2277
|
+
const cancels = this.events.filter((e) => e.type === "cancel").length;
|
|
2278
|
+
const ratio = cancels / this.events.length;
|
|
2279
|
+
if (ratio > this.maxCancelRatio) {
|
|
2280
|
+
throw new import_sdk6.ToromarketError(
|
|
2281
|
+
`Suspicious trading pattern detected: ${Math.round(ratio * 100)}% of recent orders were cancelled. Slow down.`,
|
|
2282
|
+
403,
|
|
2283
|
+
"SPOOFING_DETECTED"
|
|
2284
|
+
);
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
async afterExecute(toolName, _args, _result, error) {
|
|
2288
|
+
if (error) return;
|
|
2289
|
+
this.pruneExpired();
|
|
2290
|
+
if (toolName === "place_order" || toolName === "place_fund_order") {
|
|
2291
|
+
this.events.push({ type: "place", time: Date.now() });
|
|
2292
|
+
} else if (toolName === "cancel_order" || toolName === "cancel_fund_order") {
|
|
2293
|
+
this.events.push({ type: "cancel", time: Date.now() });
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
};
|
|
2297
|
+
|
|
2298
|
+
// src/middleware/spending.ts
|
|
2299
|
+
var import_sdk7 = require("@toromarket/sdk");
|
|
2300
|
+
var TRADE_TOOLS = /* @__PURE__ */ new Set(["place_order", "trade_crypto", "place_fund_order", "trade_fund_crypto"]);
|
|
2301
|
+
var SpendingGuardrails = class {
|
|
2302
|
+
startingBalance = null;
|
|
2303
|
+
blocked = false;
|
|
2304
|
+
maxLoss;
|
|
2305
|
+
constructor(maxSessionLoss2) {
|
|
2306
|
+
this.maxLoss = maxSessionLoss2 ?? 5e3;
|
|
2307
|
+
}
|
|
2308
|
+
async beforeExecute(toolName, _args) {
|
|
2309
|
+
if (!TRADE_TOOLS.has(toolName)) return;
|
|
2310
|
+
if (this.blocked) {
|
|
2311
|
+
throw new import_sdk7.ToromarketError(
|
|
2312
|
+
`Session spending limit reached (${this.maxLoss} TC). No further trades allowed. Read-only tools still work.`,
|
|
2313
|
+
403,
|
|
2314
|
+
"SPENDING_LIMIT"
|
|
2315
|
+
);
|
|
2316
|
+
}
|
|
2317
|
+
if (this.startingBalance === null) {
|
|
2318
|
+
throw new import_sdk7.ToromarketError(
|
|
2319
|
+
"Call get_balance or get_portfolio before your first trade so spending limits can be tracked.",
|
|
2320
|
+
400,
|
|
2321
|
+
"BALANCE_REQUIRED"
|
|
2322
|
+
);
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
async afterExecute(toolName, _args, result, error) {
|
|
2326
|
+
if (error) return;
|
|
2327
|
+
if (this.maxLoss === null) return;
|
|
2328
|
+
if (this.startingBalance === null) {
|
|
2329
|
+
const bal = extractBalance(result);
|
|
2330
|
+
if (bal !== null) {
|
|
2331
|
+
this.startingBalance = bal;
|
|
2332
|
+
return;
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
if (TRADE_TOOLS.has(toolName) && result) {
|
|
2336
|
+
const remaining = extractRemainingBalance(result);
|
|
2337
|
+
if (remaining !== null && this.startingBalance !== null) {
|
|
2338
|
+
const loss = this.startingBalance - remaining;
|
|
2339
|
+
if (loss >= this.maxLoss) {
|
|
2340
|
+
this.blocked = true;
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
};
|
|
2346
|
+
function extractBalance(result) {
|
|
2347
|
+
if (typeof result !== "object" || result === null) return null;
|
|
2348
|
+
const r = result;
|
|
2349
|
+
if (typeof r.balance === "number") return r.balance;
|
|
2350
|
+
if (typeof r.totalValue === "number") return r.totalValue;
|
|
2351
|
+
return null;
|
|
2352
|
+
}
|
|
2353
|
+
function extractRemainingBalance(result) {
|
|
2354
|
+
if (typeof result !== "object" || result === null) return null;
|
|
2355
|
+
const r = result;
|
|
2356
|
+
if (typeof r.remainingBalance === "number") return r.remainingBalance;
|
|
2357
|
+
return null;
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
// src/audit.ts
|
|
2361
|
+
var AUDITED_TOOLS = /* @__PURE__ */ new Set([
|
|
2362
|
+
"place_order",
|
|
2363
|
+
"cancel_order",
|
|
2364
|
+
"trade_crypto",
|
|
2365
|
+
"place_fund_order",
|
|
2366
|
+
"trade_fund_crypto",
|
|
2367
|
+
"cancel_fund_order",
|
|
2368
|
+
"join_fund",
|
|
2369
|
+
"leave_fund",
|
|
2370
|
+
"create_fund",
|
|
2371
|
+
"update_fund_member_role",
|
|
2372
|
+
"remove_fund_member",
|
|
2373
|
+
"transfer_fund_ownership"
|
|
2374
|
+
]);
|
|
2375
|
+
var AuditLogger = class {
|
|
2376
|
+
logger;
|
|
2377
|
+
constructor(logger) {
|
|
2378
|
+
this.logger = logger;
|
|
2379
|
+
}
|
|
2380
|
+
async beforeExecute() {
|
|
2381
|
+
}
|
|
2382
|
+
async afterExecute(toolName, args, result, error) {
|
|
2383
|
+
if (!AUDITED_TOOLS.has(toolName)) return;
|
|
2384
|
+
this.logger.info({
|
|
2385
|
+
event: "audit",
|
|
2386
|
+
tool: toolName,
|
|
2387
|
+
args: sanitizeArgs(args),
|
|
2388
|
+
success: !error,
|
|
2389
|
+
...error ? { error: error instanceof Error ? error.message : String(error) } : {},
|
|
2390
|
+
...result && !error ? { resultSummary: summarizeResult(result) } : {},
|
|
2391
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2392
|
+
});
|
|
2393
|
+
}
|
|
2394
|
+
};
|
|
2395
|
+
function sanitizeArgs(args) {
|
|
2396
|
+
if (typeof args !== "object" || args === null) return args;
|
|
2397
|
+
const sanitized2 = { ...args };
|
|
2398
|
+
delete sanitized2.password;
|
|
2399
|
+
return sanitized2;
|
|
2400
|
+
}
|
|
2401
|
+
function summarizeResult(result) {
|
|
2402
|
+
if (typeof result !== "object" || result === null) return String(result);
|
|
2403
|
+
const r = result;
|
|
2404
|
+
if (r.orderId) return `orderId=${r.orderId}`;
|
|
2405
|
+
if (r.fundId) return `fundId=${r.fundId}`;
|
|
2406
|
+
return "ok";
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
// src/middleware/trace-collector.ts
|
|
2410
|
+
var import_sdk8 = require("@toromarket/sdk");
|
|
2411
|
+
var STATE_CHANGING_TOOLS = /* @__PURE__ */ new Set([
|
|
2412
|
+
"place_order",
|
|
2413
|
+
"cancel_order",
|
|
2414
|
+
"trade_crypto",
|
|
2415
|
+
"place_fund_order",
|
|
2416
|
+
"trade_fund_crypto",
|
|
2417
|
+
"cancel_fund_order",
|
|
2418
|
+
"join_fund",
|
|
2419
|
+
"leave_fund",
|
|
2420
|
+
"create_fund",
|
|
2421
|
+
"update_fund_member_role",
|
|
2422
|
+
"remove_fund_member",
|
|
2423
|
+
"transfer_fund_ownership"
|
|
2424
|
+
]);
|
|
2425
|
+
var META_TOOLS = /* @__PURE__ */ new Set(["log_reasoning"]);
|
|
2426
|
+
var TraceCollector = class {
|
|
2427
|
+
sequence = [];
|
|
2428
|
+
pendingReasoning = null;
|
|
2429
|
+
decisionCount = 0;
|
|
2430
|
+
traceStartTime = Date.now();
|
|
2431
|
+
client;
|
|
2432
|
+
logger;
|
|
2433
|
+
sessionId;
|
|
2434
|
+
constructor(client, logger, sessionId) {
|
|
2435
|
+
this.client = client;
|
|
2436
|
+
this.logger = logger;
|
|
2437
|
+
this.sessionId = sessionId;
|
|
2438
|
+
}
|
|
2439
|
+
/** Called by the log_reasoning tool handler to store pending reasoning */
|
|
2440
|
+
setReasoning(reasoning, confidence, signals) {
|
|
2441
|
+
this.pendingReasoning = {
|
|
2442
|
+
reasoning,
|
|
2443
|
+
...confidence !== void 0 ? { confidence } : {},
|
|
2444
|
+
...signals !== void 0 ? { signals } : {}
|
|
2445
|
+
};
|
|
2446
|
+
}
|
|
2447
|
+
async beforeExecute(toolName) {
|
|
2448
|
+
if (STATE_CHANGING_TOOLS.has(toolName) && !this.pendingReasoning) {
|
|
2449
|
+
throw new import_sdk8.ToromarketError(
|
|
2450
|
+
"Call log_reasoning before trading. Every trade requires a reasoning explanation.",
|
|
2451
|
+
400,
|
|
2452
|
+
"REASONING_REQUIRED"
|
|
2453
|
+
);
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
async afterExecute(toolName, args, result, error) {
|
|
2457
|
+
if (META_TOOLS.has(toolName)) return;
|
|
2458
|
+
this.sequence.push({
|
|
2459
|
+
tool: toolName,
|
|
2460
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2461
|
+
...args && typeof args === "object" && Object.keys(args).length > 0 ? { args: sanitizeArgs2(args) } : {},
|
|
2462
|
+
...result && !error ? { resultSummary: summarizeResult2(result) } : {}
|
|
2463
|
+
});
|
|
2464
|
+
if (STATE_CHANGING_TOOLS.has(toolName) && error) {
|
|
2465
|
+
this.sequence.pop();
|
|
2466
|
+
return;
|
|
2467
|
+
}
|
|
2468
|
+
if (STATE_CHANGING_TOOLS.has(toolName) && !error && this.pendingReasoning) {
|
|
2469
|
+
await this.submitTrace(toolName, args);
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
async submitTrace(trigger, args) {
|
|
2473
|
+
if (!this.pendingReasoning) return;
|
|
2474
|
+
if (!this.client.getToken()) return;
|
|
2475
|
+
this.decisionCount++;
|
|
2476
|
+
const durationMs = Date.now() - this.traceStartTime;
|
|
2477
|
+
const tradeContext = extractTradeContext(trigger, args);
|
|
2478
|
+
try {
|
|
2479
|
+
await this.client.traces.create({
|
|
2480
|
+
sessionId: this.sessionId,
|
|
2481
|
+
decisionNumber: this.decisionCount,
|
|
2482
|
+
trigger,
|
|
2483
|
+
reasoning: this.pendingReasoning.reasoning,
|
|
2484
|
+
...this.pendingReasoning.confidence !== void 0 ? { confidence: this.pendingReasoning.confidence } : {},
|
|
2485
|
+
...this.pendingReasoning.signals !== void 0 ? { signals: this.pendingReasoning.signals } : {},
|
|
2486
|
+
sequence: this.sequence,
|
|
2487
|
+
stepCount: this.sequence.length,
|
|
2488
|
+
durationMs,
|
|
2489
|
+
tradeContext
|
|
2490
|
+
});
|
|
2491
|
+
this.logger.info({ event: "trace_submitted", trigger, decisionNumber: this.decisionCount });
|
|
2492
|
+
} catch (err) {
|
|
2493
|
+
this.logger.warn({
|
|
2494
|
+
event: "trace_submit_failed",
|
|
2495
|
+
trigger,
|
|
2496
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2497
|
+
});
|
|
2498
|
+
}
|
|
2499
|
+
this.pendingReasoning = null;
|
|
2500
|
+
this.sequence = [];
|
|
2501
|
+
this.traceStartTime = Date.now();
|
|
2502
|
+
}
|
|
2503
|
+
};
|
|
2504
|
+
function sanitizeArgs2(args) {
|
|
2505
|
+
if (typeof args !== "object" || args === null) return args;
|
|
2506
|
+
const sanitized2 = { ...args };
|
|
2507
|
+
delete sanitized2.password;
|
|
2508
|
+
return sanitized2;
|
|
2509
|
+
}
|
|
2510
|
+
function summarizeResult2(result) {
|
|
2511
|
+
if (typeof result !== "object" || result === null) return String(result);
|
|
2512
|
+
const r = result;
|
|
2513
|
+
if (typeof r.balance === "number") return `balance=${r.balance}`;
|
|
2514
|
+
if (r.orderId) return `orderId=${String(r.orderId)}`;
|
|
2515
|
+
if (r.fundId) return `fundId=${String(r.fundId)}`;
|
|
2516
|
+
if (Array.isArray(result)) return `[${result.length} items]`;
|
|
2517
|
+
if (r.title) return String(r.title);
|
|
2518
|
+
if (r.markets && Array.isArray(r.markets)) return `${r.markets.length} markets`;
|
|
2519
|
+
return "ok";
|
|
2520
|
+
}
|
|
2521
|
+
function extractTradeContext(trigger, args) {
|
|
2522
|
+
const a = args ?? {};
|
|
2523
|
+
return {
|
|
2524
|
+
...a.marketId ? { marketId: String(a.marketId) } : {},
|
|
2525
|
+
...a.symbol ? { symbol: String(a.symbol) } : {},
|
|
2526
|
+
...a.outcomeId ? { outcomeId: String(a.outcomeId) } : {},
|
|
2527
|
+
side: String(a.side ?? "UNKNOWN"),
|
|
2528
|
+
price: Number(a.price ?? 0),
|
|
2529
|
+
quantity: Number(a.quantity ?? 0)
|
|
2530
|
+
};
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
// src/notifications.ts
|
|
2534
|
+
var import_ws = __toESM(require("ws"), 1);
|
|
2535
|
+
var MAX_RECONNECT_DELAY_MS = 3e4;
|
|
2536
|
+
var INITIAL_RECONNECT_DELAY_MS = 1e3;
|
|
2537
|
+
var MIN_POLL_INTERVAL_MS = 5e3;
|
|
2538
|
+
var NotificationService = class {
|
|
2539
|
+
ws = null;
|
|
2540
|
+
reconnectTimer = null;
|
|
2541
|
+
reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
|
|
2542
|
+
stopped = false;
|
|
2543
|
+
usingWebSocket = false;
|
|
2544
|
+
// Polling fallback state
|
|
2545
|
+
pollIntervalId = null;
|
|
2546
|
+
isPolling = false;
|
|
2547
|
+
previousOrders = /* @__PURE__ */ new Map();
|
|
2548
|
+
client;
|
|
2549
|
+
server;
|
|
2550
|
+
logger;
|
|
2551
|
+
baseUrl;
|
|
2552
|
+
pollIntervalMs;
|
|
2553
|
+
constructor(options) {
|
|
2554
|
+
this.client = options.client;
|
|
2555
|
+
this.server = options.server;
|
|
2556
|
+
this.logger = options.logger;
|
|
2557
|
+
this.baseUrl = options.baseUrl;
|
|
2558
|
+
this.pollIntervalMs = Math.max(options.pollIntervalMs, MIN_POLL_INTERVAL_MS);
|
|
2559
|
+
}
|
|
2560
|
+
start() {
|
|
2561
|
+
if (this.stopped) return;
|
|
2562
|
+
const token2 = this.client.getToken();
|
|
2563
|
+
if (!token2) return;
|
|
2564
|
+
const wsUrl = this.buildWsUrl(token2);
|
|
2565
|
+
if (wsUrl) {
|
|
2566
|
+
this.connectWebSocket(wsUrl);
|
|
2567
|
+
} else {
|
|
2568
|
+
this.startPolling();
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
stop() {
|
|
2572
|
+
this.stopped = true;
|
|
2573
|
+
if (this.ws) {
|
|
2574
|
+
this.ws.removeAllListeners();
|
|
2575
|
+
this.ws.close();
|
|
2576
|
+
this.ws = null;
|
|
2577
|
+
}
|
|
2578
|
+
if (this.reconnectTimer) {
|
|
2579
|
+
clearTimeout(this.reconnectTimer);
|
|
2580
|
+
this.reconnectTimer = null;
|
|
2581
|
+
}
|
|
2582
|
+
this.stopPolling();
|
|
2583
|
+
this.logger.info({ event: "notifications_stop" });
|
|
2584
|
+
}
|
|
2585
|
+
buildWsUrl(token2) {
|
|
2586
|
+
try {
|
|
2587
|
+
const url = new URL(this.baseUrl);
|
|
2588
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
2589
|
+
url.pathname = "/ws";
|
|
2590
|
+
url.searchParams.set("token", token2);
|
|
2591
|
+
return url.toString();
|
|
2592
|
+
} catch {
|
|
2593
|
+
return null;
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
// --- WebSocket transport ---
|
|
2597
|
+
connectWebSocket(wsUrl) {
|
|
2598
|
+
if (this.stopped) return;
|
|
2599
|
+
this.logger.info({ event: "ws_connecting", url: wsUrl.replace(/token=[^&]+/, "token=***") });
|
|
2600
|
+
try {
|
|
2601
|
+
this.ws = new import_ws.default(wsUrl);
|
|
2602
|
+
} catch (err) {
|
|
2603
|
+
this.logger.warn({ event: "ws_create_failed", error: err instanceof Error ? err.message : String(err) });
|
|
2604
|
+
this.scheduleReconnect();
|
|
2605
|
+
return;
|
|
2606
|
+
}
|
|
2607
|
+
this.ws.on("open", () => {
|
|
2608
|
+
this.usingWebSocket = true;
|
|
2609
|
+
this.reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
|
|
2610
|
+
this.stopPolling();
|
|
2611
|
+
this.logger.info({ event: "ws_connected" });
|
|
2612
|
+
});
|
|
2613
|
+
this.ws.on("message", (data) => {
|
|
2614
|
+
try {
|
|
2615
|
+
const event = JSON.parse(data.toString());
|
|
2616
|
+
this.handleWsEvent(event);
|
|
2617
|
+
} catch {
|
|
2618
|
+
this.logger.warn({ event: "ws_invalid_message", raw: data.toString().slice(0, 200) });
|
|
2619
|
+
}
|
|
2620
|
+
});
|
|
2621
|
+
this.ws.on("close", (code, reason) => {
|
|
2622
|
+
this.logger.info({ event: "ws_disconnected", code, reason: reason.toString() });
|
|
2623
|
+
this.ws = null;
|
|
2624
|
+
this.usingWebSocket = false;
|
|
2625
|
+
if (!this.stopped) {
|
|
2626
|
+
this.scheduleReconnect();
|
|
2627
|
+
}
|
|
2628
|
+
});
|
|
2629
|
+
this.ws.on("error", (err) => {
|
|
2630
|
+
this.logger.warn({ event: "ws_error", error: err.message });
|
|
2631
|
+
});
|
|
2632
|
+
this.ws.on("ping", () => {
|
|
2633
|
+
});
|
|
2634
|
+
}
|
|
2635
|
+
handleWsEvent(event) {
|
|
2636
|
+
switch (event.event) {
|
|
2637
|
+
case "connected":
|
|
2638
|
+
this.logger.info({ event: "ws_authenticated", userId: event.data.userId });
|
|
2639
|
+
break;
|
|
2640
|
+
case "order.filled":
|
|
2641
|
+
this.notify("order_filled", {
|
|
2642
|
+
orderId: event.data.orderId,
|
|
2643
|
+
marketId: event.data.marketId,
|
|
2644
|
+
side: event.data.side,
|
|
2645
|
+
quantity: event.data.quantity,
|
|
2646
|
+
filledPrice: event.data.filledPrice,
|
|
2647
|
+
message: `Order ${String(event.data.orderId).slice(0, 8)} filled (${event.data.side} ${event.data.quantity} shares @ ${event.data.filledPrice})`
|
|
2648
|
+
});
|
|
2649
|
+
break;
|
|
2650
|
+
case "order.partial_fill":
|
|
2651
|
+
this.notify("order_partial_fill", {
|
|
2652
|
+
orderId: event.data.orderId,
|
|
2653
|
+
marketId: event.data.marketId,
|
|
2654
|
+
side: event.data.side,
|
|
2655
|
+
filledNow: event.data.filledQuantity,
|
|
2656
|
+
totalFilled: event.data.totalFilled,
|
|
2657
|
+
totalQuantity: event.data.totalQuantity,
|
|
2658
|
+
message: `Order ${String(event.data.orderId).slice(0, 8)} partially filled: ${event.data.filledQuantity} more shares (${event.data.totalFilled}/${event.data.totalQuantity} total)`
|
|
2659
|
+
});
|
|
2660
|
+
break;
|
|
2661
|
+
case "order.cancelled":
|
|
2662
|
+
this.notify("order_cancelled", {
|
|
2663
|
+
orderId: event.data.orderId,
|
|
2664
|
+
marketId: event.data.marketId,
|
|
2665
|
+
reason: event.data.reason,
|
|
2666
|
+
message: `Order ${String(event.data.orderId).slice(0, 8)} cancelled: ${event.data.reason}`
|
|
2667
|
+
});
|
|
2668
|
+
break;
|
|
2669
|
+
default:
|
|
2670
|
+
this.notify(event.event, event.data);
|
|
2671
|
+
break;
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
scheduleReconnect() {
|
|
2675
|
+
if (this.stopped) return;
|
|
2676
|
+
if (this.reconnectTimer) return;
|
|
2677
|
+
this.logger.info({ event: "ws_reconnect_scheduled", delayMs: this.reconnectDelay });
|
|
2678
|
+
this.startPolling();
|
|
2679
|
+
this.reconnectTimer = setTimeout(() => {
|
|
2680
|
+
this.reconnectTimer = null;
|
|
2681
|
+
if (this.stopped) return;
|
|
2682
|
+
const token2 = this.client.getToken();
|
|
2683
|
+
if (!token2) {
|
|
2684
|
+
this.logger.warn({ event: "ws_reconnect_no_token", message: "No auth token during reconnect. Will retry." });
|
|
2685
|
+
this.scheduleReconnect();
|
|
2686
|
+
return;
|
|
2687
|
+
}
|
|
2688
|
+
const wsUrl = this.buildWsUrl(token2);
|
|
2689
|
+
if (wsUrl) {
|
|
2690
|
+
this.connectWebSocket(wsUrl);
|
|
2691
|
+
}
|
|
2692
|
+
}, this.reconnectDelay);
|
|
2693
|
+
this.reconnectDelay = Math.min(this.reconnectDelay * 2, MAX_RECONNECT_DELAY_MS);
|
|
2694
|
+
}
|
|
2695
|
+
fallbackToPolling() {
|
|
2696
|
+
this.logger.info({ event: "ws_fallback_to_polling" });
|
|
2697
|
+
this.startPolling();
|
|
2698
|
+
}
|
|
2699
|
+
// --- Polling fallback (kept from original implementation) ---
|
|
2700
|
+
startPolling() {
|
|
2701
|
+
if (this.pollIntervalId) return;
|
|
2702
|
+
if (this.pollIntervalMs <= 0) return;
|
|
2703
|
+
this.logger.info({ event: "notifications_polling_start", pollIntervalMs: this.pollIntervalMs });
|
|
2704
|
+
this.pollIntervalId = setInterval(() => {
|
|
2705
|
+
this.poll().catch((err) => {
|
|
2706
|
+
this.logger.error({ event: "notification_poll_error", error: err instanceof Error ? err.message : String(err) });
|
|
2707
|
+
});
|
|
2708
|
+
}, this.pollIntervalMs);
|
|
2709
|
+
}
|
|
2710
|
+
stopPolling() {
|
|
2711
|
+
if (this.pollIntervalId) {
|
|
2712
|
+
clearInterval(this.pollIntervalId);
|
|
2713
|
+
this.pollIntervalId = null;
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
async poll() {
|
|
2717
|
+
if (this.isPolling) return;
|
|
2718
|
+
if (!this.client.getToken()) return;
|
|
2719
|
+
if (this.usingWebSocket) return;
|
|
2720
|
+
this.isPolling = true;
|
|
2721
|
+
try {
|
|
2722
|
+
let currentOrders;
|
|
2723
|
+
try {
|
|
2724
|
+
const raw = await this.client.request("GET", "/api/v1/portfolio/open-orders");
|
|
2725
|
+
currentOrders = Array.isArray(raw) ? raw.map(normalizeOrder) : [];
|
|
2726
|
+
} catch (pollErr) {
|
|
2727
|
+
const message = pollErr instanceof Error ? pollErr.message : String(pollErr);
|
|
2728
|
+
const isAuthError = message.includes("401") || message.includes("Unauthorized") || message.includes("AUTH");
|
|
2729
|
+
if (isAuthError) {
|
|
2730
|
+
this.logger.warn({
|
|
2731
|
+
event: "notification_poll_auth_failure",
|
|
2732
|
+
message: "Notification polling failed due to expired or invalid token. Re-authenticate to restore order notifications."
|
|
2733
|
+
});
|
|
2734
|
+
}
|
|
2735
|
+
return;
|
|
2736
|
+
}
|
|
2737
|
+
const currentMap = /* @__PURE__ */ new Map();
|
|
2738
|
+
for (const order of currentOrders) {
|
|
2739
|
+
currentMap.set(order.id, order);
|
|
2740
|
+
}
|
|
2741
|
+
for (const [id2, prev] of this.previousOrders) {
|
|
2742
|
+
const current = currentMap.get(id2);
|
|
2743
|
+
if (!current) {
|
|
2744
|
+
if (prev.filled > 0) {
|
|
2745
|
+
this.notify("order_filled", {
|
|
2746
|
+
orderId: id2,
|
|
2747
|
+
marketId: prev.marketId,
|
|
2748
|
+
side: prev.side,
|
|
2749
|
+
quantity: prev.quantity,
|
|
2750
|
+
filled: prev.filled,
|
|
2751
|
+
message: `Order ${id2.slice(0, 8)} completed (${prev.side} ${prev.filled}/${prev.quantity} shares filled)`
|
|
2752
|
+
});
|
|
2753
|
+
}
|
|
2754
|
+
} else if (current.filled > prev.filled) {
|
|
2755
|
+
const newFills = current.filled - prev.filled;
|
|
2756
|
+
this.notify("order_partial_fill", {
|
|
2757
|
+
orderId: id2,
|
|
2758
|
+
marketId: current.marketId,
|
|
2759
|
+
side: current.side,
|
|
2760
|
+
filledNow: newFills,
|
|
2761
|
+
totalFilled: current.filled,
|
|
2762
|
+
totalQuantity: current.quantity,
|
|
2763
|
+
message: `Order ${id2.slice(0, 8)} partially filled: ${newFills} more shares (${current.filled}/${current.quantity} total)`
|
|
2764
|
+
});
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
this.previousOrders = currentMap;
|
|
2768
|
+
} finally {
|
|
2769
|
+
this.isPolling = false;
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
// --- Shared notification dispatch ---
|
|
2773
|
+
async notify(event, data) {
|
|
2774
|
+
this.logger.info({ event: `notification:${event}`, ...data });
|
|
2775
|
+
try {
|
|
2776
|
+
await this.server.sendLoggingMessage({
|
|
2777
|
+
level: "info",
|
|
2778
|
+
data: { event, ...data }
|
|
2779
|
+
});
|
|
2780
|
+
} catch {
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
};
|
|
2784
|
+
function normalizeOrder(raw) {
|
|
2785
|
+
const o = raw;
|
|
2786
|
+
return {
|
|
2787
|
+
id: String(o.id ?? ""),
|
|
2788
|
+
marketId: String(o.marketId ?? ""),
|
|
2789
|
+
outcomeId: o.outcomeId ? String(o.outcomeId) : void 0,
|
|
2790
|
+
side: String(o.side ?? ""),
|
|
2791
|
+
quantity: Number(o.quantity ?? 0),
|
|
2792
|
+
filled: Number(o.filled ?? 0),
|
|
2793
|
+
status: String(o.status ?? "OPEN")
|
|
2794
|
+
};
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
// src/metrics.ts
|
|
2798
|
+
var TRADE_TOOLS2 = /* @__PURE__ */ new Set(["place_order", "cancel_order", "trade_crypto"]);
|
|
2799
|
+
var MetricsCollector = class {
|
|
2800
|
+
metrics;
|
|
2801
|
+
logger;
|
|
2802
|
+
constructor(logger, sessionId) {
|
|
2803
|
+
this.logger = logger;
|
|
2804
|
+
this.metrics = {
|
|
2805
|
+
sessionId: sessionId ?? "stdio",
|
|
2806
|
+
userId: null,
|
|
2807
|
+
tier: "FREE",
|
|
2808
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2809
|
+
toolCalls: {},
|
|
2810
|
+
totalTrades: 0,
|
|
2811
|
+
totalErrors: 0
|
|
2812
|
+
};
|
|
2813
|
+
}
|
|
2814
|
+
setUserId(userId) {
|
|
2815
|
+
this.metrics.userId = userId;
|
|
2816
|
+
}
|
|
2817
|
+
setTier(tier) {
|
|
2818
|
+
this.metrics.tier = tier;
|
|
2819
|
+
}
|
|
2820
|
+
recordCall(toolName, durationMs, error) {
|
|
2821
|
+
let metric = this.metrics.toolCalls[toolName];
|
|
2822
|
+
if (!metric) {
|
|
2823
|
+
metric = { count: 0, errors: 0, totalDurationMs: 0 };
|
|
2824
|
+
this.metrics.toolCalls[toolName] = metric;
|
|
2825
|
+
}
|
|
2826
|
+
metric.count++;
|
|
2827
|
+
metric.totalDurationMs += durationMs;
|
|
2828
|
+
if (error) {
|
|
2829
|
+
metric.errors++;
|
|
2830
|
+
this.metrics.totalErrors++;
|
|
2831
|
+
}
|
|
2832
|
+
if (TRADE_TOOLS2.has(toolName) && !error) {
|
|
2833
|
+
this.metrics.totalTrades++;
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
getSnapshot() {
|
|
2837
|
+
return { ...this.metrics, toolCalls: { ...this.metrics.toolCalls } };
|
|
2838
|
+
}
|
|
2839
|
+
flush() {
|
|
2840
|
+
const snapshot = this.getSnapshot();
|
|
2841
|
+
const totalCalls = Object.values(snapshot.toolCalls).reduce((sum, m) => sum + m.count, 0);
|
|
2842
|
+
if (totalCalls === 0) return;
|
|
2843
|
+
this.logger.info({
|
|
2844
|
+
event: "session_metrics",
|
|
2845
|
+
...snapshot,
|
|
2846
|
+
summary: {
|
|
2847
|
+
totalCalls,
|
|
2848
|
+
totalTrades: snapshot.totalTrades,
|
|
2849
|
+
totalErrors: snapshot.totalErrors,
|
|
2850
|
+
durationMs: Date.now() - new Date(snapshot.startedAt).getTime(),
|
|
2851
|
+
topTools: Object.entries(snapshot.toolCalls).sort(([, a], [, b]) => b.count - a.count).slice(0, 5).map(([name, m]) => ({ name, count: m.count }))
|
|
2852
|
+
}
|
|
2853
|
+
});
|
|
2854
|
+
}
|
|
2855
|
+
};
|
|
2856
|
+
|
|
2857
|
+
// src/server.ts
|
|
2858
|
+
function createToromarketClient(options) {
|
|
2859
|
+
return new import_sdk9.ToromarketClient({
|
|
2860
|
+
baseUrl: options.baseUrl,
|
|
2861
|
+
clientId: "mcp-server/0.1.0",
|
|
2862
|
+
...options.token ? { token: options.token } : {},
|
|
2863
|
+
...options.apiKey ? { apiKey: options.apiKey } : {},
|
|
2864
|
+
...options.agentSecret ? { agentSecret: options.agentSecret } : {}
|
|
2865
|
+
});
|
|
2866
|
+
}
|
|
2867
|
+
function createToromarketServer(client, baseUrl2, logger, middlewares = [], executeOptions, metrics, toolProfile2, compactDescriptions2) {
|
|
2868
|
+
const server = new import_server.Server(
|
|
2869
|
+
{ name: "toromarket", version: "0.1.0" },
|
|
2870
|
+
{ capabilities: { tools: {}, resources: {}, prompts: {}, logging: {} } }
|
|
2871
|
+
);
|
|
2872
|
+
const allTools = [...PHASE1_TOOLS, ...PHASE2_TOOLS, ...PHASE3_TOOLS, ...FUND_TRADING_TOOLS, ...EXTRA_TOOLS, ...BILLING_TOOLS, ...CONTEXT_TOOLS, ...TRACE_TOOLS, ...IDENTITY_TOOLS];
|
|
2873
|
+
const profileToolSet = toolProfile2 ? getProfileToolSet(toolProfile2) : null;
|
|
2874
|
+
const profileTools = profileToolSet ? allTools.filter((t) => profileToolSet.has(t.name)) : allTools;
|
|
2875
|
+
const finalTools = compactDescriptions2 ? profileTools.map((t) => {
|
|
2876
|
+
const compact = COMPACT_DESCRIPTIONS[t.name];
|
|
2877
|
+
return compact ? { ...t, description: compact } : t;
|
|
2878
|
+
}) : profileTools;
|
|
2879
|
+
server.setRequestHandler(import_types.ListToolsRequestSchema, async () => ({
|
|
2880
|
+
tools: finalTools
|
|
2881
|
+
}));
|
|
2882
|
+
const allowedTools = profileToolSet ? profileToolSet : new Set(allTools.map((t) => t.name));
|
|
2883
|
+
server.setRequestHandler(import_types.CallToolRequestSchema, async (request) => {
|
|
2884
|
+
const toolName = request.params.name;
|
|
2885
|
+
if (!allowedTools.has(toolName)) {
|
|
2886
|
+
return {
|
|
2887
|
+
isError: true,
|
|
2888
|
+
content: [{
|
|
2889
|
+
type: "text",
|
|
2890
|
+
text: JSON.stringify({
|
|
2891
|
+
error: `Tool "${toolName}" is not available in the "${toolProfile2 ?? "full"}" profile. Switch to a broader profile or use TOROMARKET_TOOL_PROFILE=full.`,
|
|
2892
|
+
availableProfiles: ["trader", "social", "fund_manager", "full"]
|
|
2893
|
+
}, null, 2)
|
|
2894
|
+
}]
|
|
2895
|
+
};
|
|
2896
|
+
}
|
|
2897
|
+
const args = request.params.arguments;
|
|
2898
|
+
const startTime = Date.now();
|
|
2899
|
+
try {
|
|
2900
|
+
for (const mw of middlewares) {
|
|
2901
|
+
await mw.beforeExecute(toolName, args);
|
|
2902
|
+
}
|
|
2903
|
+
const result = await executeTool(client, baseUrl2, toolName, args, executeOptions);
|
|
2904
|
+
for (const mw of middlewares) {
|
|
2905
|
+
await mw.afterExecute(toolName, args, result);
|
|
2906
|
+
}
|
|
2907
|
+
const durationMs = Date.now() - startTime;
|
|
2908
|
+
logger.info({ tool: toolName, args, durationMs, success: true });
|
|
2909
|
+
metrics?.recordCall(toolName, durationMs, false);
|
|
2910
|
+
return {
|
|
2911
|
+
content: [{ type: "text", text: JSON.stringify(result ?? { ok: true }, null, 2) }]
|
|
2912
|
+
};
|
|
2913
|
+
} catch (error) {
|
|
2914
|
+
for (const mw of middlewares) {
|
|
2915
|
+
await mw.afterExecute(toolName, args, void 0, error);
|
|
2916
|
+
}
|
|
2917
|
+
const durationMs = Date.now() - startTime;
|
|
2918
|
+
const classified = classifyError(error);
|
|
2919
|
+
logger.warn({ tool: toolName, args, durationMs, success: false, error: classified });
|
|
2920
|
+
metrics?.recordCall(toolName, durationMs, true);
|
|
2921
|
+
return {
|
|
2922
|
+
isError: true,
|
|
2923
|
+
content: [
|
|
2924
|
+
{ type: "text", text: JSON.stringify(classified, null, 2) }
|
|
2925
|
+
]
|
|
2926
|
+
};
|
|
2927
|
+
}
|
|
2928
|
+
});
|
|
2929
|
+
server.setRequestHandler(import_types.ListResourcesRequestSchema, async () => ({
|
|
2930
|
+
resources: [
|
|
2931
|
+
{
|
|
2932
|
+
uri: "toromarket://portfolio",
|
|
2933
|
+
name: "Portfolio",
|
|
2934
|
+
description: "Current portfolio state \u2014 balance, positions, holdings",
|
|
2935
|
+
mimeType: "application/json"
|
|
2936
|
+
},
|
|
2937
|
+
{
|
|
2938
|
+
uri: "toromarket://markets",
|
|
2939
|
+
name: "Markets",
|
|
2940
|
+
description: "Active prediction markets summary",
|
|
2941
|
+
mimeType: "application/json"
|
|
2942
|
+
}
|
|
2943
|
+
]
|
|
2944
|
+
}));
|
|
2945
|
+
server.setRequestHandler(import_types.ReadResourceRequestSchema, async (request) => {
|
|
2946
|
+
const uri = request.params.uri;
|
|
2947
|
+
switch (uri) {
|
|
2948
|
+
case "toromarket://portfolio": {
|
|
2949
|
+
if (!client.getToken()) {
|
|
2950
|
+
return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify({ error: "Not authenticated" }) }] };
|
|
2951
|
+
}
|
|
2952
|
+
const portfolio = await client.portfolio.get();
|
|
2953
|
+
return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(portfolio) }] };
|
|
2954
|
+
}
|
|
2955
|
+
case "toromarket://markets": {
|
|
2956
|
+
const markets = await client.predictions.listMarkets({ limit: 20, offset: 0 });
|
|
2957
|
+
return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(markets) }] };
|
|
2958
|
+
}
|
|
2959
|
+
default:
|
|
2960
|
+
throw new Error(`Unknown resource: ${uri}`);
|
|
2961
|
+
}
|
|
2962
|
+
});
|
|
2963
|
+
server.setRequestHandler(import_types.ListPromptsRequestSchema, async () => ({
|
|
2964
|
+
prompts: [
|
|
2965
|
+
{
|
|
2966
|
+
name: "trading-strategy",
|
|
2967
|
+
description: "System prompt for trading agents \u2014 includes current portfolio state and market overview"
|
|
2968
|
+
}
|
|
2969
|
+
]
|
|
2970
|
+
}));
|
|
2971
|
+
server.setRequestHandler(import_types.GetPromptRequestSchema, async (request) => {
|
|
2972
|
+
if (request.params.name === "trading-strategy") {
|
|
2973
|
+
return {
|
|
2974
|
+
messages: [
|
|
2975
|
+
{
|
|
2976
|
+
role: "user",
|
|
2977
|
+
content: {
|
|
2978
|
+
type: "text",
|
|
2979
|
+
text: "You are a trading agent on Toromarket, a prediction market platform. Use get_trading_context at the start of each decision cycle to understand your current state. Place orders based on your analysis of market probabilities. Track your performance with get_my_performance. Be disciplined with position sizing."
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
]
|
|
2983
|
+
};
|
|
2984
|
+
}
|
|
2985
|
+
throw new Error(`Unknown prompt: ${request.params.name}`);
|
|
2986
|
+
});
|
|
2987
|
+
return server;
|
|
2988
|
+
}
|
|
2989
|
+
function createServerFactory(options) {
|
|
2990
|
+
const logger = createLogger(options.logLevel);
|
|
2991
|
+
const pollIntervalMs2 = options.pollIntervalMs ?? 1e4;
|
|
2992
|
+
const createMcpServer = () => {
|
|
2993
|
+
const client = createToromarketClient(options);
|
|
2994
|
+
const metrics = new MetricsCollector(logger);
|
|
2995
|
+
const rateLimiter = new RateLimiter();
|
|
2996
|
+
const traceCollector = new TraceCollector(client, logger, `session-${Date.now()}`);
|
|
2997
|
+
const middlewares = [
|
|
2998
|
+
new RegistrationThrottle(),
|
|
2999
|
+
rateLimiter,
|
|
3000
|
+
new SpoofingDetector(),
|
|
3001
|
+
new SpendingGuardrails(options.maxSessionLoss),
|
|
3002
|
+
new ErrorBudget(),
|
|
3003
|
+
new AuditLogger(logger),
|
|
3004
|
+
traceCollector
|
|
3005
|
+
];
|
|
3006
|
+
let notificationService = null;
|
|
3007
|
+
let mcpServer = null;
|
|
3008
|
+
let currentTrustTier = null;
|
|
3009
|
+
const executeOptions = {
|
|
3010
|
+
traceCollector,
|
|
3011
|
+
...options.operatorId ? { defaultOperatorId: options.operatorId } : {},
|
|
3012
|
+
onTrustTier: (trustTier) => {
|
|
3013
|
+
currentTrustTier = trustTier;
|
|
3014
|
+
logger.info({ event: "trust_tier_applied", trustTier });
|
|
3015
|
+
},
|
|
3016
|
+
onAuth: (tier, userId) => {
|
|
3017
|
+
rateLimiter.updateTier(tier);
|
|
3018
|
+
metrics.setTier(tier);
|
|
3019
|
+
metrics.setUserId(userId);
|
|
3020
|
+
logger.info({ event: "tier_applied", tier, userId });
|
|
3021
|
+
if (!notificationService && pollIntervalMs2 > 0 && mcpServer) {
|
|
3022
|
+
notificationService = new NotificationService({
|
|
3023
|
+
client,
|
|
3024
|
+
server: mcpServer,
|
|
3025
|
+
logger,
|
|
3026
|
+
pollIntervalMs: pollIntervalMs2,
|
|
3027
|
+
baseUrl: options.baseUrl
|
|
3028
|
+
});
|
|
3029
|
+
notificationService.start();
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
};
|
|
3033
|
+
mcpServer = createToromarketServer(client, options.baseUrl, logger, middlewares, executeOptions, metrics, options.toolProfile, options.compactDescriptions);
|
|
3034
|
+
if (client.getToken() && pollIntervalMs2 > 0) {
|
|
3035
|
+
notificationService = new NotificationService({
|
|
3036
|
+
client,
|
|
3037
|
+
server: mcpServer,
|
|
3038
|
+
logger,
|
|
3039
|
+
pollIntervalMs: pollIntervalMs2,
|
|
3040
|
+
baseUrl: options.baseUrl
|
|
3041
|
+
});
|
|
3042
|
+
notificationService.start();
|
|
3043
|
+
}
|
|
3044
|
+
return {
|
|
3045
|
+
server: mcpServer,
|
|
3046
|
+
cleanup: () => {
|
|
3047
|
+
metrics.flush();
|
|
3048
|
+
if (notificationService) {
|
|
3049
|
+
notificationService.stop();
|
|
3050
|
+
notificationService = null;
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
};
|
|
3054
|
+
};
|
|
3055
|
+
return { logger, createMcpServer };
|
|
3056
|
+
}
|
|
3057
|
+
async function startStdioServer(options) {
|
|
3058
|
+
const { logger, createMcpServer } = createServerFactory(options);
|
|
3059
|
+
const { server, cleanup } = createMcpServer();
|
|
3060
|
+
const transport2 = new import_stdio.StdioServerTransport();
|
|
3061
|
+
transport2.onclose = () => cleanup();
|
|
3062
|
+
logger.info({ event: "server_start", transport: "stdio", baseUrl: options.baseUrl });
|
|
3063
|
+
await server.connect(transport2);
|
|
3064
|
+
}
|
|
3065
|
+
|
|
3066
|
+
// src/transports/http.ts
|
|
3067
|
+
var import_node_http = require("http");
|
|
3068
|
+
var import_node_crypto = require("crypto");
|
|
3069
|
+
var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
3070
|
+
var MAX_SESSIONS = 100;
|
|
3071
|
+
var MAX_BODY_SIZE = 1048576;
|
|
3072
|
+
var SESSION_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
3073
|
+
function checkApiKey(configuredKey, providedKey) {
|
|
3074
|
+
if (!configuredKey) return true;
|
|
3075
|
+
return providedKey === configuredKey;
|
|
3076
|
+
}
|
|
3077
|
+
async function startHttpTransport(options) {
|
|
3078
|
+
const { port, logger } = options;
|
|
3079
|
+
if (!process.env.TOROMARKET_MCP_API_KEY) {
|
|
3080
|
+
logger.warn({ event: "http_no_api_key", message: "HTTP transport running without authentication. Set TOROMARKET_MCP_API_KEY for production." });
|
|
3081
|
+
}
|
|
3082
|
+
const allowedOrigin = process.env.TOROMARKET_CORS_ORIGIN ?? "http://localhost:3000";
|
|
3083
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
3084
|
+
const httpServer = (0, import_node_http.createServer)(async (req, res) => {
|
|
3085
|
+
res.setHeader("Access-Control-Allow-Origin", allowedOrigin);
|
|
3086
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
3087
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, mcp-session-id, x-mcp-api-key, Authorization");
|
|
3088
|
+
res.setHeader("Access-Control-Expose-Headers", "mcp-session-id");
|
|
3089
|
+
if (req.method === "OPTIONS") {
|
|
3090
|
+
res.writeHead(204);
|
|
3091
|
+
res.end();
|
|
3092
|
+
return;
|
|
3093
|
+
}
|
|
3094
|
+
const url = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
3095
|
+
if (url.pathname === "/health") {
|
|
3096
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3097
|
+
res.end(JSON.stringify({
|
|
3098
|
+
status: "ok",
|
|
3099
|
+
sessions: sessions.size,
|
|
3100
|
+
uptime: Math.floor(process.uptime())
|
|
3101
|
+
}));
|
|
3102
|
+
return;
|
|
3103
|
+
}
|
|
3104
|
+
const mcpApiKey = process.env.TOROMARKET_MCP_API_KEY;
|
|
3105
|
+
if (mcpApiKey) {
|
|
3106
|
+
const provided = req.headers["x-mcp-api-key"] ?? req.headers["authorization"]?.replace(/^bearer\s+/i, "");
|
|
3107
|
+
if (!checkApiKey(mcpApiKey, provided)) {
|
|
3108
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
3109
|
+
res.end(JSON.stringify({ error: "Invalid or missing MCP API key" }));
|
|
3110
|
+
return;
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
if (url.pathname !== "/mcp") {
|
|
3114
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
3115
|
+
res.end(JSON.stringify({ error: "Not found. Use /mcp endpoint." }));
|
|
3116
|
+
return;
|
|
3117
|
+
}
|
|
3118
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
3119
|
+
if (req.method === "GET" || req.method === "POST") {
|
|
3120
|
+
let entry = sessionId ? sessions.get(sessionId) : void 0;
|
|
3121
|
+
if (entry) {
|
|
3122
|
+
entry.lastActivity = Date.now();
|
|
3123
|
+
}
|
|
3124
|
+
if (!entry) {
|
|
3125
|
+
if (sessions.size >= MAX_SESSIONS) {
|
|
3126
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
3127
|
+
res.end(JSON.stringify({ error: "Maximum sessions reached" }));
|
|
3128
|
+
return;
|
|
3129
|
+
}
|
|
3130
|
+
const transport2 = new import_streamableHttp.StreamableHTTPServerTransport({
|
|
3131
|
+
sessionIdGenerator: () => (0, import_node_crypto.randomUUID)()
|
|
3132
|
+
});
|
|
3133
|
+
const createdServer = options.createServer();
|
|
3134
|
+
await createdServer.server.connect(transport2);
|
|
3135
|
+
const newSessionId = transport2.sessionId;
|
|
3136
|
+
entry = { transport: transport2, server: createdServer, lastActivity: Date.now() };
|
|
3137
|
+
sessions.set(newSessionId, entry);
|
|
3138
|
+
logger.info({ event: "session_created", sessionId: newSessionId });
|
|
3139
|
+
transport2.onclose = () => {
|
|
3140
|
+
createdServer.cleanup();
|
|
3141
|
+
sessions.delete(newSessionId);
|
|
3142
|
+
logger.info({ event: "session_closed", sessionId: newSessionId });
|
|
3143
|
+
};
|
|
3144
|
+
}
|
|
3145
|
+
let parsedBody;
|
|
3146
|
+
if (req.method === "POST") {
|
|
3147
|
+
const chunks = [];
|
|
3148
|
+
let totalSize = 0;
|
|
3149
|
+
for await (const chunk of req) {
|
|
3150
|
+
totalSize += chunk.length;
|
|
3151
|
+
if (totalSize > MAX_BODY_SIZE) {
|
|
3152
|
+
res.writeHead(413, { "Content-Type": "application/json" });
|
|
3153
|
+
res.end(JSON.stringify({ error: "Request body too large" }));
|
|
3154
|
+
return;
|
|
3155
|
+
}
|
|
3156
|
+
chunks.push(chunk);
|
|
3157
|
+
}
|
|
3158
|
+
const bodyStr = Buffer.concat(chunks).toString("utf-8");
|
|
3159
|
+
try {
|
|
3160
|
+
parsedBody = JSON.parse(bodyStr);
|
|
3161
|
+
} catch {
|
|
3162
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
3163
|
+
res.end(JSON.stringify({ error: "Invalid JSON in request body" }));
|
|
3164
|
+
return;
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
await entry.transport.handleRequest(req, res, parsedBody);
|
|
3168
|
+
return;
|
|
3169
|
+
}
|
|
3170
|
+
if (req.method === "DELETE" && sessionId) {
|
|
3171
|
+
const entry = sessions.get(sessionId);
|
|
3172
|
+
if (entry) {
|
|
3173
|
+
await entry.transport.close();
|
|
3174
|
+
entry.server.cleanup();
|
|
3175
|
+
sessions.delete(sessionId);
|
|
3176
|
+
logger.info({ event: "session_deleted", sessionId });
|
|
3177
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3178
|
+
res.end(JSON.stringify({ ok: true }));
|
|
3179
|
+
} else {
|
|
3180
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
3181
|
+
res.end(JSON.stringify({ error: "Session not found" }));
|
|
3182
|
+
}
|
|
3183
|
+
return;
|
|
3184
|
+
}
|
|
3185
|
+
res.writeHead(405, { "Content-Type": "application/json" });
|
|
3186
|
+
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
3187
|
+
});
|
|
3188
|
+
httpServer.listen(port, () => {
|
|
3189
|
+
logger.info({ event: "http_server_start", port, endpoint: `/mcp` });
|
|
3190
|
+
process.stderr.write(`Toromarket MCP server (HTTP) listening on port ${port}
|
|
3191
|
+
`);
|
|
3192
|
+
});
|
|
3193
|
+
const cleanupInterval = setInterval(() => {
|
|
3194
|
+
const now = Date.now();
|
|
3195
|
+
for (const [id2, entry] of sessions) {
|
|
3196
|
+
if (now - entry.lastActivity > SESSION_TIMEOUT_MS) {
|
|
3197
|
+
entry.transport.close().catch(() => {
|
|
3198
|
+
});
|
|
3199
|
+
entry.server.cleanup();
|
|
3200
|
+
sessions.delete(id2);
|
|
3201
|
+
logger.info({ event: "session_timeout", sessionId: id2 });
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
}, 6e4);
|
|
3205
|
+
const shutdown = () => {
|
|
3206
|
+
logger.info({ event: "http_server_shutdown" });
|
|
3207
|
+
clearInterval(cleanupInterval);
|
|
3208
|
+
for (const entry of sessions.values()) {
|
|
3209
|
+
entry.transport.close().catch(() => {
|
|
3210
|
+
});
|
|
3211
|
+
entry.server.cleanup();
|
|
3212
|
+
}
|
|
3213
|
+
httpServer.close(() => {
|
|
3214
|
+
process.exit(0);
|
|
3215
|
+
});
|
|
3216
|
+
};
|
|
3217
|
+
process.on("SIGINT", shutdown);
|
|
3218
|
+
process.on("SIGTERM", shutdown);
|
|
3219
|
+
await new Promise((resolve) => {
|
|
3220
|
+
httpServer.on("close", resolve);
|
|
3221
|
+
});
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3224
|
+
// src/index.ts
|
|
3225
|
+
function readEnv(name) {
|
|
3226
|
+
const value = process.env[name];
|
|
3227
|
+
return value && value.trim().length > 0 ? value : void 0;
|
|
3228
|
+
}
|
|
3229
|
+
var baseUrl = readEnv("TOROMARKET_BASE_URL") ?? "https://api.toromarket.io";
|
|
3230
|
+
var token = readEnv("TOROMARKET_TOKEN");
|
|
3231
|
+
var apiKey = readEnv("TOROMARKET_API_KEY");
|
|
3232
|
+
var logLevel = readEnv("TOROMARKET_LOG_LEVEL") ?? "info";
|
|
3233
|
+
var maxSessionLossStr = readEnv("TOROMARKET_MAX_SESSION_LOSS");
|
|
3234
|
+
var maxSessionLoss = maxSessionLossStr ? Number(maxSessionLossStr) : void 0;
|
|
3235
|
+
var transport = readEnv("TOROMARKET_TRANSPORT") ?? "stdio";
|
|
3236
|
+
var httpPort = Number(readEnv("TOROMARKET_HTTP_PORT") ?? "3001");
|
|
3237
|
+
var agentSecret = readEnv("TOROMARKET_AGENT_SECRET");
|
|
3238
|
+
var pollIntervalStr = readEnv("TOROMARKET_POLL_INTERVAL");
|
|
3239
|
+
var pollIntervalMs = pollIntervalStr ? Number(pollIntervalStr) : void 0;
|
|
3240
|
+
var toolProfile = readEnv("TOROMARKET_TOOL_PROFILE") ?? "full";
|
|
3241
|
+
var compactDescriptions = readEnv("TOROMARKET_TOOL_DESCRIPTIONS") === "compact";
|
|
3242
|
+
var operatorId = readEnv("TOROMARKET_OPERATOR_ID");
|
|
3243
|
+
if (!isValidProfile(toolProfile)) {
|
|
3244
|
+
process.stderr.write(`Invalid TOROMARKET_TOOL_PROFILE: "${toolProfile}". Valid: trader, social, fund_manager, full
|
|
3245
|
+
`);
|
|
3246
|
+
process.exit(1);
|
|
3247
|
+
}
|
|
3248
|
+
var serverOptions = {
|
|
3249
|
+
baseUrl,
|
|
3250
|
+
logLevel,
|
|
3251
|
+
toolProfile,
|
|
3252
|
+
compactDescriptions,
|
|
3253
|
+
...token ? { token } : {},
|
|
3254
|
+
...apiKey ? { apiKey } : {},
|
|
3255
|
+
...agentSecret ? { agentSecret } : {},
|
|
3256
|
+
...maxSessionLoss ? { maxSessionLoss } : {},
|
|
3257
|
+
...pollIntervalMs !== void 0 ? { pollIntervalMs } : {},
|
|
3258
|
+
...operatorId ? { operatorId } : {}
|
|
3259
|
+
};
|
|
3260
|
+
async function main() {
|
|
3261
|
+
if (transport === "http") {
|
|
3262
|
+
const { logger, createMcpServer } = createServerFactory(serverOptions);
|
|
3263
|
+
await startHttpTransport({
|
|
3264
|
+
port: httpPort,
|
|
3265
|
+
createServer: createMcpServer,
|
|
3266
|
+
logger
|
|
3267
|
+
});
|
|
3268
|
+
} else {
|
|
3269
|
+
await startStdioServer(serverOptions);
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
main().catch((error) => {
|
|
3273
|
+
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
3274
|
+
process.stderr.write(`${message}
|
|
3275
|
+
`);
|
|
3276
|
+
process.exit(1);
|
|
3277
|
+
});
|
|
3278
|
+
//# sourceMappingURL=index.cjs.map
|