@unhedged/mcp 0.1.2

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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +48 -0
  5. package/dist/client/errors.d.ts +15 -0
  6. package/dist/client/errors.js +40 -0
  7. package/dist/client/http.d.ts +21 -0
  8. package/dist/client/http.js +132 -0
  9. package/dist/client/types.d.ts +140 -0
  10. package/dist/client/types.js +6 -0
  11. package/dist/config.d.ts +7 -0
  12. package/dist/config.js +27 -0
  13. package/dist/index.d.ts +6 -0
  14. package/dist/index.js +4 -0
  15. package/dist/init/index.d.ts +10 -0
  16. package/dist/init/index.js +137 -0
  17. package/dist/init/snippet.d.ts +35 -0
  18. package/dist/init/snippet.js +72 -0
  19. package/dist/prompts/frontmatter.d.ts +28 -0
  20. package/dist/prompts/frontmatter.js +113 -0
  21. package/dist/prompts/index.d.ts +10 -0
  22. package/dist/prompts/index.js +50 -0
  23. package/dist/prompts/loader.d.ts +13 -0
  24. package/dist/prompts/loader.js +36 -0
  25. package/dist/safety/index.d.ts +1 -0
  26. package/dist/safety/index.js +1 -0
  27. package/dist/safety/spend-cap.d.ts +33 -0
  28. package/dist/safety/spend-cap.js +64 -0
  29. package/dist/server.d.ts +3 -0
  30. package/dist/server.js +75 -0
  31. package/dist/tools/balance.d.ts +2 -0
  32. package/dist/tools/balance.js +175 -0
  33. package/dist/tools/bets.d.ts +2 -0
  34. package/dist/tools/bets.js +109 -0
  35. package/dist/tools/index.d.ts +4 -0
  36. package/dist/tools/index.js +17 -0
  37. package/dist/tools/market-maker.d.ts +2 -0
  38. package/dist/tools/market-maker.js +38 -0
  39. package/dist/tools/markets.d.ts +2 -0
  40. package/dist/tools/markets.js +120 -0
  41. package/dist/tools/orders.d.ts +2 -0
  42. package/dist/tools/orders.js +256 -0
  43. package/dist/tools/portfolio.d.ts +2 -0
  44. package/dist/tools/portfolio.js +34 -0
  45. package/dist/tools/types.d.ts +44 -0
  46. package/dist/tools/types.js +10 -0
  47. package/package.json +54 -0
  48. package/skills/cash-out.md +51 -0
  49. package/skills/market-creator-assistant.md +45 -0
  50. package/skills/market-maker-workflow.md +58 -0
  51. package/skills/market-research.md +42 -0
  52. package/skills/place-hedged-position.md +51 -0
  53. package/skills/portfolio-review.md +34 -0
@@ -0,0 +1,256 @@
1
+ import { z } from 'zod';
2
+ import { defineTool, READ_ONLY } from './types.js';
3
+ import { UnhedgedApiError } from '../client/errors.js';
4
+ import { SpendCapExceededError } from '../safety/spend-cap.js';
5
+ const LIST_DESCRIPTION = `
6
+ List the user's open **order-book orders**. (Parimutuel bets show up under \`list_bets\` instead.)
7
+
8
+ Filters:
9
+ - **marketId** (optional): restrict to one market. Omit it to enumerate open orders account-wide.
10
+ - **outcome** (optional): filter by outcome label, e.g. \`"YES"\`, \`"$95k - $100k"\`. Requires \`marketId\`.
11
+ - **limit**: 1-500, default 100.
12
+ - **cursor**: opaque pagination cursor from the preceding response.
13
+ - **includeTotal**: include an exact account-wide total.
14
+ - **includeMarketDetails**: include compact market metadata for account-wide rows.
15
+
16
+ Returns \`{ orders, nextCursor }\` and, when requested, \`total\` / \`markets\`. Follow \`nextCursor\` until it is null when a complete account-wide inventory is required. Each order has \`id\`, \`marketId\`, \`outcome\`, \`side\` (\`bid\` or \`ask\`), \`type\` (\`Limit | Market | StopLoss | TakeProfit\`), \`price\`, \`quantity\` or \`quoteQuantity\`, and status.
17
+
18
+ Only works on CLOB markets — parimutuel markets won't have any orders to return.
19
+
20
+ Requires scope: \`order:read\`.
21
+
22
+ ### Examples
23
+ All open orders on a market:
24
+ \`\`\`json
25
+ { "marketId": "clxabc123..." }
26
+ \`\`\`
27
+
28
+ All open orders across the account:
29
+ \`\`\`json
30
+ { "limit": 500, "includeTotal": true, "includeMarketDetails": true }
31
+ \`\`\`
32
+
33
+ Only the user's bids on the YES outcome:
34
+ \`\`\`json
35
+ { "marketId": "clxabc123...", "outcome": "YES" }
36
+ \`\`\`
37
+ `.trim();
38
+ const PLACE_DESCRIPTION = `
39
+ Place an **order on a CLOB market**. Use this for limit/market/stop orders on order-book markets only — parimutuel markets use \`place_bet\`.
40
+
41
+ Required parameters:
42
+ - **marketId**: which market.
43
+ - **outcome**: outcome label, e.g. \`"YES"\`, \`"NO"\`, \`"$95k - $100k"\`. Match exactly the labels from \`get_market\`.
44
+ - **side**: \`"bid"\` to buy a contract on this outcome, \`"ask"\` to sell.
45
+ - **type**: \`"Limit"\`, \`"Market"\`, \`"StopLoss"\`, or \`"TakeProfit"\`.
46
+ - **One of** \`quantity\` (positive integer contract count, e.g. \`5\`) **or** \`quoteQuantity\` (decimal USD notional string with exactly 2 decimals). Not both.
47
+
48
+ Conditional parameters:
49
+ - **price**: required for \`Limit\`. Decimal string with exactly 2 decimals, strictly between 0 and 1 (probability).
50
+ - **triggerPrice**: required for \`StopLoss\` and \`TakeProfit\`. Same bounds as price.
51
+
52
+ Returns \`{ order: ... }\` with the engine-assigned ID, status, and any immediate fills.
53
+
54
+ The MCP server automatically adds an \`Idempotency-Key\`.
55
+
56
+ Requires scope: \`order:place\`.
57
+
58
+ ### Safety
59
+ - If a **session spend cap** is configured, \`bid\` orders contribute to the cap (estimated notional = \`quoteQuantity\` if given, else \`price × quantity\` for limit orders). Ask orders are not counted (they reduce exposure).
60
+ - For \`bid\` \`Market\` orders **without** \`quoteQuantity\`, the notional can't be estimated client-side. Either set \`quoteQuantity\` or use a limit order so the spend cap can supervise.
61
+
62
+ ### Example
63
+ Buy 5 contracts of YES at probability 0.55:
64
+ \`\`\`json
65
+ {
66
+ "marketId": "clxabc123...",
67
+ "outcome": "YES",
68
+ "side": "bid",
69
+ "type": "Limit",
70
+ "price": "0.55",
71
+ "quantity": 5
72
+ }
73
+ \`\`\`
74
+
75
+ Sell $3.00 of NO at market:
76
+ \`\`\`json
77
+ {
78
+ "marketId": "clxabc123...",
79
+ "outcome": "NO",
80
+ "side": "ask",
81
+ "type": "Market",
82
+ "quoteQuantity": "3.00"
83
+ }
84
+ \`\`\`
85
+ `.trim();
86
+ const CANCEL_DESCRIPTION = `
87
+ Cancel one, several, or all of the user's open orders on a single CLOB market.
88
+
89
+ Required:
90
+ - **marketId**: market to cancel on.
91
+ - **orderIds**: array of order IDs (as strings) to cancel. **Pass an empty array \`[]\` to cancel all of the user's open orders on the market.**
92
+
93
+ Returns \`{ result }\` with engine-side per-order outcomes.
94
+
95
+ The market must still be ACTIVE and before the trading cutoff (\`scheduledResolutionTime\` or \`endTime\`); cancelling on a closed market is rejected.
96
+
97
+ The MCP server adds an \`Idempotency-Key\` automatically.
98
+
99
+ Requires scope: \`order:cancel\`.
100
+
101
+ ### Examples
102
+ Cancel two specific orders:
103
+ \`\`\`json
104
+ { "marketId": "clxabc123...", "orderIds": ["1042", "1051"] }
105
+ \`\`\`
106
+
107
+ Cancel **all** open orders on this market:
108
+ \`\`\`json
109
+ { "marketId": "clxabc123...", "orderIds": [] }
110
+ \`\`\`
111
+ `.trim();
112
+ const listSchema = {
113
+ marketId: z.string().min(1).optional().describe('Optional market ID. Omit for account-wide open orders.'),
114
+ outcome: z.string().optional().describe('Outcome label filter. Requires marketId.'),
115
+ limit: z.number().int().min(1).max(500).optional(),
116
+ cursor: z.string().min(1).optional().describe('Opaque nextCursor from the preceding page.'),
117
+ includeTotal: z.boolean().optional(),
118
+ includeMarketDetails: z.boolean().optional(),
119
+ };
120
+ const probabilityStringSchema = z
121
+ .string()
122
+ .regex(/^(?:0|[1-9]\d*)\.\d{2}$/)
123
+ .refine((value) => {
124
+ const n = Number(value);
125
+ return Number.isFinite(n) && n > 0 && n < 1;
126
+ });
127
+ const positiveDecimalStringSchema = z
128
+ .string()
129
+ .regex(/^(?:0|[1-9]\d*)\.\d{2}$/)
130
+ .refine((value) => {
131
+ const n = Number(value);
132
+ return Number.isFinite(n) && n > 0;
133
+ });
134
+ const placeSchema = {
135
+ marketId: z.string().min(1),
136
+ outcome: z.string().min(1).describe('Outcome label, e.g. "YES" or "$95k - $100k".'),
137
+ side: z.enum(['bid', 'ask']),
138
+ type: z.enum(['Limit', 'Market', 'StopLoss', 'TakeProfit']),
139
+ price: probabilityStringSchema
140
+ .optional()
141
+ .describe('Probability price decimal string between 0 and 1 with exactly 2 decimals. Required for Limit orders.'),
142
+ triggerPrice: probabilityStringSchema
143
+ .optional()
144
+ .describe('Trigger probability decimal string between 0 and 1 with exactly 2 decimals. Required for StopLoss/TakeProfit.'),
145
+ quantity: z
146
+ .number()
147
+ .int()
148
+ .positive()
149
+ .optional()
150
+ .describe('Number of contracts as a positive integer.'),
151
+ quoteQuantity: positiveDecimalStringSchema
152
+ .optional()
153
+ .describe('USD notional string with exactly 2 decimals. Use INSTEAD of quantity.'),
154
+ };
155
+ const cancelSchema = {
156
+ marketId: z.string().min(1),
157
+ orderIds: z
158
+ .array(z.string())
159
+ .describe('Order IDs to cancel. Pass [] to cancel ALL of the user\'s orders on this market.'),
160
+ };
161
+ /**
162
+ * Best-effort client-side notional estimate for the spend cap.
163
+ * Returns `null` when we can't estimate (e.g. unsized market bid).
164
+ */
165
+ function estimateBidNotional(args) {
166
+ if (args.side !== 'bid')
167
+ return 0;
168
+ if (typeof args.quoteQuantity === 'string')
169
+ return Number(args.quoteQuantity);
170
+ if (args.type === 'Limit' && typeof args.price === 'string' && typeof args.quantity === 'number') {
171
+ const price = Number(args.price);
172
+ if (!Number.isFinite(price))
173
+ return null;
174
+ return price * args.quantity;
175
+ }
176
+ return null;
177
+ }
178
+ export const orderTools = [
179
+ defineTool({
180
+ name: 'list_orders',
181
+ title: 'List open orders',
182
+ description: LIST_DESCRIPTION,
183
+ inputSchema: listSchema,
184
+ annotations: READ_ONLY,
185
+ handler: async (args, ctx) => {
186
+ if (args.outcome !== undefined && args.marketId === undefined) {
187
+ throw new Error('marketId is required when filtering orders by outcome.');
188
+ }
189
+ return ctx.client.request({
190
+ method: 'GET',
191
+ path: '/api/v1/orders',
192
+ query: args,
193
+ });
194
+ },
195
+ }),
196
+ defineTool({
197
+ name: 'place_order',
198
+ title: 'Place an order-book order',
199
+ description: PLACE_DESCRIPTION,
200
+ inputSchema: placeSchema,
201
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
202
+ handler: async (args, ctx) => {
203
+ // Cross-field rules the per-field zod schema can't express. Enforced
204
+ // before submitting the order.
205
+ if ((args.quantity === undefined) === (args.quoteQuantity === undefined)) {
206
+ throw new Error('Provide exactly one of quantity or quoteQuantity.');
207
+ }
208
+ if (args.type === 'Limit' && args.price === undefined) {
209
+ throw new Error('price is required for Limit orders.');
210
+ }
211
+ if ((args.type === 'StopLoss' || args.type === 'TakeProfit') && args.triggerPrice === undefined) {
212
+ throw new Error(`triggerPrice is required for ${args.type} orders.`);
213
+ }
214
+ let reserved = 0;
215
+ if (ctx.spendCap.effectiveCap() !== null) {
216
+ const notional = estimateBidNotional(args);
217
+ if (notional === null) {
218
+ throw new SpendCapExceededError(`Refusing place_order: a session spend cap is configured but the notional for this order can't be estimated client-side ` +
219
+ `(${args.type} ${args.side} without quoteQuantity). Set quoteQuantity, or use a Limit order with an explicit price+quantity, so the cap can supervise.`, ctx.spendCap.totalSpent(), ctx.spendCap.effectiveCap());
220
+ }
221
+ ctx.spendCap.reserve(notional, 'place_order');
222
+ reserved = notional;
223
+ }
224
+ try {
225
+ return await ctx.client.request({
226
+ method: 'POST',
227
+ path: '/api/v1/orders',
228
+ body: args,
229
+ });
230
+ }
231
+ catch (err) {
232
+ // Definitive server-side rejection (4xx) — nothing was spent, so give
233
+ // the reservation back. Ambiguous outcomes keep it.
234
+ if (reserved > 0 && err instanceof UnhedgedApiError && err.status >= 400 && err.status < 500) {
235
+ ctx.spendCap.release(reserved);
236
+ }
237
+ throw err;
238
+ }
239
+ },
240
+ }),
241
+ defineTool({
242
+ name: 'cancel_order',
243
+ title: 'Cancel one or more open orders',
244
+ description: CANCEL_DESCRIPTION,
245
+ inputSchema: cancelSchema,
246
+ // Destructive: an empty orderIds array cancels EVERY open order on the market.
247
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
248
+ handler: async (args, ctx) => {
249
+ return ctx.client.request({
250
+ method: 'DELETE',
251
+ path: '/api/v1/orders',
252
+ body: args,
253
+ });
254
+ },
255
+ }),
256
+ ];
@@ -0,0 +1,2 @@
1
+ import { type AnyToolDefinition } from './types.js';
2
+ export declare const portfolioTools: AnyToolDefinition[];
@@ -0,0 +1,34 @@
1
+ import { defineTool, READ_ONLY } from './types.js';
2
+ const DESCRIPTION = `
3
+ Read a summary of the user's portfolio: total bets, win rate, ROI, active-position exposure, pending order-book exposure, and recent payouts.
4
+
5
+ Returns a \`portfolio\` object with:
6
+ - \`summary\`: aggregate metrics — \`totalBets\`, \`totalWagered\`, \`totalWins\`, \`totalLosses\`, \`pendingBets\`, \`pendingOrderBookPositions\`, \`pendingOrderBookCostBasis\`, \`totalPayouts\`, \`totalProfit\`, \`winRate\` and \`roi\`. The last two are already percentage points (e.g. \`40\` means 40%); do not multiply them by 100.
7
+ - \`positions\`: a preview of up to 50 active/ended positions across markets, one entry per outcome held. This array is not a complete inventory when \`positionsTotal\` is greater than its length.
8
+ - \`positionsTotal\`: the complete active/ended position-row count available from the paginated REST endpoint \`/api/v1/portfolio/me/positions\`.
9
+ - \`recentPayouts\`: the 10 most recent settled payouts.
10
+
11
+ This MCP tool intentionally returns the bounded preview only; it has no position-pagination input. Use the paginated REST \`/api/v1/portfolio/me/positions\` and \`/api/v1/portfolio/me/history\` endpoints through an authorized API client when a complete inventory or historical result is required.
12
+
13
+ Cached at the API for ~30s so calling this repeatedly is cheap.
14
+
15
+ Requires scope: \`portfolio:read\`.
16
+
17
+ ### When to use
18
+ For periodic "how am I doing?" check-ins. As the first step in the \`portfolio-review\` workflow. Before recommending a \`cash-out\` to highlight the user's biggest exposures.
19
+ `.trim();
20
+ export const portfolioTools = [
21
+ defineTool({
22
+ name: 'get_portfolio',
23
+ title: 'Get portfolio summary',
24
+ description: DESCRIPTION,
25
+ inputSchema: {},
26
+ annotations: READ_ONLY,
27
+ handler: async (_args, ctx) => {
28
+ return ctx.client.request({
29
+ method: 'GET',
30
+ path: '/api/v1/portfolio/me',
31
+ });
32
+ },
33
+ }),
34
+ ];
@@ -0,0 +1,44 @@
1
+ import type { z, ZodRawShape } from 'zod';
2
+ import type { UnhedgedHttpClient } from '../client/http.js';
3
+ import type { UnhedgedMcpConfig } from '../config.js';
4
+ import type { SpendCapTracker } from '../safety/spend-cap.js';
5
+ export interface ToolContext {
6
+ client: UnhedgedHttpClient;
7
+ config: UnhedgedMcpConfig;
8
+ spendCap: SpendCapTracker;
9
+ }
10
+ /**
11
+ * MCP behavior hints (spec `ToolAnnotations`). Clients use these to gate
12
+ * approval flows — e.g. auto-approving read-only tools while requiring
13
+ * explicit confirmation for destructive ones.
14
+ */
15
+ export interface ToolAnnotations {
16
+ readOnlyHint?: boolean;
17
+ destructiveHint?: boolean;
18
+ idempotentHint?: boolean;
19
+ openWorldHint?: boolean;
20
+ }
21
+ export interface ToolDefinition<Shape extends ZodRawShape = ZodRawShape> {
22
+ name: string;
23
+ title: string;
24
+ description: string;
25
+ inputSchema: Shape;
26
+ annotations: ToolAnnotations;
27
+ handler: (args: z.infer<z.ZodObject<Shape>>, ctx: ToolContext) => Promise<unknown>;
28
+ }
29
+ export type AnyToolDefinition = {
30
+ name: string;
31
+ title: string;
32
+ description: string;
33
+ inputSchema: ZodRawShape;
34
+ annotations: ToolAnnotations;
35
+ handler: (args: unknown, ctx: ToolContext) => Promise<unknown>;
36
+ };
37
+ /** All Unhedged tools talk to a single closed API, never an open world. */
38
+ export declare const READ_ONLY: ToolAnnotations;
39
+ /**
40
+ * Define a tool with a narrow input schema while exposing it under the loose
41
+ * `AnyToolDefinition` shape used by the registry. The handler is type-safe
42
+ * because the cast happens once at registration time, not at each call site.
43
+ */
44
+ export declare function defineTool<Shape extends ZodRawShape>(def: ToolDefinition<Shape>): AnyToolDefinition;
@@ -0,0 +1,10 @@
1
+ /** All Unhedged tools talk to a single closed API, never an open world. */
2
+ export const READ_ONLY = { readOnlyHint: true, openWorldHint: false };
3
+ /**
4
+ * Define a tool with a narrow input schema while exposing it under the loose
5
+ * `AnyToolDefinition` shape used by the registry. The handler is type-safe
6
+ * because the cast happens once at registration time, not at each call site.
7
+ */
8
+ export function defineTool(def) {
9
+ return def;
10
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@unhedged/mcp",
3
+ "version": "0.1.2",
4
+ "description": "Model Context Protocol server for Unhedged. Connect Claude, Codex, Cursor, and other MCP clients to markets, balances, trading, and withdrawals through a scoped API key.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "unhedged-mcp": "dist/cli.js"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "skills",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "engines": {
18
+ "node": ">=20"
19
+ },
20
+ "scripts": {
21
+ "prepublishOnly": "node scripts/prepublish-check.mjs && npm run build",
22
+ "build": "rm -rf dist && tsc -p tsconfig.json",
23
+ "dev": "tsx src/cli.ts",
24
+ "start": "node dist/cli.js",
25
+ "check": "tsc --noEmit",
26
+ "test": "vitest",
27
+ "test:unit": "vitest run tests/unit",
28
+ "lint": "eslint src tests"
29
+ },
30
+ "dependencies": {
31
+ "@modelcontextprotocol/sdk": "^1.30.0",
32
+ "zod": "^4.3.6"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^26.2.0",
36
+ "@typescript-eslint/eslint-plugin": "^8.67.0",
37
+ "@typescript-eslint/parser": "^8.62.1",
38
+ "eslint": "^10.8.1",
39
+ "tsx": "^4.23.12",
40
+ "typescript": "^5.9.3",
41
+ "vitest": "^4.1.10"
42
+ },
43
+ "keywords": [
44
+ "mcp",
45
+ "model-context-protocol",
46
+ "unhedged",
47
+ "prediction-markets",
48
+ "ai-agent",
49
+ "claude",
50
+ "codex"
51
+ ],
52
+ "license": "MIT",
53
+ "homepage": "https://unhedged.gg/mcp"
54
+ }
@@ -0,0 +1,51 @@
1
+ ---
2
+ name: cash-out
3
+ description: Close open orders, optionally sell open positions at market, and prepare for withdrawal.
4
+ arguments:
5
+ - name: withdraw_to_wallet
6
+ description: If "true", actually request a withdrawal after closing. If "false" or omitted, stop after closing and let the user decide.
7
+ required: false
8
+ - name: destination_party_id
9
+ description: Canton party ID for the withdrawal (web2 users only). Ignored if withdraw_to_wallet is not "true".
10
+ required: false
11
+ ---
12
+
13
+ # Cash-out workflow
14
+
15
+ The user wants to wind down activity on Unhedged. Your job:
16
+ 1. Cancel every open order across every market.
17
+ 2. Optionally sell open CLOB positions at market.
18
+ 3. Optionally request a withdrawal.
19
+
20
+ Each step requires explicit confirmation before proceeding to the next, unless the user has clearly said "do it all".
21
+
22
+ ## Steps
23
+
24
+ 1. **Discover positions and every open order**.
25
+ - Call `get_portfolio` and note:
26
+ - `summary.pendingOrderBookPositions` — open CLOB positions.
27
+ - `summary.pendingBets` — open parimutuel bets (these can't be cashed out early; they wait for resolution).
28
+ - `positionsTotal` and `positions.length` — the portfolio tool returns only a preview of up to 50 active/ended rows, so do not treat that array as a complete CLOB inventory.
29
+ - Independently call `list_orders` without `marketId`, using `limit: 500` and `includeTotal: true`. Follow `nextCursor` until it is null. Do not infer the order inventory solely from portfolio positions: a completely unfilled resting bid can lock funds without creating a share position.
30
+
31
+ 2. **Cancel every resting order**. Group the account-wide order inventory by `marketId`. For each market, call `cancel_order` with `orderIds: []` (cancel-all on that market). Report back which markets were touched and how many orders were cancelled.
32
+
33
+ 3. **Sell CLOB positions** (only if the user said to). Obtain the complete CLOB position inventory first; `get_portfolio.positions` is only a preview and the current MCP toolset has no position-pagination input. Use an authorized REST client to page `/api/v1/portfolio/me/positions?marketType=ORDER_BOOK&limit=500&offset=...`, or stop and ask the user if that inventory is unavailable. For each open CLOB position, call `place_order` with:
34
+ - `side: "ask"`
35
+ - `type: "Market"`
36
+ - `quantity` set to the position's contract count.
37
+ Confirm with the user **before** issuing market sells — fills can be poor.
38
+
39
+ 4. **Wait for orders to clear**. Call `get_balance` and verify `lockedEngine` has dropped close to zero. If it has not, list orders account-wide again and cancel any remaining markets before rechecking. Don't withdraw while engine funds are still locked — the request will fail or be undersized.
40
+
41
+ 5. **Withdraw** if `withdraw_to_wallet` is `"true"`:
42
+ - Call `get_balance` to get current `available` and `withdrawalMinAmount`.
43
+ - Call `request_withdrawal` with `amount: available - withdrawalFee` and `destinationPartyId: {{destination_party_id}}` if supplied. Use the full available balance unless the user specified otherwise.
44
+ - Report the withdrawal ID and the queued status; settlement is asynchronous on-chain.
45
+
46
+ 6. **Confirm**. Summarize: orders cancelled, positions sold (if any), balance change, withdrawal queued (if any).
47
+
48
+ ## Guardrails
49
+
50
+ - **Parimutuel bets cannot be unwound** before market resolution. Don't try to "cancel" them — there's no API for it. Tell the user they're locked until the market resolves.
51
+ - If the API key lacks `balance:withdraw`, step 5 will return a 403. That's a *good* default — encourage the user to mint a new key with that scope only when they're ready.
@@ -0,0 +1,45 @@
1
+ ---
2
+ name: market-creator-assistant
3
+ description: Draft a new prediction market with appropriate outcomes, resolution rules, and metadata, then guide the user through submission.
4
+ arguments:
5
+ - name: idea
6
+ description: The user's idea for the market, in plain English (e.g. "will ETH ETF launch before July?").
7
+ required: true
8
+ ---
9
+
10
+ # Market creator workflow
11
+
12
+ > ⚠️ **Requirements:** This skill requires the `MARKET_CREATOR` role on the user's Unhedged account. The Role enum in the platform's database is `USER | MARKET_CREATOR | MARKET_MAKER | WHALEPLAY | SUPER_ADMIN`; if the user is just a `USER`, market creation endpoints return 403. Direct them to <contact@unhedged.gg> to request the role.
13
+
14
+ The user wants to create a market for: **{{idea}}**.
15
+
16
+ Note: this MCP package does **not** ship dedicated market-creation tools in v1 — those endpoints sit under `/api/v1/creator/*` and require additional metadata (image uploads, category tagging) that don't fit a single tool call. This skill walks the user through preparing the spec so they can submit it via the web UI at <https://unhedged.gg/creators/manage/create>.
17
+
18
+ ## Steps
19
+
20
+ 1. **Refine the question**. Reframe the user's idea as a single yes/no or multi-outcome question that has an objectively verifiable answer. Bad: "Will ETH do well in 2026?" Good: "Will the spot ETH price on Coinbase be above $5,000 USD at 00:00 UTC on 2026-12-31?"
21
+
22
+ 2. **Define outcomes**. For a binary market, that's YES and NO. For a range market, propose 3-5 buckets covering the realistic range plus an "Other / above" bucket. Show the user the outcome list and let them adjust.
23
+
24
+ 3. **Pick a resolution source**. For crypto prices, the standard is exchange 1-minute kline (candle) data via the platform's `exchange-kline` resolver. For sports, StatPal feeds. Esports resolve via GRID feeds, and there are dedicated resolvers for F1, golf, cricket, horse racing, and macro data (CPI, Fed rate, unemployment). For political / event markets, manual resolution by a moderator is the default. Recommend one and explain how the auto-resolver will read it.
25
+
26
+ 4. **Set the timeline**. `scheduledResolutionTime` is when the question is settled. `endTime` is when trading closes (often the same instant for time-based crypto questions; earlier for sports where the game outcome is known some minutes before resolution can complete).
27
+
28
+ 5. **Recommend a category and tags** drawn from the existing taxonomy: `crypto`, `sports`, `esports`, `politics`, `entertainment`, `tech`. Tags should be searchable terms users would type (e.g. "BTC", "ETH", "ETF", "NBA-finals").
29
+
30
+ 6. **Compose the spec** and present it to the user, ready to paste into <https://unhedged.gg/creators/manage/create>:
31
+ ```
32
+ Question: ...
33
+ Outcomes: ...
34
+ Resolution source: ... (specifics)
35
+ Resolution time: ... UTC
36
+ End time: ... UTC
37
+ Category: ...
38
+ Tags: [..., ...]
39
+ ```
40
+
41
+ ## Guardrails
42
+
43
+ - Don't promise the market will be approved — depending on platform policy, MARKET_CREATOR submissions may still go through a moderation queue.
44
+ - For oracle-resolved markets, the price source has to match a data feed the platform supports. Don't invent feed identifiers; if you're unsure, recommend the user check the docs at <https://unhedged.gg/docs>.
45
+ - Refuse to draft markets that obviously violate platform terms (markets on individuals' deaths, markets that would constitute insider trading on private events, etc.).
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: market-maker-workflow
3
+ description: Quote both sides of an Unhedged CLOB order book around a chosen fair value, refreshing as the market moves.
4
+ arguments:
5
+ - name: market_id
6
+ description: The CLOB market to make on. Parimutuel markets are not supported by this skill.
7
+ required: true
8
+ - name: outcome
9
+ description: The outcome label to quote (e.g. "YES" or "$95k - $100k").
10
+ required: true
11
+ - name: fair_value
12
+ description: The user's estimated fair probability for the outcome (between 0 and 1).
13
+ required: true
14
+ - name: spread
15
+ description: Half-spread in probability points. e.g. 0.02 = ±2%. Default 0.02.
16
+ required: false
17
+ - name: size_per_side
18
+ description: Notional USD per side, formatted as a string with exactly two decimals (e.g. "5.00"). Default "5.00".
19
+ required: false
20
+ ---
21
+
22
+ # Market-maker workflow
23
+
24
+ The user is quoting both sides of market **{{market_id}}**, outcome **{{outcome}}**, around fair value **{{fair_value}}** with a half-spread of {{spread}} and {{size_per_side}} USD per side.
25
+
26
+ ## Steps
27
+
28
+ 1. **Read the market-maker profile first**. Call `get_market_maker_profile` (no arguments). Confirm `approvalStatus` is `APPROVED`, and read `currentTier`, `effectiveMakerFee`/`effectiveTakerFee`, and `rebateRate` — use these to sanity-check that the chosen spread keeps quotes profitable after fees, and widen the half-spread toward the maker fee if the requested spread is too tight. Also note `orderRateLimit` so the refresh loop stays within it. If `approvalStatus` is not `APPROVED` (or the call 403s), stop and tell the user the account isn't an active market maker.
29
+
30
+ 2. **Snapshot the book**. Call `list_orders` with `marketId: "{{market_id}}"` and `outcome: "{{outcome}}"`. Note the user's current open orders on this outcome — you'll need to refresh them, not duplicate them.
31
+
32
+ 3. **Cancel stale quotes** with `cancel_order`. Pass the IDs of the user's existing orders on this outcome. If there are none, skip this step.
33
+
34
+ 4. **Place a bid** with `place_order`:
35
+ - `side: "bid"`
36
+ - `type: "Limit"`
37
+ - `price: fair_value - spread` (clamp to (0, 1) and round to 2 decimals)
38
+ - `quoteQuantity: {{size_per_side}}` — must be a string with exactly two decimals, e.g. `"5.00"`
39
+
40
+ 5. **Place an ask** with `place_order`:
41
+ - `side: "ask"`
42
+ - `type: "Limit"`
43
+ - `price: fair_value + spread` (same clamp + rounding)
44
+ - `quoteQuantity: {{size_per_side}}` — same two-decimal string format
45
+
46
+ 6. **Confirm** by listing orders again and reporting back the two new orders with their IDs, prices, and sizes.
47
+
48
+ ## Refresh loop
49
+
50
+ If the user asks to "refresh" or "re-quote", repeat steps 2-6. Each tool invocation receives a fresh idempotency key. If a placement fails ambiguously, check the account's transaction history before trying again: a filled order will be absent from open orders. Stop and ask the user if the outcome remains uncertain.
51
+
52
+ ## Guardrails
53
+
54
+ - This is single-outcome, single-pair quoting. If the user wants to quote multiple outcomes simultaneously, run this skill once per outcome.
55
+ - The session spend cap counts the bid side notional but not the ask side. Make sure the cap (`UNHEDGED_MAX_SPEND_PER_SESSION`) is generous enough for the planned cycle of re-quotes.
56
+ - Do **not** widen the spread silently to chase a fill. Tell the user what spread you're using and what the bid/ask resolve to.
57
+ - A 403 with "missing required scope: `order:place`" means the API key wasn't minted with that scope. Tell the user to mint a new key with `order:place` and `order:cancel`.
58
+ - **Cancel-on-disconnect (bots):** if an admin enabled `cancelOnDisconnect` on the account, long-running agents should call `POST /api/v1/orders/session/connect` at startup (UUID `sessionId`), `POST .../session/heartbeat` every ~30s, and `POST .../session/disconnect` on shutdown. API-key auth with `order:cancel` is required because the session lifecycle can cancel open CLOB orders when the last session ends.
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: market-research
3
+ description: Find Unhedged prediction markets matching a topic and summarize their odds, volume, and resolution rules.
4
+ arguments:
5
+ - name: topic
6
+ description: The topic or keyword to research (e.g. "BTC price 2026", "NBA finals", "US election").
7
+ required: true
8
+ - name: max_results
9
+ description: Maximum number of markets to summarize. Default 10.
10
+ required: false
11
+ ---
12
+
13
+ # Market research workflow
14
+
15
+ You are helping the user understand the prediction-market landscape on Unhedged around the topic: **{{topic}}**.
16
+
17
+ ## Steps
18
+
19
+ 1. **Search** with `search_markets` using `search: "{{topic}}"` and `limit` set to `max_results` (default 10). By default this returns ACTIVE and ENDED markets. Use `status: "ACTIVE"` for markets open to trading, or explicitly request `status: "RESOLVED"` for historical context.
20
+
21
+ 2. For the top markets returned, call `get_market` to pull the full outcome list and resolution rules. Pay attention to:
22
+ - The market `question` (this is what determines winning, not your interpretation).
23
+ - The `outcomes` array, with their indexes and labels.
24
+ - `autoResolutionConfig` if present — this tells you how the market resolves automatically (Pyth oracle, sports API, manual, etc).
25
+ - `scheduledResolutionTime` or `endTime` — when the market closes.
26
+
27
+ 3. For the most interesting candidates, call `get_market_stats` to retrieve current implied probabilities per outcome plus volume.
28
+
29
+ 4. **Summarize back to the user** in this shape:
30
+
31
+ For each market, present:
32
+ - The question, one line.
33
+ - Status + resolution time (relative, e.g. "resolves in 3 days").
34
+ - The outcomes with current implied probability (%) and notional pool size.
35
+ - One sentence on how it resolves (manual / oracle / sport feed / etc).
36
+ - One sentence on whether the odds look interesting, biased, or noisy — keep this honest, not promotional.
37
+
38
+ ## Guardrails
39
+
40
+ - Don't fabricate market IDs or outcome labels. If a market doesn't appear in the search results, say so.
41
+ - Don't recommend bets. This skill is **information-only**. If the user wants to actually place a position, escalate to the `place-hedged-position` skill.
42
+ - The market category may not exactly match the topic (e.g. "BTC" sits under `crypto`, "NBA" under `sports`). Use `search` rather than guessing categories.
@@ -0,0 +1,51 @@
1
+ ---
2
+ name: place-hedged-position
3
+ description: Calculate and place a hedged position across multiple outcomes of an Unhedged market.
4
+ arguments:
5
+ - name: market_id
6
+ description: The market to take a position on. The user can find this via the market-research skill.
7
+ required: true
8
+ - name: total_budget
9
+ description: Total USD to commit across the position. Must fit the available platform balance.
10
+ required: true
11
+ - name: thesis
12
+ description: One-line description of the user's thesis (e.g. "BTC closes above 100k by Sunday"). Used only in confirmation prose.
13
+ required: false
14
+ ---
15
+
16
+ # Hedged position workflow
17
+
18
+ The user wants to place a position on market **{{market_id}}** with a budget of **{{total_budget}} USD**. Their thesis: {{thesis}}.
19
+
20
+ A "hedged" position here means allocating across multiple outcomes — typically a primary bet on the thesis plus a smaller bet on the opposite outcome to cap downside. Your job is to size it sensibly and execute.
21
+
22
+ ## Steps
23
+
24
+ 1. **Pull the market**. Call `get_market` with `id: "{{market_id}}"` and `get_market_stats` for the current implied probabilities. Read out:
25
+ - The outcome labels and their indexes.
26
+ - Current implied probability per outcome (from stats).
27
+ - Whether the market is parimutuel (no order book, just pool sizes) or CLOB (has limit orders). The presence of `outcomeStats[].matchingEngineId` is a strong signal it's CLOB.
28
+
29
+ 2. **Check the user's balance**. Call `get_balance`. Confirm `available >= {{total_budget}}`. If not, say so and stop — do not place any partial position without checking with the user.
30
+
31
+ 3. **Compute the split**. A simple default:
32
+ - 70% on the thesis-aligned outcome.
33
+ - 20% on the most likely "hedge" outcome (usually the negation of the thesis).
34
+ - 10% reserved (do not place; just leave in available balance).
35
+ Adjust based on the implied probabilities: if the thesis-aligned outcome already trades at >75%, the hedge becomes less useful — propose dialling the hedge down to 10% and reserving 20%. If it trades below 50%, the hedge becomes more useful — propose 60/30/10.
36
+ Always show the user the proposed split **before** placing anything.
37
+
38
+ 4. **Place the bets**.
39
+ - For **parimutuel markets**: call `place_bet` once per outcome you're allocating to.
40
+ - For **CLOB markets**: call `place_order` with `type: "Limit"` and a price close to the current implied probability. Don't use `Market` orders for hedged positions — limit orders give the user a known cost basis.
41
+ The MCP server adds an `Idempotency-Key` automatically but does not retry ambiguous failures. If a call times out, check `list_bets` or `list_orders` to see whether it landed, then ask the user before retrying.
42
+
43
+ 5. **Confirm**. After both legs are placed (or attempted), call `get_balance` again and report:
44
+ - The new available balance.
45
+ - The two positions just opened, with their outcome labels and amounts.
46
+ - The total at risk and the maximum payout under each resolution.
47
+
48
+ ## Guardrails
49
+
50
+ - **Never** place a bet larger than the user explicitly authorized. If your split math suggests a leg larger than `total_budget`, stop and ask.
51
+ - If a tool returns a `SpendCapExceededError`, do not retry. The cap exists for a reason. Tell the user, summarize what's been placed so far, and stop.