@torque-labs/mcp 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +281 -0
  3. package/dist/index.js +368 -0
  4. package/package.json +72 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Torque Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,281 @@
1
+ # Torque MCP Server
2
+
3
+ An [MCP](https://modelcontextprotocol.io/) server that connects AI assistants to the [Torque](https://torque.so) incentive platform on Solana. It enables creating and managing reward programs for token teams — incentivizing swaps, bonding curve trades, token holds, on-chain program interactions, and off-chain events — all through natural language.
4
+
5
+ ## Quick Start
6
+
7
+ ### Claude Code
8
+
9
+ ```bash
10
+ claude mcp add torque -- npx @torque-labs/mcp
11
+ ```
12
+
13
+ With an auth token:
14
+
15
+ ```bash
16
+ claude mcp add torque -e TORQUE_API_TOKEN=your-token -- npx @torque-labs/mcp
17
+ ```
18
+
19
+ With a Solana private key:
20
+
21
+ ```bash
22
+ claude mcp add torque -- npx @torque-labs/mcp --privateKey your-base58-key
23
+ ```
24
+
25
+ ### Claude Desktop
26
+
27
+ Add to your `claude_desktop_config.json`:
28
+
29
+ ```json
30
+ {
31
+ "mcpServers": {
32
+ "torque": {
33
+ "command": "npx",
34
+ "args": ["@torque-labs/mcp"],
35
+ "env": {
36
+ "TORQUE_API_TOKEN": "your-auth-token"
37
+ }
38
+ }
39
+ }
40
+ }
41
+ ```
42
+
43
+ Or with a private key:
44
+
45
+ ```json
46
+ {
47
+ "mcpServers": {
48
+ "torque": {
49
+ "command": "npx",
50
+ "args": [
51
+ "@torque-labs/mcp",
52
+ "--privateKey", "your-base58-private-key"
53
+ ]
54
+ }
55
+ }
56
+ }
57
+ ```
58
+
59
+ ### Cursor
60
+
61
+ Add to your Cursor MCP settings:
62
+
63
+ ```json
64
+ {
65
+ "mcpServers": {
66
+ "torque": {
67
+ "command": "npx",
68
+ "args": ["@torque-labs/mcp"],
69
+ "env": {
70
+ "TORQUE_API_TOKEN": "your-auth-token"
71
+ }
72
+ }
73
+ }
74
+ }
75
+ ```
76
+
77
+ ### Global Install
78
+
79
+ ```bash
80
+ npm install -g @torque-labs/mcp
81
+ torque-mcp
82
+ ```
83
+
84
+ ## Prerequisites
85
+
86
+ - Node.js >= 20
87
+
88
+ ## Configuration
89
+
90
+ Configuration is resolved in priority order: CLI args > environment variables > defaults.
91
+
92
+ | CLI Arg | Environment Variable | Default | Description |
93
+ | --------------- | --------------------- | ---------------------------- | -------------------------------------------- |
94
+ | `--apiToken` | `TORQUE_API_TOKEN` | — | Auth token (takes priority over wallet auth) |
95
+ | `--privateKey` | `WALLET_PRIVATE_KEY` | — | Solana private key for wallet-based JWT auth |
96
+ | `--apiUrl` | `TORQUE_API_URL` | `https://server.torque.so` | Torque API base URL |
97
+ | `--platformUrl` | `TORQUE_PLATFORM_URL` | `https://platform.torque.so` | Torque platform URL |
98
+ | `--ingesterUrl` | `TORQUE_INGESTER_URL` | `https://ingest.torque.so` | Event ingestion endpoint |
99
+
100
+ You can provide either an auth token or a private key, or authenticate at runtime using the `authenticate` tool.
101
+
102
+ ## Tools
103
+
104
+ ### Authentication
105
+
106
+ | Tool | Description |
107
+ | ------------------- | ----------------------------------------------------- |
108
+ | `authenticate` | Authenticate with Torque using an auth token |
109
+ | `check_auth_status` | Check current authentication state and token validity |
110
+ | `logout` | Log out and clear stored authentication token |
111
+
112
+ ### API Keys
113
+
114
+ | Tool | Description |
115
+ | ---------------- | ----------------------------------------------------- |
116
+ | `list_api_keys` | List API keys for the Torque event ingestion pipeline |
117
+ | `create_api_key` | Create a new API key for sending custom events |
118
+
119
+ ### Projects
120
+
121
+ | Tool | Description |
122
+ | -------------------- | --------------------------------------------------------------- |
123
+ | `create_project` | Create a new project — collects token mint or program address based on project type |
124
+ | `list_projects` | List all projects and show which is currently active |
125
+ | `set_active_project` | Set the active project for subsequent project-scoped operations |
126
+
127
+ ### Custom Events
128
+
129
+ | Tool | Description |
130
+ | --------------------- | ----------------------------------------------------------------- |
131
+ | `create_custom_event` | Create a custom event schema for tracking off-chain user activity |
132
+ | `list_project_events` | List custom events attached to the active project |
133
+ | `list_custom_events` | List all custom events you've created across all projects |
134
+ | `attach_custom_event` | Attach an existing custom event to the active project |
135
+
136
+ ### IDL & On-Chain Programs
137
+
138
+ | Tool | Description |
139
+ | -------------------- | --------------------------------------------------------- |
140
+ | `parse_idl` | Parse a Solana Anchor IDL and show available instructions |
141
+ | `create_idl` | Upload an Anchor IDL and create instruction tracking |
142
+ | `create_instruction` | Add an instruction to an already-uploaded IDL |
143
+ | `list_idls` | List uploaded IDLs and their tracked instructions |
144
+
145
+ ### Incentive Query Building
146
+
147
+ | Tool | Description |
148
+ | ---------------------------- | ---------------------------------------------------------------------------------------------------------------- |
149
+ | `generate_incentive_query` | Generate SQL for any incentive source — token swaps, bonding curve trades, token holds, custom events, or IDL instructions |
150
+ | `preview_incentive_query` | Execute a query against real indexed data and preview results before creating an incentive |
151
+
152
+ ### Recurring Incentives
153
+
154
+ | Tool | Description |
155
+ | ----------------------------------- | -------------------------------------------------------------------------- |
156
+ | `create_recurring_incentive` | Create a recurring reward program (leaderboard, rebate, raffle, or direct). Raffles default to weighted tickets (WEIGHTED_BY_METRIC). |
157
+ | `list_recurring_incentives` | List all recurring incentives for the active project |
158
+ | `get_recurring_incentive` | Get full details of a specific recurring incentive |
159
+ | `get_recurring_incentive_analytics` | Get performance analytics for an incentive |
160
+
161
+ ### Context
162
+
163
+ | Tool | Description |
164
+ | ---------------- | ---------------------------------------------------------------- |
165
+ | `get_ai_context` | Fetch project documentation, domain knowledge, and configuration |
166
+
167
+ ## Prompts
168
+
169
+ Guided workflows that walk through multi-step processes interactively:
170
+
171
+ | Prompt | Description |
172
+ | ------------------- | ---------------------------------------------------------------------------------------------------- |
173
+ | `create-incentive` | Unified incentive creation — guides through action type selection, query building, preview, and reward configuration |
174
+
175
+ ## Resources
176
+
177
+ Static reference guides available to the LLM for context:
178
+
179
+ | Resource | Description |
180
+ | -------------------------- | ------------------------------------------------------------------------------ |
181
+ | `torque://capabilities` | Complete guide to all tools, prompts, and quick start instructions |
182
+ | `torque://workflows` | Common workflows and tool pipelines |
183
+ | `torque://incentive-types` | Incentive types (leaderboard, rebate, raffle, direct), formulas, and variables |
184
+
185
+ ## Workflows
186
+
187
+ ### Getting Started
188
+
189
+ 1. **Authenticate** — provide an auth token in config, or ask the assistant to authenticate
190
+ 2. **Select a project** — create or pick an existing project
191
+ 3. **Create an incentive** — use the `create-incentive` prompt for a guided workflow
192
+
193
+ ### Token Incentive (Zero Setup)
194
+
195
+ For incentivizing on-chain token activity — data is already indexed from Solana DEX protocols and launchpads.
196
+
197
+ 1. `generate_incentive_query` with `source: "swap"` / `"bonding_curve"` / `"hold"` — build the SQL query
198
+ 2. `preview_incentive_query` — (optional) validate results with real data
199
+ 3. `create_recurring_incentive` — create the reward program with the query
200
+
201
+ ### Custom Event Incentive
202
+
203
+ For off-chain activity (social actions, content creation, etc.).
204
+
205
+ 1. `create_custom_event` — define the event schema
206
+ 2. `attach_custom_event` — attach it to your project
207
+ 3. `generate_incentive_query` with `source: "custom_event"` — build the SQL query
208
+ 4. `preview_incentive_query` — (optional) validate results
209
+ 5. `create_recurring_incentive` — create the reward program
210
+
211
+ ### IDL Instruction Incentive
212
+
213
+ For on-chain program interactions not covered by token actions.
214
+
215
+ 1. `parse_idl` — parse the Anchor IDL to see available instructions
216
+ 2. `create_idl` — upload the IDL with selected instructions
217
+ 3. `generate_incentive_query` with `source: "idl_instruction"` — build the SQL query
218
+ 4. `preview_incentive_query` — (optional) validate results
219
+ 5. `create_recurring_incentive` — create the reward program
220
+
221
+ ## Development
222
+
223
+ ### Setup
224
+
225
+ ```bash
226
+ git clone https://github.com/torque-labs/mcp.git
227
+ cd mcp
228
+ npm install
229
+ npm run build
230
+ ```
231
+
232
+ ### Scripts
233
+
234
+ ```bash
235
+ npm run build # Build with tsup
236
+ npm run dev # Build with watch mode
237
+ npm start # Run the server (node dist/index.js)
238
+ npm test # Run all tests (vitest)
239
+ npm test -- tests/tools/auth.test.ts # Run a single test file
240
+ npm run test:watch # Tests in watch mode
241
+ npm run typecheck # TypeScript type checking
242
+ npm run lint # ESLint on src/ and tests/
243
+ npm run lint:fix # ESLint with auto-fix
244
+ npm run format # Prettier format
245
+ npm run format:check # Prettier check
246
+ ```
247
+
248
+ ### Evals
249
+
250
+ LLM-based evaluations that test whether an AI assistant selects the correct tools for given prompts. Uses [mcp-evals](https://github.com/modelcontextprotocol/mcp-evals) with Anthropic's Claude as the judge model.
251
+
252
+ ```bash
253
+ npm run eval # Run all evals
254
+ ```
255
+
256
+ Requires `ANTHROPIC_API_KEY` — set it as an environment variable or in a `.env` file at the project root.
257
+
258
+ ### Project Structure
259
+
260
+ ```
261
+ src/
262
+ index.ts # Entry point — parses config, starts server
263
+ server.ts # McpServer setup, tool/prompt/resource registration
264
+ types.ts # Shared types (TorqueContext, ToolHandler, etc.)
265
+ tools/ # Tool handlers (one file per domain)
266
+ prompts/ # Guided prompt workflows
267
+ resources/ # Static reference resources for LLM context
268
+ lib/ # Internal utilities (signing)
269
+ utils/ # Auth, config, guards, query builders, context
270
+ tests/
271
+ tools/ # Tool handler tests
272
+ utils/ # Utility tests
273
+ helpers.ts # Test helpers (createMockContext, getTextContent)
274
+ evals/
275
+ evals.yaml # Eval definitions
276
+ run.js # Eval runner script
277
+ ```
278
+
279
+ ## License
280
+
281
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,368 @@
1
+ #!/usr/bin/env node
2
+ function Z(t){let e=new Set(t);if(e.has("--help")||e.has("-h"))return{action:"help",params:{}};if(e.has("--version")||e.has("-v"))return{action:"version",params:{}};let r={};for(let n=0;n<t.length;n+=2){let o=t[n]?.replace("--",""),i=t[n+1];o&&i&&(r[o]=i)}return{action:"run",params:r}}function ee(){let t=`
3
+ Torque MCP Server \u2014 connect AI assistants to the Torque incentive platform on Solana.
4
+
5
+ Usage:
6
+ npx @torque-labs/mcp [options]
7
+
8
+ Options:
9
+ --apiToken <token> Auth token (takes priority over wallet auth)
10
+ --privateKey <key> Solana private key for wallet-based JWT auth
11
+ --apiUrl <url> Torque API base URL (default: https://server.torque.so)
12
+ --platformUrl <url> Torque platform URL (default: https://platform.torque.so)
13
+ --ingesterUrl <url> Event ingestion endpoint (default: https://ingest.torque.so)
14
+ --help, -h Show this help message
15
+ --version, -v Show version
16
+
17
+ Environment Variables:
18
+ TORQUE_API_TOKEN Auth token
19
+ WALLET_PRIVATE_KEY Solana private key
20
+ TORQUE_API_URL API base URL
21
+ TORQUE_PLATFORM_URL Platform URL
22
+ TORQUE_INGESTER_URL Ingestion endpoint
23
+
24
+ Examples:
25
+ claude mcp add torque -- npx @torque-labs/mcp
26
+ claude mcp add torque -e TORQUE_API_TOKEN=your-token -- npx @torque-labs/mcp
27
+ npx @torque-labs/mcp --apiToken your-token
28
+ `.trimStart();process.stderr.write(t)}function te(){process.stderr.write(`0.2.0
29
+ `)}var it="https://server.torque.so",st="https://platform.torque.so",at="https://ingest.torque.so",ct="Sign in to Torque";function re(t){let e=t.apiUrl??process.env.TORQUE_API_URL??it,r=t.platformUrl??process.env.TORQUE_PLATFORM_URL??st,n=t.ingesterUrl??process.env.TORQUE_INGESTER_URL??at,o=t.privateKey??process.env.WALLET_PRIVATE_KEY??void 0,i=t.apiToken??process.env.TORQUE_API_TOKEN??void 0;return{apiBaseUrl:e,platformUrl:r,ingesterUrl:n,signStatement:ct,privateKey:o,apiToken:i}}import{McpServer as lr}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as dr}from"@modelcontextprotocol/sdk/server/stdio.js";import{createKeyPairSignerFromBytes as ut,getBase58Encoder as lt,getTransactionDecoder as dt,getTransactionEncoder as pt,partiallySignTransaction as mt,signBytes as ft}from"@solana/kit";async function ne(t){let e=t.trim(),r;if(e.startsWith("["))try{let o=JSON.parse(e);r=new Uint8Array(o)}catch{throw new Error("Invalid byte array format for private key")}else try{let i=lt().encode(e);r=Uint8Array.from(i)}catch{throw new Error("Invalid private key format. Provide a base58-encoded key or JSON byte array.")}let n=await ut(r);return{address:n.address,async signMessage(o){let i=await ft(n.keyPair.privateKey,o);return new Uint8Array(i)},async signTransaction(o){let i=dt(),s=pt(),c=i.decode(o),a=await mt([n.keyPair],c);return new Uint8Array(s.encode(a))}}}import j from"fs/promises";import C from"path";var N=class{token=null;authToken=null;baseUrl;cacheDir;constructor(e,r){this.baseUrl=e,this.cacheDir=r||C.join(import.meta.dirname,"..",".cache")}getToken(){return this.authToken??this.token}setAuthToken(e){this.authToken=e}getAuthHeader(){let e=this.getToken();return e?{Authorization:`Bearer ${e}`}:null}async setToken(e,r){this.token=e,await this.persistToken(e,r)}async clearToken(){this.token=null,this.authToken=null;let e=C.join(this.cacheDir,"token.json");try{await j.unlink(e)}catch{}}async loadPersistedToken(){let e=C.join(this.cacheDir,"token.json");try{let r=await j.readFile(e,"utf-8"),n=JSON.parse(r);this.token=n.token}catch{}}async login(e,r){return this.performLogin(e,"",r)}async loginWithStatement(e,r,n){return this.performLogin(e,r,n)}async performLogin(e,r,n){let o=await fetch(`${this.baseUrl}/login`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({authType:"basic",pubKey:e,payload:{input:r,output:n}})});if(!o.ok){let s=await o.text();throw new Error(`Login failed (${o.status}): ${s}`)}let i=await o.json();if(!i.token||typeof i.token!="string")throw new Error("Login response missing token field");return i.token}async verify(){let e=this.getToken();if(!e)return!1;try{return(await fetch(`${this.baseUrl}/verify`,{method:"GET",headers:{Authorization:`Bearer ${e}`}})).ok}catch{return!1}}async persistToken(e,r){await j.mkdir(this.cacheDir,{recursive:!0});let n=C.join(this.cacheDir,"token.json"),o={token:e,publicKey:r,timestamp:Date.now()};await j.writeFile(n,JSON.stringify(o,null,2))}};function F(t){let e=new N(t.apiBaseUrl),r=null,n=null,o=null;return t.apiToken&&e.setAuthToken(t.apiToken),{getWallet:async()=>{if(r)return r;if(!t.privateKey)throw new Error("No private key configured. Set WALLET_PRIVATE_KEY env var or privateKey in MCP config.");return r=await ne(t.privateKey),r},auth:{login:(i,s)=>e.login(i,s),loginWithStatement:(i,s,c)=>e.loginWithStatement(i,s,c),verify:()=>e.verify(),getToken:()=>e.getToken(),setToken:(i,s)=>e.setToken(i,s),clearToken:()=>e.clearToken(),loadPersistedToken:()=>e.loadPersistedToken(),setAuthToken:i=>e.setAuthToken(i),getAuthHeader:()=>e.getAuthHeader()},get activeProject(){return n},setActiveProject:i=>{i?.id!==n?.id&&(o=null),n=i},get aiContextCache(){return o},set aiContextCache(i){o=i},apiRequest:async(i,s)=>{let c=e.getAuthHeader();if(!c)throw new Error("Not authenticated. Use the authenticate tool first or set TORQUE_API_TOKEN.");let a=`${t.apiBaseUrl}${i}`,u=await fetch(a,{method:s?.method??"GET",headers:{...c,...s?.body?{"Content-Type":"application/json"}:{}},...s?.body?{body:JSON.stringify(s.body)}:{}});if(!u.ok){let d=await u.text();throw new Error(`API request failed (${u.status}): ${d}`)}return(await u.json()).data},config:t}}import{z as ht}from"zod";import{getBase58Decoder as yt}from"@solana/kit";var P="\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501";function S(t){let e=`${t}/connect-mcp`;return["\u{1F510} **Authentication Required**","",`Visit [${e}](${e}) to create an account and get your auth token.`,"","Then use the `authenticate` tool with your auth token to connect."].join(`
30
+ `)}function oe(t,e){if(t.length===0)return["\u{1F4C1} **No projects found**","","Use `create_project` to create one."].join(`
31
+ `);let r=[`\u{1F4C1} **Your Projects** (${t.length})`,"","| Project | ID | Status |","|---|---|---|"];for(let n of t){let o=n.id===e?"\u2705 active":"";r.push(`| **${n.name}** | \`${n.id}\` | ${o} |`)}return r.join(`
32
+ `)}function ie(t){let e=["\u{1F4CB} **New Project Preview**",P,"",` Name: ${t.name}`];t.description&&e.push(` Description: ${t.description}`),t.image&&e.push(` Image: ${t.image}`),t.defaultTokenAddress&&e.push(` Token: ${t.defaultTokenAddress}`),t.defaultProgramAddress&&e.push(` Program: ${t.defaultProgramAddress}`);let r=t.adminWallets;if(r&&r.length>0){e.push(""),e.push(` Admins (${r.length}):`);for(let n of r)e.push(` ${n}`)}return e.push("",P,"\u26A0\uFE0F Confirm to proceed."),e.join(`
33
+ `)}function se(t){if(t.length===0)return["\u{1F4CB} **No custom events on this project**","","To get started:"," \u2022 Use `attach_custom_event` to attach an existing event"," \u2022 Use `create_custom_event` to create a new one first"].join(`
34
+ `);let e=[`\u{1F4CB} **Custom Events on Project** (${t.length})`,"","| Event | ID | Fields | Status |","|---|---|---|---|"];for(let r of t){let n=r.columnMapping?"\u2705 query-ready":"\u26A0\uFE0F awaiting first ingestion",o=r.fields.map(i=>`\`${i.fieldName}\` (${i.type})`).join(", ");e.push(`| **${r.name}** \`${r.eventName}\` | \`${r.id}\` | ${o} | ${n} |`)}return e.join(`
35
+ `)}function ae(t,e){if(t.length===0)return["\u{1F4CB} **No custom events found**","","Use `create_custom_event` to create one."].join(`
36
+ `);let r=[`\u{1F4CB} **Your Custom Events** (${t.length})`,"","| Event | ID | Fields | Projects |","|---|---|---|---|"];for(let n of t){let o=n.fields.map(s=>`\`${s.fieldName}\` (${s.type})`).join(", "),i;n.projects.length===0?i="\u2014":i=n.projects.map(s=>{let c=e?.get(s.projectId);return c?`**${c}**`:`\`${s.projectId}\``}).join(", "),r.push(`| **${n.name}** \`${n.eventName}\` | \`${n.id}\` | ${o} | ${i} |`)}return r.join(`
37
+ `)}function ce(t){let e=t.fields,r=["\u{1F4CB} **New Custom Event Preview**",P,"",` Event ID: \`${t.eventName}\``,` Name: ${t.name}`,"",` Fields (${e.length}):`];for(let n of e){let o=n.label?` \u2014 ${n.label}`:"";r.push(` \`${n.fieldName}\` (${n.type})${o}`)}return r.push("","\u2139\uFE0F Note: `userPubkey` (wallet address) is always required as a top-level property when sending events \u2014 it is not part of the event schema.","",P,"\u26A0\uFE0F Confirm to proceed."),r.join(`
38
+ `)}function ue(t,e,r,n){let o={userPubkey:"<wallet address>",timestamp:Date.now(),eventName:e,data:r.reduce((i,s)=>(i[s.fieldName]=`<${s.type.toLowerCase()}>`,i),{})};return["","\u{1F4E1} **Send events to the ingester**","",`**Endpoint:** \`POST ${t}/events\``,"","**Required headers:**","- `Content-Type: application/json`","- `x-api-key: <your-api-key>` \u2014 use `create_api_key` to generate one","","`userPubkey` (the user's wallet address) and `timestamp` are required top-level properties on every event. Your custom fields go inside `data`.","","```bash",`curl -X POST ${t}/events \\`,' -H "Content-Type: application/json" \\',' -H "x-api-key: <your-api-key>" \\',` -d '${JSON.stringify(o,null,2)}'`,"```","",`\u26A0\uFE0F **An API key is required to send events.** Use \`create_api_key\` to generate one, or manage your keys at [${n}/developer](${n}/developer).`].join(`
39
+ `)}function le(t,e){if(t.length===0)return["\u{1F511} **No API keys found**","","Use `create_api_key` to create one for sending events to the ingestion pipeline.","",`Manage API keys and view ingestion logs at [${e}/developer](${e}/developer).`].join(`
40
+ `);let r=[`\u{1F511} **Your API Keys** (${t.length})`,"","| Name | Key | Status | Last Used | Created |","|---|---|---|---|---|"];for(let n of t){let o=n.name??"\u2014",i=n.isActive?"\u2705 active":"\u274C revoked",s=n.lastUsedAt?new Date(n.lastUsedAt).toLocaleDateString():"never",c=new Date(n.createdAt).toLocaleDateString();r.push(`| ${o} | \`${n.key}\` | ${i} | ${s} | ${c} |`)}return r.push(""),r.push(`Manage API keys and view ingestion logs at [${e}/developer](${e}/developer).`),r.join(`
41
+ `)}function de(t,e){return["\u{1F511} **API Key Created**","",` Name: ${t.name??"\u2014"}`,` Key: \`${t.key}\``,"","\u26A0\uFE0F **Copy this key now \u2014 it will not be shown again.**","","Use this key in the `x-api-key` header when sending events to the ingestion pipeline.","",`Manage API keys and view ingestion logs at [${e}/developer](${e}/developer).`].join(`
42
+ `)}function pe(t){if(t.length===0)return["\u{1F4CA} **No recurring incentives found**","","Use `create_recurring_incentive` to create one, or use the `create-incentive` prompt for a guided workflow."].join(`
43
+ `);let e=[`\u{1F4CA} **Recurring Incentives** (${t.length})`,"","| Name | ID | Type | Epoch | Status |","|---|---|---|---|---|"];for(let r of t){let n=r.isActive?"\u2705 active":"\u23F8 inactive",o=r.evalDurationDays?`${r.evalDurationDays}-day epochs`:"\u2014";e.push(`| **${r.name}** | \`${r.id}\` | ${r.type} | ${o} | ${n} |`)}return e.join(`
44
+ `)}function me(t){let e=t.isActive?"\u2705 active":"\u23F8 inactive",r=[`\u{1F4CA} **${t.name||"Incentive Detail"}**`,"","| | |","|---|---|",`| **Status** | ${e} |`,`| **ID** | \`${t.id}\` |`];t.type&&r.push(`| **Type** | ${t.type} |`),t.evalDurationDays&&r.push(`| **Eval Period** | ${t.evalDurationDays} day(s) |`),t.startDate&&r.push(`| **Start** | ${t.startDate} |`),t.maxIterations&&r.push(`| **Max Epochs** | ${t.maxIterations} |`);let n=t.distributionConfig;n&&(n.distributionType&&r.push(`| **Distribution** | ${n.distributionType} |`),n.emissionType&&r.push(`| **Emission** | ${n.emissionType} |`),n.totalFundAmount!==void 0&&r.push(`| **Total Fund** | ${n.totalFundAmount} |`),n.customFormula&&r.push(`| **Formula** | \`${n.customFormula}\` |`));let o=t.offerMetadata;return o?.description&&(r.push(""),r.push(`${o.description}`)),r.join(`
45
+ `)}function fe(t){let e={leaderboard:"Leaderboard Competition",rebate:"Rebate",raffle:"Raffle",direct:"Direct Distribution"},r=t.type,n=["\u{1F4CB} **New Incentive Preview**",P,"",` Name: ${t.name}`,` Type: ${e[r]||r}`,` Emission: ${t.emissionType}`,` Eval Period: ${t.evalDurationDays} day(s)`,` Start: ${t.startDate}`];if(t.description&&n.push(` Description: ${t.description}`),r==="direct"){n.push(" Claim Window: 24 hours");let o=t.allocations;if(o&&o.length>0){let i=o.reduce((s,c)=>s+c.amount,0);n.push(""),n.push(` **Allocations** (${o.length} recipients)`);for(let s of o)n.push(` ${s.address} \u2192 ${s.amount}`);n.push(""),n.push(` Total Fund: ${i}`)}}else if(r==="raffle"){let o=t.raffleBuckets;if(o){let i=o.reduce((s,c)=>s+c.amount*c.count,0);n.push(""),n.push(" **Prize Tiers**");for(let s of o)n.push(` ${s.count}\xD7 winner(s) \u2192 ${s.amount} each`);n.push(""),n.push(` Total Fund: ${i}`)}}else r==="rebate"?(n.push(` Rebate: ${t.rebatePercentage}%`),n.push(` Total Fund: ${t.totalFundAmount}`)):n.push(` Total Fund: ${t.totalFundAmount}`);return t.sqlQuery&&(n.push(""),n.push(" **SQL Query**"),n.push(` \`${t.sqlQuery}\``)),t.customEventId&&n.push(` Event ID: ${t.customEventId}`),t.customFormula&&n.push(` Formula: ${t.customFormula}`),t.maxIterations&&n.push(` Max Epochs: ${t.maxIterations}`),t.maxPerParticipant&&n.push(` Max/User: ${t.maxPerParticipant}`),n.push("",P,"\u26A0\uFE0F Confirm to proceed."),n.join(`
46
+ `)}var he={name:"authenticate",description:"Authenticate with Torque. Accepts an auth token directly, or \u2014 if a private key is configured \u2014 automatically signs in using the wallet. If neither is available, returns instructions to get a token.",inputSchema:{authToken:ht.string().optional().describe("Torque auth token for authentication (optional if privateKey is configured)")},handler:async(t,e)=>{try{let r=t.authToken;if(r)return e.auth.setAuthToken(r),{content:[{type:"text",text:"\u2705 Successfully authenticated with auth token."}]};if(e.config.privateKey){let n=await e.getWallet(),o=new TextEncoder().encode(e.config.signStatement),i=await n.signMessage(o),s=yt().decode(i),c=await e.auth.loginWithStatement(n.address,e.config.signStatement,s);return await e.auth.setToken(c,n.address),{content:[{type:"text",text:`\u2705 Successfully authenticated with wallet \`${n.address}\`.`}]}}return{content:[{type:"text",text:S(e.config.platformUrl)}],isError:!0}}catch(r){return{content:[{type:"text",text:`Authentication failed: ${r instanceof Error?r.message:String(r)}`}],isError:!0}}}},ye={name:"check_auth_status",description:"Check if you are currently authenticated with Torque. Verifies the stored token with the server. ALWAYS call this automatically at the start of every session before attempting any other Torque operation. If not authenticated, use `authenticate`.",inputSchema:{},handler:async(t,e)=>e.auth.getToken()?await e.auth.verify()?{content:[{type:"text",text:"\u2705 You are authenticated. Your token is valid."}]}:{content:[{type:"text",text:`\u{1F504} Your token has expired or is invalid.
47
+
48
+ `+S(e.config.platformUrl)}]}:{content:[{type:"text",text:S(e.config.platformUrl)}]}},ge={name:"logout",description:"Log out from Torque. Clears the stored authentication token from this session and local cache.",inputSchema:{},handler:async(t,e)=>{try{return await e.auth.clearToken(),{content:[{type:"text",text:"Successfully logged out. Your authentication has been cleared."}]}}catch(r){return{content:[{type:"text",text:`Logout failed: ${r instanceof Error?r.message:String(r)}`}],isError:!0}}}};import{z as gt}from"zod";function h(t){return t.auth.getToken()?null:{content:[{type:"text",text:S(t.config.platformUrl)}],isError:!0}}function E(t){return t.activeProject?null:{content:[{type:"text",text:"\u{1F4CB} No active project set. Use `set_active_project` or `create_project` first."}],isError:!0}}var ve={name:"list_api_keys",description:"List your API keys for the Torque event ingestion pipeline. These keys are used in the `x-api-key` header when sending custom events to the ingester. Keys are shown masked \u2014 to manage or revoke keys, visit the developer dashboard. IMPORTANT: Always display the full list to the user in your response. IMPORTANT: Always include the developer dashboard link from the response so the user knows where to manage keys and view ingestion logs.",requires:["auth"],inputSchema:{},handler:async(t,e)=>{let r=h(e);if(r)return r;try{let n=await e.apiRequest("/api-keys"),o=Array.isArray(n)?n:n.data??[];return{content:[{type:"text",text:le(o,e.config.platformUrl)}]}}catch(n){return{content:[{type:"text",text:`\u274C Failed to list API keys: ${n instanceof Error?n.message:String(n)}`}],isError:!0}}}},we={name:"create_api_key",description:"Create a new API key for sending custom events to the Torque ingestion pipeline. The key is shown once upon creation \u2014 it cannot be retrieved later. Use this key in the `x-api-key` header when sending events via the ingester. IMPORTANT: Always include the developer dashboard link from the response so the user knows where to manage keys and view ingestion logs.",requires:["auth"],inputSchema:{name:gt.string().optional().describe("Optional display name for the API key. If omitted, the API generates a default name.")},handler:async(t,e)=>{let r=h(e);if(r)return r;try{let n={};t.name&&(n.name=t.name);let o=await e.apiRequest("/api-keys",{method:"POST",body:n});return{content:[{type:"text",text:de(o,e.config.platformUrl)}]}}catch(n){return{content:[{type:"text",text:`\u274C Failed to create API key: ${n instanceof Error?n.message:String(n)}`}],isError:!0}}}};import{z as R}from"zod";var vt=.05,be={name:"create_project",description:"Create a new project on Torque. Projects are the workspace for incentives, custom events, and analytics. The created project automatically becomes your active project for subsequent operations. IMPORTANT: Before creating a project, always ask the user what type of project this is: (1) Token team \u2014 they have a token and want to incentivize activity around it (swaps, holds, bonding curve). Ask for their token mint address and set it as `defaultTokenAddress`. (2) Protocol/product \u2014 they have an on-chain program and want to incentivize interactions with it. Ask for their program address and set it as `defaultProgramAddress`. They may also have a token \u2014 ask if they do and collect both if so. This ensures incentives can be created efficiently without asking for addresses every time.",requires:["auth"],inputSchema:{name:R.string().describe("Project name"),description:R.string().optional().describe("Project description"),image:R.string().optional().describe("Project image URL"),defaultTokenAddress:R.string().optional().describe("Default SPL token mint address for the project. This is the token they want to incentivize \u2014 used as the default for swap, hold, and bonding curve incentives. Essential for token teams."),defaultProgramAddress:R.string().optional().describe("Default on-chain program address for the project. This is the program they want to track interactions for \u2014 used for IDL instruction incentives. Typically provided by protocol/product teams."),adminWallets:R.array(R.string()).optional().describe("Wallet addresses to add as project admins"),confirmed:R.boolean().optional().describe("Set to true to confirm and execute the creation. Omit to preview first.")},handler:async(t,e)=>{let r=h(e);if(r)return r;if(!t.confirmed)return{content:[{type:"text",text:ie(t)}]};try{let n={name:t.name,protocolFee:vt};if(t.description&&(n.description=t.description),t.image&&(n.image=t.image),t.defaultTokenAddress&&(n.defaultTokenAddress=t.defaultTokenAddress),t.defaultProgramAddress&&(n.defaultProgramAddress=t.defaultProgramAddress),t.adminWallets){let i=t.adminWallets;n.roles=i.map(s=>({userPubKey:s,role:"ADMIN"}))}let o=await e.apiRequest("/projects",{method:"POST",body:n});return e.setActiveProject({id:o.id,name:o.name}),{content:[{type:"text",text:`Project "${o.name}" created successfully (ID: ${o.id}). It is now your active project.`}]}}catch(n){return{content:[{type:"text",text:`Failed to create project: ${n instanceof Error?n.message:String(n)}`}],isError:!0}}}},Te={name:"list_projects",description:"List all your Torque projects and show which one is currently active. AUTOMATICALLY call this after authentication succeeds to show the user their projects. Also use when the user mentions projects, wants to switch projects, or needs a project ID for `set_active_project`. If no project is active, suggest selecting one \u2014 most tools require an active project. IMPORTANT: Always display the full project list to the user in your response.",requires:["auth"],inputSchema:{},handler:async(t,e)=>{let r=h(e);if(r)return r;try{let n=await e.apiRequest("/projects"),o=Array.isArray(n)?n:n.data??[];return{content:[{type:"text",text:oe(o,e.activeProject?.id)}]}}catch(n){return{content:[{type:"text",text:`Failed to list projects: ${n instanceof Error?n.message:String(n)}`}],isError:!0}}}},Ee={name:"set_active_project",description:"Set the active project for subsequent operations. All project-scoped tools (events, incentives, analytics, context) operate on the active project. IMPORTANT: Before calling this tool, you MUST first call `list_projects` and display the full project list to the user so they can choose. Never ask 'which project?' without showing the list. Switching projects changes which data all subsequent tools operate on.",requires:["auth"],inputSchema:{projectId:R.string().describe("The project ID to set as active")},handler:async(t,e)=>{let r=h(e);if(r)return r;try{let n=t.projectId,o=await e.apiRequest(`/projects/${n}`);return e.setActiveProject({id:o.id,name:o.name}),{content:[{type:"text",text:`Active project set to "${o.name}" (ID: ${o.id}).`}]}}catch(n){return{content:[{type:"text",text:`Failed to set active project: ${n instanceof Error?n.message:String(n)}`}],isError:!0}}}};import{z as f}from"zod";var Ae="tokenswap_partitioned",_e="launchlabtrade_partitioned",ke="mplxgenesislaunchpooldepositwithdraw_partitioned",q="tokenbalance_partitioned",wt=[/'/,/"/,/;/,/--/,/\/\*/,/\\/];function I(t,e){for(let r of wt)if(r.test(t))throw new Error(`Invalid ${e}: contains forbidden characters. Value must not contain quotes, semicolons, comments, or backslashes.`);return t}function L(t){return{content:[{type:"text",text:`${t.description}
49
+
50
+ \`\`\`sql
51
+ ${t.sql}
52
+ \`\`\`
53
+
54
+ Use this query with \`create_recurring_incentive\` to set up the incentive.`},{type:"text",text:JSON.stringify({sql:t.sql,...t.metadata})}]}}function Q(t){return Array.isArray(t)?t:t.split(`
55
+ `).map(e=>e.trim()).filter(e=>e.length>0).map(e=>{let r=e.split(",");if(r.length<2)throw new Error(`Invalid allocation line: "${e}". Expected "address,amount".`);let n=r[0].trim(),o=Number(r[1].trim());if(isNaN(o))throw new Error(`Invalid amount in line: "${e}". Amount must be a number.`);return{address:n,amount:o}})}var B={leaderboard:{name:"leaderboard",displayName:"Leaderboard Competition",description:"Rank-based reward distribution. IMPORTANT: Always ask the user how they want rewards distributed before creating. Common patterns: fixed rank prizes (1st=10000, 2nd=5000, etc.), tiered ranges (top 10 get X, 11-50 get Y), or proportional splits (TOTAL_REWARD_POOL / TOTAL_PARTICIPANTS). The fallback formula N pays each user their raw metric value \u2014 this is rarely desired since it would e.g. pay a trader back their full volume. Build a customFormula from the user's stated payout intent.",type:"LEADERBOARD",distributionType:"FORMULA",customFormula:"N",primitive:{type:"COMPETITION"},requiredInputs:[]},rebate:{name:"rebate",displayName:"Rebate",description:"Percentage-based return on a tracked value. The MCP builds the formula automatically from rebatePercentage (e.g. 5% \u2192 VALUE * 0.05). The query determines what value is measured (spend, volume, etc.).",type:"REBATE",distributionType:"FORMULA",formulaTemplate:"VALUE * {rebateMultiplier}",primitive:{type:"REBATE"},requiredInputs:["rebatePercentage"]},raffle:{name:"raffle",displayName:"Raffle",description:"Random winner selection from qualifying users. Requires prize tier configuration (raffleBuckets). Default weighting is WEIGHTED_BY_METRIC \u2014 each user's query metric value determines their ticket count (more activity = more raffle entries). Use raffleWeighting: 'EQUAL_CHANCES' for equal odds regardless of metric value.",type:"RAFFLE",distributionType:"RAFFLE",primitive:{type:"RAFFLE"},requiredInputs:["raffleBuckets"]},direct:{name:"direct",displayName:"Direct Distribution",description:"Fixed amounts to specific wallet addresses. Provide a list of allocations \u2014 the MCP constructs an inline SQL query and uses FORMULA distribution internally. Recipients must claim their rewards.",type:"LEADERBOARD",distributionType:"FORMULA",customFormula:"N",primitive:{type:"COMPETITION"},requiredInputs:["allocations"]}};function bt(t,e){if(!(t in B))return`Unknown type "${t}". Valid types: leaderboard, rebate, raffle, direct. See the torque://incentive-types resource for details.`;let r=B[t];for(let o of r.requiredInputs)if(e[o]===void 0||e[o]===null)return`Type "${t}" requires "${o}" to be provided.`;if(e.emissionType==="TOKENS"){if(!e.tokenAddress)return"emissionType TOKENS requires tokenAddress to be provided.";if(e.tokenDecimals===void 0)return"emissionType TOKENS requires tokenDecimals to be provided."}return null}function Tt(t,e){let r=new Date(e.startDate),n=e.evalDurationDays,o=new Date(r.getTime()+n*24*60*60*1e3),i={distributionType:t.distributionType,distributionMethod:"CLAIM",claimWindowStart:o.toISOString(),claimWindowDuration:604800,emissionType:e.emissionType,totalFundAmount:e.totalFundAmount};e.maxPerParticipant!==void 0&&(i.maxPerParticipant=e.maxPerParticipant),e.emissionType==="TOKENS"&&(i.tokenAddress=e.tokenAddress,i.tokenDecimals=e.tokenDecimals);let s=e.customFormula;if(t.name==="leaderboard")i.customFormula=s||"N";else if(t.name==="rebate"){let c=e.rebatePercentage;i.customFormula=s||`VALUE * ${c/100}`}else if(t.name==="raffle"){let c=e.raffleBuckets;i.prizeBuckets=c,i.totalFundAmount=c.reduce((u,l)=>u+l.amount*l.count,0);let a=e.raffleWeighting;i.selectionLogic=a==="EQUAL_CHANCES"?"EQUAL_CHANCES":"WEIGHTED_BY_METRIC",i.allowDuplicateWinners=!1}else if(t.name==="direct"){let c=Q(e.allocations),a=e.customFormula;i.customFormula=a||"N",i.totalFundAmount=c.reduce((u,l)=>u+l.amount,0)}return i}var Re={DAILY:1,WEEKLY:7,MONTHLY:30};function Et(t){if(t.evalDurationDays)return t.evalDurationDays;let e=t.interval;return e&&e in Re?Re[e]:null}var Ie={name:"create_recurring_incentive",requires:["auth","activeProject"],description:"Create a recurring incentive (reward program) for the active project. Consider calling `get_ai_context` first to understand the project's domain. An incentive has three layers: (1) a query that defines how users are tracked and measured \u2014 use `generate_incentive_query` with the appropriate `source` param (swap, bonding_curve, hold, custom_event, or idl_instruction), or provide raw SQL via `sqlQuery`, (2) a type that determines reward distribution \u2014 leaderboard (rank-based), rebate (percentage-back), raffle (random winners), or direct (fixed allocations), and (3) reward configuration (tokens/SOL, amounts, frequency). IMPORTANT for leaderboard: Always ask the user how they want rewards distributed before creating. Common payout patterns: fixed rank prizes ('1st gets 10000, 2nd gets 5000'), tiered ranges ('top 50 split the pool'), or proportional splits. Build a `customFormula` from their stated intent \u2014 do not silently default to `N` which pays raw metric values. IMPORTANT for raffle: Ask the user how raffle tickets should be distributed \u2014 WEIGHTED_BY_METRIC (default) means users with higher query values (e.g., more volume) get more tickets and higher chances of winning, while EQUAL_CHANCES means every qualifying user gets exactly 1 ticket regardless of their metric value. The query results must be sorted (highest value first) for weighted raffles to work correctly. See `torque://incentive-types` resource for formula variables, type details, and examples. For custom event incentives: list_project_events \u2192 generate_incentive_query (source: \"custom_event\") \u2192 create_recurring_incentive. For IDL instruction incentives: list_idls \u2192 generate_incentive_query (source: \"idl_instruction\") \u2192 create_recurring_incentive.",inputSchema:{name:f.string().describe("Display name for the recurring incentive"),description:f.string().optional().describe("Optional description"),type:f.string().describe("Incentive type: 'leaderboard', 'rebate', 'raffle', or 'direct'. Determines the reward distribution shape and what additional inputs are required."),emissionType:f.enum(["TOKENS","SOL"]).describe("Type of reward emission"),tokenAddress:f.string().optional().describe("SPL token mint address (required if emissionType is TOKENS)"),tokenDecimals:f.number().optional().describe("Token decimal places (required if emissionType is TOKENS)"),totalFundAmount:f.number().describe("Total reward pool per epoch (ignored for raffle \u2014 calculated from buckets)"),evalDurationDays:f.number().optional().describe("Duration of each evaluation period in days. Common values: 1 (daily), 7 (weekly), 30 (monthly). This is how long user activity is tracked before rewards are distributed. Either this or interval must be provided. Takes priority over interval if both given."),interval:f.enum(["DAILY","WEEKLY","MONTHLY"]).optional().describe("Shorthand for evalDurationDays: DAILY=1, WEEKLY=7, MONTHLY=30. Either this or evalDurationDays must be provided."),startDate:f.string().describe("When the first epoch's evaluation period begins (ISO 8601 date string)"),maxIterations:f.number().optional().describe("Maximum number of epochs to run. Omit for unlimited."),maxPerParticipant:f.number().optional().describe("Maximum reward per user per epoch. Useful for capping rewards."),rebatePercentage:f.number().optional().describe("Rebate percentage (required if type is 'rebate'). E.g. 5 for 5%."),raffleBuckets:f.array(f.object({amount:f.number(),count:f.number()})).optional().describe("Prize tiers for raffle (required if type is 'raffle'). Array of {amount, count}."),raffleWeighting:f.enum(["WEIGHTED_BY_METRIC","EQUAL_CHANCES"]).optional().describe("How raffle tickets are assigned. Default: WEIGHTED_BY_METRIC \u2014 each user's query metric value determines their number of tickets (e.g., a user with 100 volume gets 100 tickets vs 10 for someone with 10 volume). Use EQUAL_CHANCES for equal odds regardless of metric value (1 ticket per qualifying user)."),allocations:f.union([f.string(),f.array(f.object({address:f.string(),amount:f.number()}))]).optional().describe("Wallet allocations for direct distribution (required if type is 'direct'). Either a CSV string (address,amount per line) or an array of {address, amount}."),sqlQuery:f.string().optional().describe("Custom SQL query generated by generate_incentive_query. When provided, replaces the default stub query."),customEventId:f.string().optional().describe("ID of the custom event this incentive is based on. Mutually exclusive with instructionId."),instructionId:f.string().optional().describe("ID of the IDL instruction this incentive is based on. Mutually exclusive with customEventId."),customFormula:f.string().optional().describe("Reward formula that produces the direct payout amount for each user. Each user receives exactly what the formula evaluates to, capped by the remaining reward pool (users are processed highest-value first). Variables: N/VALUE (user's metric), RANK (1-based position), INDEX (0-based), TOTAL_PARTICIPANTS, TOTAL_REWARD_POOL. Functions: sqrt, pow, abs, floor, ceil, round, min, max, log, exp. Common patterns: 'RANK == 1 ? 10000 : RANK == 2 ? 5000 : RANK <= 10 ? 1000 : 0' (tiered rank prizes), 'TOTAL_REWARD_POOL / TOTAL_PARTICIPANTS' (equal split), 'min(N * 0.1, 500)' (10% of metric, capped at 500). For leaderboards: always ask the user how they want payouts structured and build the formula from their intent. The fallback N pays raw metric values which is rarely desired (e.g. would pay back full trade volume). For rebates: omit this \u2014 the formula is auto-built from rebatePercentage. See `torque://incentive-types` resource for full reference."),confirmed:f.boolean().optional().describe("Set to true to confirm and execute the creation. Omit to preview first.")},handler:async(t,e)=>{let r=h(e);if(r)return r;let n=E(e);if(n)return n;let o=t.type,i=bt(o,t);if(i)return{content:[{type:"text",text:i}],isError:!0};let s=Et(t);if(!s)return{content:[{type:"text",text:"\u274C Either `evalDurationDays` or `interval` (DAILY, WEEKLY, MONTHLY) must be provided."}],isError:!0};if(t.evalDurationDays=s,!t.confirmed){let u={...t};return o==="direct"&&t.allocations&&(u={...u,allocations:Q(t.allocations)}),{content:[{type:"text",text:fe(u)}]}}let c=B[o],a=e.activeProject.id;try{if(o==="direct"&&t.allocations){let T=Q(t.allocations).map(v=>`('${I(v.address,"address")}', ${v.amount})`).join(", ");t.sqlQuery=`SELECT address, value FROM (VALUES ${T}) AS t(address, value)`}let u=await e.apiRequest(`/project/${a}/query`,{method:"POST",body:{name:`${t.name} - Query - ${Date.now()}`,query:t.sqlQuery||"SELECT 1",paramMap:{startDate:"DATE",endDate:"DATE"}}}),l=Tt(c,t),d={title:t.name,primitive:c.primitive};t.description&&(d.description=t.description);let m={name:t.name,type:c.type,evalDurationDays:s,startDate:t.startDate,sqlQueryId:u.id,distributionConfig:l,offerMetadata:d,metricColumn:"value",addressColumn:"address"};t.description&&(m.description=t.description),t.maxIterations!==void 0&&(m.maxIterations=t.maxIterations),t.customEventId&&(m.customEventId=t.customEventId),t.instructionId&&(m.instructionId=t.instructionId);let p=await e.apiRequest(`/project/${a}/recurring-offer`,{method:"POST",body:m});return{content:[{type:"text",text:`Recurring incentive "${p.name||t.name}" created successfully (ID: ${p.id}). Fund it here: ${e.config.platformUrl}/${a}/incentives`}]}}catch(u){return{content:[{type:"text",text:`Failed to create recurring incentive: ${u instanceof Error?u.message:String(u)}`}],isError:!0}}}},xe={name:"list_recurring_incentives",requires:["auth","activeProject"],description:"List all recurring incentives (reward programs) for the active project. Shows name, type, status, and epoch duration. Use this when the user asks about their incentives, rewards, campaigns, leaderboards, or reward programs. Also use when the user says 'show me what I have', 'what incentives exist', or 'how are my rewards doing'. Provides incentive IDs needed for `get_recurring_incentive` and `get_recurring_incentive_analytics`. IMPORTANT: Always display the full incentive list to the user.",inputSchema:{},handler:async(t,e)=>{let r=h(e);if(r)return r;let n=E(e);if(n)return n;let o=e.activeProject.id;try{let i=await e.apiRequest(`/project/${o}/recurring-offer`);return{content:[{type:"text",text:pe(Array.isArray(i)?i:[])}]}}catch(i){return{content:[{type:"text",text:`Failed to list recurring incentives: ${i instanceof Error?i.message:String(i)}`}],isError:!0}}}},Pe={name:"get_recurring_incentive",requires:["auth","activeProject"],description:"Get full details of a specific recurring incentive \u2014 configuration, distribution settings, current epoch status, and query info. Use this when the user wants to inspect or understand how a specific incentive is set up. Use `list_recurring_incentives` to find the incentive ID.",inputSchema:{recurringOfferId:f.string().describe("The recurring incentive ID")},handler:async(t,e)=>{let r=h(e);if(r)return r;let n=E(e);if(n)return n;let o=e.activeProject.id,i=t.recurringOfferId;try{let s=await e.apiRequest(`/project/${o}/recurring-offer/${i}`);return{content:[{type:"text",text:me(s)}]}}catch(s){return{content:[{type:"text",text:`Failed to get recurring incentive: ${s instanceof Error?s.message:String(s)}`}],isError:!0}}}},Se={name:"get_recurring_incentive_analytics",requires:["auth","activeProject"],description:"Get performance analytics for a recurring incentive \u2014 participation counts, rewards distributed, and epoch-by-epoch history. Use this when the user asks how an incentive is performing, wants stats, or needs to evaluate results. Consider calling `get_ai_context` for project domain context when interpreting results. Use `list_recurring_incentives` to find the incentive ID.",inputSchema:{recurringOfferId:f.string().describe("The recurring incentive ID")},handler:async(t,e)=>{let r=h(e);if(r)return r;let n=E(e);if(n)return n;let o=e.activeProject.id,i=t.recurringOfferId;try{let s=await e.apiRequest(`/project/${o}/recurring-offer/${i}/analytics`);return{content:[{type:"text",text:JSON.stringify(s,null,2)}]}}catch(s){return{content:[{type:"text",text:`Failed to get recurring incentive analytics: ${s instanceof Error?s.message:String(s)}`}],isError:!0}}}};import{z as k}from"zod";var De={name:"create_custom_event",description:"Create a custom event schema for tracking off-chain user activity (e.g. purchases, game actions, signups). Use this when the user wants to track activity that isn't on-chain. Events are independent of projects \u2014 they can be attached to one or multiple projects via `attach_custom_event`. Does NOT require an active project \u2014 do not prompt the user to select a project before creating an event. If called during an existing workflow (e.g. incentive creation), continue that workflow after creation. RULES: (1) Never include wallet/pubkey as a field \u2014 `userPubkey` is a required top-level event property, not a schema field. (2) Confirm the `eventName` with the user before creating \u2014 suggest a snake_case version of the display name (e.g. 'Sign Up' \u2192 'sign_up'), but let them choose. (3) After creation: if an active project exists, offer to attach the event to it by name. If no active project, ask if they want to attach to a project \u2014 if yes, call `list_projects` to let them choose, then `set_active_project` + `attach_custom_event`.",requires:["auth"],inputSchema:{eventName:k.string().describe("Identifier for the custom event"),name:k.string().describe("Display name for the custom event"),confirmed:k.boolean().optional().describe("Set to true to confirm and execute the creation. Omit to preview first."),fields:k.array(k.object({fieldName:k.string().describe("Field name"),type:k.enum(["string","boolean","number"]).describe("Field data type"),label:k.string().optional().describe("Display label for the field"),description:k.string().optional().describe("Description of what this field represents")})).describe("Fields that define the event schema")},handler:async(t,e)=>{let r=h(e);if(r)return r;let n=t.fields;if(!n.length)return{content:[{type:"text",text:"Custom event must have at least one field."}],isError:!0};if(!t.confirmed)return{content:[{type:"text",text:ce(t)}]};try{let o=await e.apiRequest("/events",{method:"POST",body:{eventName:t.eventName,name:t.name,fields:n}}),i=ue(e.config.ingesterUrl,t.eventName,n,e.config.platformUrl),s=e.activeProject,c=s?`
56
+
57
+ \u{1F4A1} Would you like to attach this event to project "${s.name}"? Use \`attach_custom_event\` to attach it.`:"\n\n\u{1F4A1} Would you like to attach this event to a project? Use `list_projects` to see your projects, then `set_active_project` and `attach_custom_event` to attach it.";return{content:[{type:"text",text:`Custom event "${t.eventName}" created successfully (ID: ${o.id}). Manage your events and get your API key at ${e.config.platformUrl}/developer
58
+ ${i}${c}`},{type:"text",text:JSON.stringify({id:o.id,eventName:t.eventName})}]}}catch(o){return{content:[{type:"text",text:`Failed to create custom event: ${o instanceof Error?o.message:String(o)}`}],isError:!0}}}},$e={name:"list_project_events",description:'List custom events attached to the active project, including events from any project admin. Shows event schemas, fields, and query-readiness status (events must be ingested at least once before they can be used in queries). Use this to find events available for `generate_incentive_query` (with `source: "custom_event"`) when building incentives. To see your own events across all projects, use `list_custom_events` instead. IMPORTANT: Always display the full event list to the user in your response (preferably as a table) so they can see and select an event.',requires:["auth","activeProject"],inputSchema:{},handler:async(t,e)=>{let r=h(e);if(r)return r;let n=E(e);if(n)return n;let o=e.activeProject.id;try{let i=await e.apiRequest(`/projects/${o}/events`),s=i.data??i;return{content:[{type:"text",text:se(s)}]}}catch(i){return{content:[{type:"text",text:`Failed to list project events: ${i instanceof Error?i.message:String(i)}`}],isError:!0}}}},je={name:"list_custom_events",description:"List all custom events you have created across all projects. Shows schemas, fields, and which projects they are attached to. Use this to browse your own events \u2014 for example, when you want to find one to attach to a project with `attach_custom_event`. To see events already on a project (including events from other admins), use `list_project_events` instead. IMPORTANT: Always display the full event list to the user in your response (preferably as a table) so they can see and select an event.",requires:["auth"],inputSchema:{},handler:async(t,e)=>{let r=h(e);if(r)return r;try{let[n,o]=await Promise.all([e.apiRequest("/events"),e.apiRequest("/projects").catch(()=>({}))]),i=n.data??n,s=new Map,c=Array.isArray(o)?o:[];for(let a of c)a.id&&a.name&&s.set(a.id,a.name);return{content:[{type:"text",text:ae(i,s)}]}}catch(n){return{content:[{type:"text",text:`Failed to list custom events: ${n instanceof Error?n.message:String(n)}`}],isError:!0}}}},Ce={name:"attach_custom_event",description:'Attach an existing custom event to the active project so it can be used in incentives. The event must already exist \u2014 use `list_custom_events` to find available events, or `create_custom_event` to create one first. After attaching, the event needs to receive data via ingestion before it becomes query-ready for `generate_incentive_query` (with `source: "custom_event"`).',requires:["auth","activeProject"],inputSchema:{customEventId:k.string().describe("ID of the custom event to attach. Use list_custom_events to find available events, or create_custom_event to create a new one first.")},handler:async(t,e)=>{let r=h(e);if(r)return r;let n=E(e);if(n)return n;let o=e.activeProject.id,i=t.customEventId;try{return await e.apiRequest(`/projects/${o}/events/${i}/attach`,{method:"POST"}),{content:[{type:"text",text:`\u2705 Event attached to project "${e.activeProject.name}" successfully. Use \`list_project_events\` to see all events on this project.`}]}}catch(s){let c=s instanceof Error?s.message:String(s);return c.includes("403")?{content:[{type:"text",text:"\u274C You need to be a project admin to attach events to this project."}],isError:!0}:{content:[{type:"text",text:`\u274C Failed to attach event: ${c}`}],isError:!0}}}};import{z as b}from"zod";var W="customevent_partitioned",At=new Set(["id","ingestionId","eventName","apiKeyId","eventId","userPubkey","projectIds","num_val_1","num_val_2","num_val_3","num_val_4","num_val_5","num_val_6","num_val_7","num_val_8","num_val_9","num_val_10","str_val_1","str_val_2","str_val_3","str_val_4","str_val_5","bool_val_1","bool_val_2","bool_val_3","bool_val_4","bool_val_5","data","receivedAt","createdAt"]);function H(t){return t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function _t(t){return t.replace(/\\/g,"\\\\").replace(/'/g,"''")}function Ne(t,e){let r=[],n=new Map;for(let s of e.fields){let c=e.columnMapping[s.fieldName];c&&n.set(s.fieldName,{column:c,type:s.type})}let o=Array.from(n.keys()).sort((s,c)=>c.length-s.length),i=t;for(let s of o)if(new RegExp(`\\b${H(s)}\\b`,"g").test(i)){let{column:a,type:u}=n.get(s);r.push({fieldName:s,column:a,type:u}),i=i.replace(new RegExp(`\\b${H(s)}\\b`,"g"),`"${a}"`)}return{resolved:i,referencedFields:r}}var kt=new Set(["SUM","AVG"]);function qe(t,e){let r=[],n=/\b(SUM|AVG|COUNT|MIN|MAX)\s*\(\s*([^)]+)\s*\)/gi,o;for(;(o=n.exec(t))!==null;){let i=o[1].toUpperCase(),s=o[2].trim();if(s!=="*")for(let c of e.fields)new RegExp(`\\b${H(c.fieldName)}\\b`).test(s)&&kt.has(i)&&c.type!=="number"&&r.push(`Cannot use ${i}() on "${c.fieldName}" \u2014 it is a ${c.type} field, not a NUMBER.`)}return r}function Le(t,e){let r=[],n=[],o=qe(e.valueExpression,t);r.push(...o);let{resolved:i}=Ne(e.valueExpression,t),s=/^\s*[\d.]+\s*$/.test(e.valueExpression),c=/\bCOUNT\s*\(\s*\*\s*\)/i.test(e.valueExpression);!s&&!c&&i===e.valueExpression&&(/\b(num_val_|str_val_|bool_val_)\d+\b/.test(i)||n.push(`Value expression "${e.valueExpression}" did not match any known fields. Available fields: ${t.fields.map(T=>T.fieldName).join(", ")}`));let a=[];if(e.filters)for(let y of e.filters){let T=qe(y,t);r.push(...T);let{resolved:v}=Ne(y,t);a.push(v)}if(r.length>0)return{errors:r};let u=e.groupByPubkey!==!1,l=e.orderBy||"DESC",d=[' "userPubkey" AS address',` ${i} AS value`],m=[`"eventId" = '${_t(t.id)}'`,'"receivedAt" BETWEEN {{startDate}} AND {{endDate}}',...a],p=`SELECT
59
+ ${d.join(`,
60
+ `)}
61
+ FROM ${W}
62
+ WHERE ${m.join(`
63
+ AND `)}`;return u&&(p+=`
64
+ GROUP BY "userPubkey"`),p+=`
65
+ ORDER BY value ${l}`,e.limit!==void 0&&e.limit>0&&(p+=`
66
+ LIMIT ${e.limit}`),{sql:p,warnings:n}}var Rt=/\b(INSERT|UPDATE|DELETE|DROP|ALTER|TRUNCATE|CREATE)\b/i;function Oe(t,e){let r=[],n=[];/^\s*SELECT\b/i.test(t)||r.push("Query must be a SELECT statement."),Rt.test(t)&&r.push("Query contains forbidden keywords (INSERT, UPDATE, DELETE, DROP, ALTER, TRUNCATE, CREATE)."),t.includes(";")&&r.push("Query must not contain semicolons (multi-statement prevention)."),/\bUNION\b/i.test(t)&&r.push("Query must not contain UNION (cross-table prevention)."),t.includes(W)||r.push(`Query must reference the "${W}" table.`),(!/\{\{startDate\}\}/i.test(t)||!/\{\{endDate\}\}/i.test(t))&&r.push("Query must include {{startDate}} and {{endDate}} parameters for the evaluation period filter."),/["']?userPubkey["']?/i.test(t)||r.push('Query must select "userPubkey" (aliased as "address") for participant identification.'),/["']?eventId["']?\s*=/i.test(t)||r.push(`Query must include an "eventId" filter for event scoping. Add: AND "eventId" = '${e.id}'`),/\bAS\s+value\b/i.test(t)||n.push('Query should alias the metric column as "value" for reward calculation.');let o=/"(num_val_\d+|str_val_\d+|bool_val_\d+)"/g,i;for(;(i=o.exec(t))!==null;)At.has(i[1])||r.push(`Referenced column "${i[1]}" does not exist in the table schema.`);let s=new Map;for(let a of e.fields){let u=e.columnMapping[a.fieldName];u&&s.set(u,a.type)}let c=/\b(SUM|AVG)\s*\(\s*"?([\w]+)"?\s*\)/gi;for(;(i=c.exec(t))!==null;){let a=i[1].toUpperCase(),u=i[2],l=s.get(u);l&&l!=="number"&&r.push(`Cannot use ${a}() on column "${u}" \u2014 it maps to a ${l} field.`)}return{valid:r.length===0,errors:r,warnings:n}}function It(t,e){if(e.isRawQuery)return`Custom SQL query against "${t}" events. Review the SQL below to understand what it does.`;let r=e.valueExpression??"",n=[],o=r.match(/^SUM\((\w+)\)(?:\s*\*\s*([\d.]+))?$/i),i=r.match(/^COUNT\(\*\)$/i),s=r.match(/^AVG\((\w+)\)$/i);if(i)n.push(`Counts how many "${t}" events each user triggered`);else if(o){let c=o[1],a=o[2];a?n.push(`Calculates ${a}x the total "${c}" per user from "${t}" events`):n.push(`Calculates the total "${c}" per user from "${t}" events`)}else s?n.push(`Calculates the average "${s[1]}" per user from "${t}" events`):r==="1"?n.push(`Gives a flat value of 1 to every user who triggered a "${t}" event`):n.push(`Evaluates "${r}" per user from "${t}" events`);if(e.filters&&e.filters.length>0){let c=e.filters.map(a=>`"${a}"`);n.push(`Only includes events where ${c.join(" and ")}`)}if(e.limit){let c=e.orderBy==="ASC"?"bottom":"top";n.push(`Returns the ${c} ${e.limit} users`)}return n.join(". ")+"."}function xt(t){let e=[t.eventName,t.name,...t.fields.map(r=>r.fieldName)].join(" ").toLowerCase();return/purchase|order|checkout|buy|cart|shop|spend|price|product|merchant|store|payment/.test(e)?"commerce":/swap|trade|stake|lend|borrow|mint|bridge|deposit|withdraw|liquidity|volume|fee|pool|yield|apy|tvl|token_pair/.test(e)?"defi":/game|match|play|score|level|round|battle|kill|win|xp|achievement|quest|player|rank/.test(e)?"gaming":/referral|signup|share|post|comment|like|invite|follow|view|watch|stream|engage/.test(e)?"social":"generic"}function Pt(t,e){let r=t.name.toLowerCase(),n=xt(t),o=e?.valueExpression??"",i=o.match(/^SUM\((\w+)\)/i),s=o.match(/^COUNT\(\*\)$/i),c;s?c=`number of ${r} events`:i?c=`total ${i[1]}`:o==="1"?c="participation":c="calculated value";let a=[`Each user's **${c}** from the query above will be used to calculate their reward.`,"Describe how you'd like rewards distributed \u2014 here are some ideas:",""];if(i){let u=i[1];n==="commerce"?(a.push(`- "Give shoppers cashback \u2014 5% of their total ${u}"`),a.push('- "Leaderboard: top 50 spenders share the reward pool"'),a.push(`- "Loyalty reward proportional to ${u}, capped at 500 tokens per customer"`),a.push('- "Everyone who spent over $10 gets an equal share of the pool"')):n==="defi"?(a.push(`- "Rebate based on trading ${u} \u2014 give back 2% of their volume"`),a.push(`- "Top 100 traders by ${u} share the reward pool"`),a.push(`- "Proportional to ${u} but cap rewards at 10,000 tokens per wallet"`),a.push(`- "Liquidity mining: split the pool based on each user's share of total ${u}"`)):n==="gaming"?(a.push(`- "Top players by ${u} win prizes \u2014 leaderboard style"`),a.push(`- "Reward proportional to ${u}, with diminishing returns for top ranks"`),a.push(`- "Bonus for every 100 ${u} earned"`),a.push(`- "All players with ${u} above 0 split the pool equally"`)):n==="social"?(a.push(`- "Reward based on engagement \u2014 proportional to their ${u}"`),a.push(`- "Top 100 contributors by ${u} share the pool"`),a.push(`- "Everyone who contributed gets a share, weighted by ${u}"`)):(a.push(`- "Give each user 5% of their ${u} back as a reward"`),a.push(`- "Top 50 users by ${u} share the reward pool"`),a.push(`- "Reward proportional to ${u}, but cap at 1000 tokens per user"`),a.push(`- "Split the pool equally among everyone with ${u} above 0"`))}else s?n==="commerce"?(a.push('- "Reward repeat customers \u2014 10 tokens per purchase"'),a.push('- "Top 100 most frequent shoppers share the pool"'),a.push('- "Everyone who made at least 3 purchases gets an equal share"')):n==="defi"?(a.push('- "Reward active traders \u2014 bonus for each transaction"'),a.push('- "Top 100 most active wallets share the pool"'),a.push('- "Everyone who traded at least once gets an equal share"')):n==="gaming"?(a.push('- "Play-to-earn: reward for every match completed"'),a.push('- "Most active players share the prize pool"'),a.push('- "Everyone who played at least 5 matches enters a raffle"')):n==="social"?(a.push('- "Reward for every post or share \u2014 more activity, more reward"'),a.push('- "Top referrers share the pool"'),a.push('- "Everyone who invited at least one friend gets tokens"')):(a.push(`- "Reward based on how many ${r} events they triggered"`),a.push(`- "5 tokens for every ${r} event"`),a.push('- "Top 100 most active users share the pool"')):o==="1"?n==="commerce"?(a.push('- "Everyone who made a purchase gets an equal reward"'),a.push('- "Raffle among all customers \u2014 random winners get prizes"')):n==="defi"?(a.push('- "Airdrop to everyone who interacted with the protocol"'),a.push('- "Equal distribution among all active wallets"')):n==="gaming"?(a.push('- "Participation reward \u2014 everyone who played gets tokens"'),a.push('- "Raffle among all players for bonus prizes"')):n==="social"?(a.push('- "Reward everyone who participated equally"'),a.push('- "Raffle among all engaged users"')):(a.push('- "Split the pool equally among all participants"'),a.push('- "Raffle among all qualifying users"')):(a.push(`- "Reward proportional to their ${c}"`),a.push('- "Split the pool equally among qualifying users"'),a.push('- "Top performers get more, with a cap per user"'));return a.join(`
67
+ `)}function Ue(t,e,r,n){let o=["\u{1F4CA} Generated SQL Query Preview","\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501",`Event: ${e.name} (${e.eventName})`];n&&(o.push(""),o.push(`**What this does:** ${It(e.eventName,n)}`)),o.push("","```sql",t,"```","","Field Mapping:");for(let i of e.fields){let s=e.columnMapping[i.fieldName];o.push(` ${i.fieldName} (${i.type}) \u2192 ${s}`)}if(r.length>0){o.push(""),o.push("\u26A0\uFE0F Warnings:");for(let i of r)o.push(` - ${i}`)}return o.push(""),o.push("**Next step: How should rewards be calculated?**"),o.push(""),o.push(Pt(e,n)),o.push("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),o.push("\u26A0\uFE0F Review the query above and confirm to proceed."),o.join(`
68
+ `)}var G="mapped_custom_instruction",St=new Set(["id","signature","slot","programId","instructionName","feePayer","idlId","instructionId",...Array.from({length:15},(t,e)=>`num_val_${e+1}`),...Array.from({length:15},(t,e)=>`str_val_${e+1}`),...Array.from({length:10},(t,e)=>`bool_val_${e+1}`),...Array.from({length:30},(t,e)=>`acc_val_${e+1}`),"receivedAt","createdAt","updatedAt","innerIxIndex","ixIndex","stackHeight"]);function K(t){return t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function Dt(t){return t.replace(/\\/g,"\\\\").replace(/'/g,"''")}function Me(t,e){let r=[],n=new Map;for(let[s,c]of Object.entries(e.accountMapping))n.set(s,{column:c,type:"string"});for(let s of e.instruction){let c=e.fieldMapping[s.fieldName];c&&n.set(s.fieldName,{column:c,type:s.type})}let o=Array.from(n.keys()).sort((s,c)=>c.length-s.length),i=t;for(let s of o)if(new RegExp(`\\b${K(s)}\\b`,"g").test(i)){let{column:a,type:u}=n.get(s);r.push({fieldName:s,column:a,type:u}),i=i.replace(new RegExp(`\\b${K(s)}\\b`,"g"),`"${a}"`)}return{resolved:i,referencedFields:r}}var $t=new Set(["SUM","AVG"]);function Fe(t,e){let r=[],n=/\b(SUM|AVG|COUNT|MIN|MAX)\s*\(\s*([^)]+)\s*\)/gi,o;for(;(o=n.exec(t))!==null;){let i=o[1].toUpperCase(),s=o[2].trim();if(s!=="*")for(let c of e)new RegExp(`\\b${K(c.fieldName)}\\b`).test(s)&&$t.has(i)&&c.type!=="number"&&r.push(`Cannot use ${i}() on "${c.fieldName}" \u2014 it is a ${c.type} field, not a NUMBER.`)}return r}function Qe(t,e){let r=[],n=[],o=Fe(e.valueExpression,t.instruction);r.push(...o);let{resolved:i}=Me(e.valueExpression,t),s=/^\s*[\d.]+\s*$/.test(e.valueExpression),c=/\bCOUNT\s*\(\s*\*\s*\)/i.test(e.valueExpression);!s&&!c&&i===e.valueExpression&&(/\b(num_val_|str_val_|bool_val_|acc_val_)\d+\b/.test(i)||n.push(`Value expression "${e.valueExpression}" did not match any known fields. Available fields: ${t.instruction.map(T=>T.fieldName).join(", ")}`));let a=[];if(e.filters)for(let y of e.filters){let T=Fe(y,t.instruction);r.push(...T);let{resolved:v}=Me(y,t);a.push(v)}if(r.length>0)return{errors:r};let u=e.groupByFeePayer!==!1,l=e.orderBy||"DESC",d=[' "feePayer" AS address',` ${i} AS value`],m=[`"instructionId" = '${Dt(t.id)}'`,'"receivedAt" BETWEEN {{startDate}} AND {{endDate}}',...a],p=`SELECT
69
+ ${d.join(`,
70
+ `)}
71
+ FROM ${G}
72
+ WHERE ${m.join(`
73
+ AND `)}`;return u&&(p+=`
74
+ GROUP BY "feePayer"`),p+=`
75
+ ORDER BY value ${l}`,{sql:p,warnings:n}}var jt=/\b(INSERT|UPDATE|DELETE|DROP|ALTER|TRUNCATE|CREATE)\b/i;function Be(t,e){let r=[],n=[];/^\s*SELECT\b/i.test(t)||r.push("Query must be a SELECT statement."),jt.test(t)&&r.push("Query contains forbidden keywords (INSERT, UPDATE, DELETE, DROP, ALTER, TRUNCATE, CREATE)."),t.includes(";")&&r.push("Query must not contain semicolons (multi-statement prevention)."),/\bUNION\b/i.test(t)&&r.push("Query must not contain UNION (cross-table prevention)."),t.includes(G)||r.push(`Query must reference the "${G}" table.`),(!/\{\{startDate\}\}/i.test(t)||!/\{\{endDate\}\}/i.test(t))&&r.push("Query must include {{startDate}} and {{endDate}} parameters for the evaluation period filter."),/["']?feePayer["']?/i.test(t)||r.push('Query must select "feePayer" (aliased as "address") for participant identification.'),/["']?instructionId["']?\s*=/i.test(t)||r.push('Query must include an "instructionId" filter for instruction scoping.'),/\bAS\s+value\b/i.test(t)||n.push('Query should alias the metric column as "value" for reward calculation.');let o=/"(num_val_\d+|str_val_\d+|bool_val_\d+|acc_val_\d+)"/g,i;for(;(i=o.exec(t))!==null;)St.has(i[1])||r.push(`Referenced column "${i[1]}" does not exist in the table schema.`);let s=new Map;for(let a of e.instruction){let u=e.fieldMapping[a.fieldName];u&&s.set(u,a.type)}let c=/\b(SUM|AVG)\s*\(\s*"?([\w]+)"?\s*\)/gi;for(;(i=c.exec(t))!==null;){let a=i[1].toUpperCase(),u=i[2],l=s.get(u);l&&l!=="number"&&r.push(`Cannot use ${a}() on column "${u}" \u2014 it maps to a ${l} field.`)}return{valid:r.length===0,errors:r,warnings:n}}function Ct(t,e){if(e.isRawQuery)return`Custom SQL query against "${t}" instructions. Review the SQL below to understand what it does.`;let r=e.valueExpression??"",n=[],o=r.match(/^SUM\((\w+)\)(?:\s*\*\s*([\d.]+))?$/i),i=r.match(/^COUNT\(\*\)$/i),s=r.match(/^AVG\((\w+)\)$/i);if(i)n.push(`Counts how many "${t}" instructions each user executed`);else if(o){let c=o[1],a=o[2];a?n.push(`Calculates ${a}x the total "${c}" per user from "${t}" instructions`):n.push(`Calculates the total "${c}" per user from "${t}" instructions`)}else s?n.push(`Calculates the average "${s[1]}" per user from "${t}" instructions`):r==="1"?n.push(`Gives a flat value of 1 to every user who executed a "${t}" instruction`):n.push(`Evaluates "${r}" per user from "${t}" instructions`);if(e.filters&&e.filters.length>0){let c=e.filters.map(a=>`"${a}"`);n.push(`Only includes instructions where ${c.join(" and ")}`)}return n.join(". ")+"."}function We(t,e,r,n,o){let i=r.displayName?` \u2014 ${r.displayName}`:"",s=["\u{1F4CA} Generated SQL Query Preview","\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501",`Program: ${r.name} (\`${r.programAddress}\`)${i}`,`Instruction: ${e.instructionName}`];o&&(s.push(""),s.push(`**What this does:** ${Ct(e.instructionName,o)}`)),s.push("","```sql",t,"```","","Field Mapping:");for(let a of e.instruction){let u=e.fieldMapping[a.fieldName];s.push(` ${a.fieldName} (${a.type}) \u2192 ${u}`)}let c=Object.entries(e.accountMapping);if(c.length>0){s.push(""),s.push("Account Mapping:");for(let[a,u]of c)s.push(` ${a} \u2192 ${u}`)}if(n.length>0){s.push(""),s.push("\u26A0\uFE0F Warnings:");for(let a of n)s.push(` - ${a}`)}return s.push(""),s.push("**Next step: How should rewards be calculated?**"),s.push(""),s.push(Nt(e,o)),s.push("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"),s.push("\u26A0\uFE0F Review the query above and confirm to proceed."),s.join(`
76
+ `)}function Nt(t,e){let r=t.instructionName,n=e?.valueExpression??"",o=n.match(/^SUM\((\w+)\)/i),i=n.match(/^COUNT\(\*\)$/i),s;i?s=`number of ${r} instructions`:o?s=`total ${o[1]}`:n==="1"?s="participation":s="calculated value";let c=[`Each user's **${s}** from the query above will be used to calculate their reward.`,"Describe how you'd like rewards distributed \u2014 here are some ideas:",""];if(o){let a=o[1];c.push(`- "Rebate based on ${a} \u2014 give back 2% of their volume"`),c.push(`- "Top 100 users by ${a} share the reward pool"`),c.push(`- "Proportional to ${a} but cap rewards at 10,000 tokens per wallet"`),c.push(`- "Split the pool based on each user's share of total ${a}"`)}else i?(c.push(`- "Reward active users \u2014 bonus for each ${r} executed"`),c.push('- "Top 100 most active wallets share the pool"'),c.push(`- "Everyone who executed at least one ${r} gets an equal share"`)):n==="1"?(c.push('- "Airdrop to everyone who interacted with the program"'),c.push('- "Equal distribution among all active wallets"')):(c.push(`- "Reward proportional to their ${s}"`),c.push('- "Split the pool equally among qualifying users"'),c.push('- "Top performers get more, with a cap per user"'));return c.join(`
77
+ `)}var He={raydium_launchlab:{table:_e,identifierColumn:"poolState",identifierParam:"poolState",amountColumn:"amountIn",volumeExpression:'SUM("amountIn")',directionMap:{buy:"buy",sell:"sell"},label:"Raydium LaunchLab"},metaplex_genesis:{table:ke,identifierColumn:"genesisAccount",identifierParam:"genesisAccount",amountColumn:"amountQuoteToken",volumeExpression:'SUM(COALESCE("usdAmount", "uiAmountQuoteToken"))',directionMap:{buy:"deposit",sell:"withdraw"},label:"Metaplex Genesis"}};function qt(t,e){return['SELECT "owner" AS address, "amount" AS value',"FROM (",' SELECT "owner", "amount",',' ROW_NUMBER() OVER (PARTITION BY "owner" ORDER BY "createdAt" DESC) AS rn',` FROM ${q}`,` WHERE "mint" = '${t}'`,` AND "amount" >= ${e}`,' AND "createdAt" BETWEEN {{startDate}} AND {{endDate}}',") sub","WHERE rn = 1","ORDER BY value DESC"].join(`
78
+ `)}function Lt(t,e){return['SELECT "owner" AS address, COUNT(DISTINCT DATE("createdAt")) AS value',`FROM ${q}`,`WHERE "mint" = '${t}'`,` AND "amount" >= ${e}`,' AND "createdAt" BETWEEN {{startDate}} AND {{endDate}}','GROUP BY "owner"',"ORDER BY value DESC"].join(`
79
+ `)}function Ot(t,e){return['SELECT "owner" AS address, COUNT(*) AS value',`FROM ${q}`,`WHERE "mint" = '${t}'`,` AND "amount" >= ${e}`,' AND "createdAt" BETWEEN {{startDate}} AND {{endDate}}','GROUP BY "owner"',"ORDER BY value DESC"].join(`
80
+ `)}var Ut={balance:qt,duration:Lt,count:Ot},Mt={balance:"latest qualifying token balance per wallet",duration:"number of distinct days with qualifying balance",count:"number of qualifying balance records"},Y={swap:["volume","count"],bonding_curve:["volume","count"],hold:["balance","duration","count"]};function g(t){return{isError:!0,content:[{type:"text",text:`\u274C ${t}`}]}}function Ft(t){let e=t.tokenMint;if(!e)return g("tokenMint is required for swap source.");let r=t.direction??"buy",n=t.minUsdAmount,o=t.protocol,i=t.measure;try{I(e,"tokenMint"),o&&I(o,"protocol")}catch(d){return g(d.message)}let s;r==="buy"?s=`"tokenOut" = '${e}'`:r==="sell"?s=`"tokenIn" = '${e}'`:s=`("tokenIn" = '${e}' OR "tokenOut" = '${e}')`;let c=i==="volume"?'SUM("usdAmount") AS value':"COUNT(*) AS value",a=[s,'"receivedAt" BETWEEN {{startDate}} AND {{endDate}}'];n!==void 0&&a.push(`"usdAmount" >= ${n}`),o&&a.push(`"protocol" = '${o}'`);let u=[`SELECT "feePayer" AS address, ${c}`,`FROM ${Ae}`,`WHERE ${a.join(`
81
+ AND `)}`,'GROUP BY "feePayer"',"ORDER BY value DESC"].join(`
82
+ `),l=`\u2705 Generated swap query for token \`${e}\`
83
+
84
+ **Direction:** ${r}
85
+ **Measure:** ${i}${i==="volume"?" (USD)":" (transactions)"}
86
+ `+(n!==void 0?`**Min USD per swap:** ${n}
87
+ `:"")+(o?`**Protocol:** ${o}
88
+ `:"**Protocol:** all");return L({description:l,sql:u,metadata:{source:"swap",tokenMint:e,direction:r,measure:i,minUsdAmount:n,protocol:o}})}function Qt(t){let e=t.launchpad;if(!e||!(e in He))return g("launchpad is required for bonding_curve source. Use 'raydium_launchlab' or 'metaplex_genesis'.");let r=He[e],n=e==="raydium_launchlab"?t.poolState:t.genesisAccount;if(!n)return g(`${r.identifierParam} is required for ${r.label}. This identifies the specific token's bonding curve.`);try{I(n,r.identifierParam)}catch(d){return g(d.message)}let o=t.direction??"buy",i=t.minAmountIn,s=t.measure,c=s==="volume"?`${r.volumeExpression} AS value`:"COUNT(*) AS value",a=[`"${r.identifierColumn}" = '${n}'`,'"receivedAt" BETWEEN {{startDate}} AND {{endDate}}'];if(o!=="both"){let d=r.directionMap[o];a.push(`"direction" = '${d}'`)}i!==void 0&&a.push(`"${r.amountColumn}" >= ${i}`);let u=[`SELECT "feePayer" AS address, ${c}`,`FROM ${r.table}`,`WHERE ${a.join(`
89
+ AND `)}`,'GROUP BY "feePayer"',"ORDER BY value DESC"].join(`
90
+ `),l=`\u2705 Generated bonding curve query for ${r.label}
91
+
92
+ **${r.identifierParam}:** \`${n}\`
93
+ **Direction:** ${o}${o!=="both"?` (mapped to '${r.directionMap[o]??o}')`:""}
94
+ **Measure:** ${s}`;return L({description:l,sql:u,metadata:{source:"bonding_curve",launchpad:e,[r.identifierParam]:n,direction:o,measure:s,minAmountIn:i}})}function Bt(t){let e=t.tokenMint;if(!e)return g("tokenMint is required for hold source.");let r=t.minBalance;if(r==null)return g("minBalance is required for hold source.");if(r<=0)return g("minBalance must be greater than 0. A hold incentive without a minimum balance threshold would include all wallets.");let n=t.measure;try{I(e,"tokenMint")}catch(s){return g(s.message)}let o=Ut[n](e,r),i=`\u2705 Generated hold query for token \`${e}\`
95
+
96
+ **Min balance:** ${r} (raw amount)
97
+ **Measure:** ${n} \u2014 ${Mt[n]}`;return L({description:i,sql:o,metadata:{source:"hold",tokenMint:e,minBalance:r,measure:n}})}async function Wt(t,e){let r=t.customEventId;if(!r)return g("customEventId is required for custom_event source.");let n=e.activeProject.id,o;try{let l=await e.apiRequest(`/projects/${n}/events`),m=(l.data??l).find(p=>p.id===r);if(!m)return g("Event not found on this project. Use `list_project_events` to see available events, or `attach_custom_event` to attach one.");if(!m.columnMapping)return g("This event does not have column mappings yet. Events need to be ingested at least once before queries can be generated.");o=m}catch(l){return g(`Failed to fetch project events: ${l instanceof Error?l.message:String(l)}`)}let i=t.rawQuery,s=t.valueExpression;if(i&&s)return g('Provide either "valueExpression" or "rawQuery", not both.');if(!i&&!s)return g(`Provide either "valueExpression" (structured mode) or "rawQuery" (raw SQL mode).
98
+
99
+ **Structured mode examples:**
100
+ - valueExpression: "SUM(amount)" \u2014 total per user
101
+ - valueExpression: "COUNT(*)" \u2014 event count per user
102
+ - valueExpression: "1" \u2014 constant value for everyone
103
+
104
+ **Raw SQL mode:** Provide a full SELECT query with {{startDate}} and {{endDate}} parameters.`);let c,a;if(i){let l=Oe(i,o);if(!l.valid)return g(`SQL query validation failed:
105
+ `+l.errors.map(d=>` - ${d}`).join(`
106
+ `)+(l.warnings.length>0?`
107
+
108
+ \u26A0\uFE0F Warnings:
109
+ `+l.warnings.map(d=>` - ${d}`).join(`
110
+ `):""));c=i,a=l.warnings}else{let l=Le(o,{valueExpression:s,filters:t.filters,groupByPubkey:t.groupByPubkey,orderBy:t.orderBy,limit:t.limit});if("errors"in l)return g(`Query build failed:
111
+ `+l.errors.map(d=>` - ${d}`).join(`
112
+ `));c=l.sql,a=l.warnings}let u=i?{isRawQuery:!0}:{valueExpression:s,filters:t.filters,limit:t.limit,orderBy:t.orderBy,groupByPubkey:t.groupByPubkey};return t.confirmed?{content:[{type:"text",text:"\u2705 Query confirmed and ready to use.\n\n```sql\n"+c+"\n```\n\nPass this query as the `sqlQuery` parameter when creating a recurring incentive with `create_recurring_incentive`."},{type:"text",text:JSON.stringify({sql:c,eventId:o.id,eventName:o.eventName})}]}:{content:[{type:"text",text:Ue(c,o,a,u)}]}}async function Ht(t,e){let r=t.instructionId;if(!r)return g("instructionId is required for idl_instruction source.");let n=e.activeProject.id,o,i;try{let d=await e.apiRequest(`/projects/${n}/idl`),m=Array.isArray(d)?d:d.data??[],p,y;for(let T of m){if(!T.instructions)continue;let v=T.instructions.find(X=>X.id===r);if(v){p=v,y=T;break}}if(!p||!y)return g("Instruction not found on this project. Use `list_idls` to see available IDLs and their instructions.");o=p,i=y}catch(d){return g(`Failed to fetch project IDLs: ${d instanceof Error?d.message:String(d)}`)}let s=t.rawQuery,c=t.valueExpression;if(s&&c)return g('Provide either "valueExpression" or "rawQuery", not both.');if(!s&&!c)return g(`Provide either "valueExpression" (structured mode) or "rawQuery" (raw SQL mode).
113
+
114
+ **Structured mode examples:**
115
+ - valueExpression: "SUM(${o.instruction[0]?.fieldName??"field"})" \u2014 total per user
116
+ - valueExpression: "COUNT(*)" \u2014 instruction count per user
117
+ - valueExpression: "1" \u2014 constant value for everyone
118
+
119
+ **Raw SQL mode:** Provide a full SELECT query with {{startDate}} and {{endDate}} parameters.`);let a,u;if(s){let d=Be(s,o);if(!d.valid)return g(`SQL query validation failed:
120
+ `+d.errors.map(m=>` - ${m}`).join(`
121
+ `)+(d.warnings.length>0?`
122
+
123
+ \u26A0\uFE0F Warnings:
124
+ `+d.warnings.map(m=>` - ${m}`).join(`
125
+ `):""));a=s,u=d.warnings}else{let d=Qe(o,{valueExpression:c,filters:t.filters,groupByFeePayer:t.groupByFeePayer,orderBy:t.orderBy});if("errors"in d)return g(`Query build failed:
126
+ `+d.errors.map(m=>` - ${m}`).join(`
127
+ `));a=d.sql,u=d.warnings}let l=s?{isRawQuery:!0}:{valueExpression:c,filters:t.filters,orderBy:t.orderBy,groupByFeePayer:t.groupByFeePayer};return t.confirmed?{content:[{type:"text",text:"\u2705 Query confirmed and ready to use.\n\n```sql\n"+a+"\n```\n\nPass this query as the `sqlQuery` parameter when creating a recurring incentive with `create_recurring_incentive`."},{type:"text",text:JSON.stringify({sql:a,instructionId:o.id,instructionName:o.instructionName,idlId:o.idlId})}]}:{content:[{type:"text",text:We(a,o,i,u,l)}]}}var Ge={name:"generate_incentive_query",description:"Generate a SQL query for any incentive source \u2014 token swaps, bonding curve trades, token holds, custom events, or IDL instructions. Use this when the user wants to create any type of incentive \u2014 leaderboards, rebates, raffles, direct distributions, reward programs, or campaigns. This is the first step before `create_recurring_incentive` \u2014 the query defines what user activity is tracked and measured. Set the `source` parameter to select the data source, then provide source-specific parameters. Token sources (swap, bonding_curve, hold) require no setup \u2014 data is already indexed. Custom event and IDL instruction sources require prior event/IDL setup. Pass the resulting query to `preview_incentive_query` to validate, then to `create_recurring_incentive` as the `sqlQuery` parameter.",requires:["auth","activeProject"],inputSchema:{source:b.enum(["swap","bonding_curve","hold","custom_event","idl_instruction"]).describe("Data source for the query. Token sources (swap, bonding_curve, hold) use indexed on-chain data. custom_event uses off-chain event data. idl_instruction uses on-chain program instruction data."),tokenMint:b.string().optional().describe("SPL token mint address (required for swap, hold)"),direction:b.enum(["buy","sell","both"]).optional().describe("Trade direction (swap, bonding_curve). Default: buy"),measure:b.enum(["volume","count","balance","duration"]).optional().describe("What to measure. swap/bonding_curve: volume or count. hold: balance, duration, or count. Not used for custom_event/idl_instruction."),minUsdAmount:b.number().positive().optional().describe("Minimum USD value per swap (swap only). Uses >= (inclusive). When a user says 'over 100' or '100+', use 100 \u2014 only exclude the stated number if they explicitly say 'more than' or 'above' without inclusion."),protocol:b.string().optional().describe("DEX protocol filter, e.g. Jupiter, Raydium (swap only)"),launchpad:b.enum(["raydium_launchlab","metaplex_genesis"]).optional().describe("Launchpad program (required for bonding_curve)"),poolState:b.string().optional().describe("Pool state address (required for raydium_launchlab)"),genesisAccount:b.string().optional().describe("Genesis account (required for metaplex_genesis)"),minAmountIn:b.number().positive().optional().describe("Minimum purchase size in raw amount (bonding_curve only). Uses >= (inclusive). When a user says 'over N' or 'N+', use N \u2014 only exclude the stated number if they explicitly say 'more than' or 'above' without inclusion."),minBalance:b.number().positive().optional().describe("Minimum token balance, must be > 0 (required for hold). Uses >= (inclusive). When a user says 'over N' or 'N+', use N \u2014 only exclude the stated number if they explicitly say 'more than' or 'above' without inclusion."),customEventId:b.string().optional().describe("Custom event ID (required for custom_event). Use list_project_events to find events."),valueExpression:b.string().optional().describe('Value expression for structured queries (custom_event, idl_instruction). E.g. "SUM(amount)", "COUNT(*)"'),filters:b.array(b.string()).optional().describe("WHERE conditions using field names (custom_event, idl_instruction). Use >= (inclusive) for threshold filters by default. When a user says 'over N' or 'N+', use >= N \u2014 only use > N if they explicitly say 'more than' or 'above' without inclusion."),groupByPubkey:b.boolean().optional().describe("Group by pubkey (custom_event only, default: true)"),orderBy:b.enum(["DESC","ASC"]).optional().describe("Sort order (custom_event, idl_instruction)"),limit:b.number().optional().describe("Max rows (custom_event only)"),rawQuery:b.string().optional().describe("Raw SQL query with {{startDate}}/{{endDate}} params (custom_event, idl_instruction)"),instructionId:b.string().optional().describe("Instruction ID (required for idl_instruction). Use list_idls to find instructions."),groupByFeePayer:b.boolean().optional().describe("Group by fee payer (idl_instruction only, default: true)"),confirmed:b.boolean().optional().describe("Set to true to confirm (custom_event, idl_instruction). Omit to preview.")},handler:async(t,e)=>{let r=h(e);if(r)return r;let n=E(e);if(n)return n;let o=t.source;if(o in Y){let i=t.measure;if(!i)return g(`measure is required for ${o} source.`);if(!Y[o].includes(i))return g(`Invalid measure '${i}' for ${o} source. Allowed: ${Y[o].join(", ")}`)}switch(o){case"swap":return Ft(t);case"bonding_curve":return Qt(t);case"hold":return Bt(t);case"custom_event":return Wt(t,e);case"idl_instruction":return Ht(t,e);default:return g(`Unknown source: ${o}`)}}};import{z as V}from"zod";var Gt=5e3,Ke=6e4,Kt=3,Yt=20;function Vt(){let t=new Date,e=new Date(Date.UTC(t.getUTCFullYear(),t.getUTCMonth(),t.getUTCDate())),r=new Date(e);return r.setUTCDate(r.getUTCDate()-Kt),{startDate:r.toISOString(),endDate:e.toISOString()}}function zt(t){return new Promise(e=>setTimeout(e,t))}function Jt(t,e){let r=t.trim().split(`
128
+ `);if(r.length===0)return"_No data_";let o=r[0].split(","),i=r.slice(1,e+1),s=[`| ${o.join(" | ")} |`,`| ${o.map(()=>"---").join(" | ")} |`,...i.map(c=>`| ${c.split(",").join(" | ")} |`)];return r.length-1>e&&s.push(`| _...and ${r.length-1-e} more rows_ |`),s.join(`
129
+ `)}async function Xt(t,e,r){let n=Date.now();for(;Date.now()-n<Ke;){let o=await t.apiRequest(`/project/${e}/query/${r}/result`),i=o.data??o;if(i.status==="COMPLETED"||i.status==="FAILED"||i.error)return{status:i.status,preview:i.preview??null,error:i.error??null,totalRows:i.totalRows??null,duration:i.duration??null,timedOut:!1};await zt(Gt)}return{status:"TIMEOUT",preview:null,error:null,totalRows:null,duration:null,timedOut:!0}}var Ye={name:"preview_incentive_query",description:"Execute a SQL query against real indexed data and preview the results. Use this to validate that a query from `generate_incentive_query` produces the expected shape and data before creating an incentive. Offer this as an optional step \u2014 it's most useful for established tokens or events that already have indexed data. For newly tracked tokens, freshly created events, or new IDL instructions, data may not exist yet \u2014 empty results are normal and the user can skip preview. The query runs asynchronously \u2014 submits to a queue and polls for results (up to 60 seconds). IMPORTANT: Before calling this tool, tell the user you are submitting the query and will wait for results. The query may be queued before it starts executing \u2014 let the user know you are waiting and will share results as soon as they are ready. By default, previews the last 3 complete days of data. Only accepts queries generated by `generate_incentive_query` or valid incentive SQL with {{startDate}}/{{endDate}} parameters.",requires:["auth","activeProject"],inputSchema:{sqlQuery:V.string().describe("The SQL query to execute and preview. Typically from generate_incentive_query output."),startDate:V.string().optional().describe("ISO 8601 start date for the query window. Default: midnight 3 days ago."),endDate:V.string().optional().describe("ISO 8601 end date for the query window. Default: midnight today (excludes today's partial data).")},handler:async(t,e)=>{let r=h(e);if(r)return r;let n=E(e);if(n)return n;let o=e.activeProject.id,i=t.sqlQuery,s=Vt(),c=t.startDate??s.startDate,a=t.endDate??s.endDate,u;try{let v=await e.apiRequest(`/project/${o}/query/request`,{method:"POST",body:{type:"RAW",query:i,paramMap:{startDate:"DATE",endDate:"DATE"},paramValues:{startDate:c,endDate:a},targetDatabase:"INDEXER"}});u=(v.data??v).queryId}catch(v){return{isError:!0,content:[{type:"text",text:`\u274C Failed to submit query for preview: ${v instanceof Error?v.message:String(v)}`}]}}let l;try{l=await Xt(e,o,u)}catch(v){return{isError:!0,content:[{type:"text",text:`\u274C Failed to poll query results (queryId: ${u}): ${v instanceof Error?v.message:String(v)}`}]}}if(l.timedOut)return{content:[{type:"text",text:`\u23F3 Query is still processing after ${Ke/1e3}s.
130
+
131
+ **Query ID:** \`${u}\`
132
+
133
+ The query may still complete \u2014 you can check results later. The query was submitted with date range ${c} to ${a}.`},{type:"text",text:JSON.stringify({queryId:u,status:"TIMEOUT",startDate:c,endDate:a})}]};if(l.error||l.status==="FAILED")return{isError:!0,content:[{type:"text",text:`\u274C Query preview failed: ${l.error??"Unknown error"}`}]};let d=l.totalRows??0,m=l.duration,p=l.preview,y;return!p||p.trim()===""?y="_No matching data found for the given date range._":y=Jt(p,Yt),{content:[{type:"text",text:`\u2705 Query preview complete
134
+
135
+ **Results:** ${d} rows`+(m!==null?` (${(m/1e3).toFixed(1)}s)`:"")+`
136
+ **Date range:** ${c} \u2192 ${a}
137
+
138
+ `+y},{type:"text",text:JSON.stringify({queryId:u,totalRows:d,duration:m,preview:p,startDate:c,endDate:a})}]}}};import{z as U}from"zod";import nr from"fs/promises";var Zt=new Set(["u8","u16","u32","u64","u128","i8","i16","i32","i64","i128","f32","f64"]),er=new Set(["string","publicKey","pubkey","bytes"]);function tr(t){if(Zt.has(t))return"number";if(er.has(t))return"string";if(t==="bool")return"boolean"}function z(t){if(typeof t=="string")return t;if(typeof t=="object"&&t!==null){let e=t.name;if(typeof e=="string")return e}}function $(t){if(typeof t=="string")return t;if(typeof t=="object"&&t!==null){let e=t;if("defined"in e)return z(e.defined)??"unknown";if("option"in e)return`option<${$(e.option)}>`;if("vec"in e)return`vec<${$(e.vec)}>`;if("array"in e){let r=e.array;return`array<${$(r[0])}>`}}return"unknown"}function D(t,e,r,n=new Set){if(typeof e=="string"){let o=tr(e);return o?[{path:t,type:o,optional:!1,idlType:e}]:[{path:t,type:"string",optional:!1,idlType:e}]}if(typeof e=="object"&&e!==null){let o=e;if("option"in o)return D(t,o.option,r,n).map(s=>({...s,optional:!0}));if("vec"in o){let i=$(o.vec);return[{path:t,type:"string",optional:!1,idlType:`vec<${i}>`}]}if("array"in o){let i=o.array,s=$(i[0]);return[{path:t,type:"string",optional:!1,idlType:`array<${s}>`}]}if("defined"in o){let i=z(o.defined);if(!i)return[{path:t,type:"string",optional:!1,idlType:String(o.defined)}];if(n.has(i))return[{path:t,type:"string",optional:!1,idlType:`circular:${i}`}];let s=r[i];if(!s)return[{path:t,type:"string",optional:!1,idlType:i}];if(s.kind==="enum")return[{path:t,type:"string",optional:!1,idlType:i}];if(s.kind==="struct"){let c=new Set(n);c.add(i);let a=[];for(let u=0;u<s.fields.length;u++){let l=s.fields[u];if(typeof l=="object"&&l!==null&&"name"in l&&"type"in l){let d=l,m=D(`${t}.${d.name}`,d.type,r,c);a.push(...m)}else{let d=D(t,l,r,c);a.push(...d)}}return a}}}return[{path:t,type:"string",optional:!1,idlType:String(e)}]}var x=class{canParse(e){if(typeof e!="object"||e===null)return!1;let r=e;return Array.isArray(r.instructions)?typeof r.name=="string"||typeof r.metadata=="object"&&r.metadata!==null&&typeof r.metadata.name=="string":!1}parse(e){if(typeof e!="object"||e===null)throw new Error("IDL must be a JSON object");let r=e;if(!Array.isArray(r.instructions))throw new Error("IDL is missing 'instructions' array - this may not be a valid Anchor IDL");let n;if(typeof r.name=="string")n=r.name;else if(typeof r.metadata=="object"&&r.metadata!==null&&typeof r.metadata.name=="string")n=r.metadata.name;else throw new Error("IDL is missing 'name' field (checked top-level and metadata.name)");let o=this.buildTypeLookup(r.types),i=r.instructions.map(c=>this.parseInstruction(c,o)),s=typeof r.address=="string"?r.address:null;return{name:n,address:s,instructions:i,types:o}}buildTypeLookup(e){let r={};if(!Array.isArray(e))return r;for(let n of e)if(typeof n=="object"&&n!==null&&typeof n.name=="string"&&typeof n.type=="object"){let o=n;r[o.name]=o.type}return r}parseInstruction(e,r){let n=typeof e.name=="string"?e.name:"unknown",i=(Array.isArray(e.args)?e.args:[]).flatMap(c=>{let a=typeof c.name=="string"?c.name:"unknown";return D(a,c.type,r)}),s=this.parseAccounts(e.accounts);return{name:n,fields:i,accounts:s}}parseAccounts(e){return Array.isArray(e)?e.flatMap(r=>{let n=typeof r.name=="string"?r.name:"unknown";return Array.isArray(r.accounts)?this.parseAccounts(r.accounts):"isMut"in r||"isSigner"in r?[{name:n,writable:r.isMut===!0,signer:r.isSigner===!0}]:[{name:n,writable:r.writable===!0,signer:r.signer===!0}]}):[]}};var rr={number:15,string:15,boolean:10,accounts:30};function O(t){let e={number:0,string:0,boolean:0,accounts:t.accounts.length};for(let n of t.fields)n.type in e&&e[n.type]++;let r=[];for(let[n,o]of Object.entries(e)){let i=rr[n];o>i&&r.push({type:n,count:o,max:i})}return{name:t.name,counts:e,withinLimits:r.length===0,exceeded:r}}function J(t,e){let r=e.map(O),n=[`Program: "${t}" (${e.length} instruction${e.length===1?"":"s"})`,""];for(let o of r){let i=o.withinLimits?"\u2705":"\u26A0\uFE0F",s=`${o.counts.number} numbers, ${o.counts.string} strings, ${o.counts.boolean} booleans, ${o.counts.accounts} accounts`,c=`${i} ${o.name} \u2014 ${s}`;if(!o.withinLimits){let a=o.exceeded.map(u=>`${u.type} by ${u.count-u.max}`).join(", ");c+=` (exceeds: ${a})`}n.push(c)}return n.push(""),n.push("Select which instructions to track."),n.join(`
139
+ `)}var Ve={name:"parse_idl",description:"Parse a Solana Anchor IDL file and return the program name, on-chain address, and available instructions with their fields and accounts. Use this before `create_idl` to show the user which instructions are available for tracking. Provide the IDL via `filePath` (preferred for large files) or inline `idl` JSON. Instructions are limited to 15 numbers, 15 strings, 10 booleans, and 30 accounts \u2014 instructions exceeding limits are flagged. After the user selects instructions, pass their choices to `create_idl` to upload. IMPORTANT: Always display the full parsed instruction summary to the user so they can review and select which instructions to track before proceeding.",inputSchema:{filePath:U.string().optional().describe("Absolute path to an Anchor IDL JSON file on disk. Preferred over inline `idl` for large files. Provide either `filePath` or `idl`, not both."),idl:U.record(U.string(),U.any()).optional().describe("Full Anchor IDL JSON content. For large IDLs, use `filePath` instead.")},handler:async t=>{let e=t.filePath,r=t.idl;if(e&&r)return{content:[{type:"text",text:"\u274C Provide either `filePath` or `idl`, not both."}],isError:!0};let n;if(e)try{let a=await nr.readFile(e,"utf-8");n=JSON.parse(a)}catch(a){let u=a instanceof Error?a.message:String(a);return{content:[{type:"text",text:`\u274C ${u.includes("ENOENT")?`File not found: ${e}`:`Failed to read IDL file: ${u}`}`}],isError:!0}}else if(r)n=r;else return{content:[{type:"text",text:"\u274C No IDL provided. Supply either `filePath` (path to IDL JSON file) or `idl` (inline JSON)."}],isError:!0};let o=new x;if(!o.canParse(n))return{content:[{type:"text",text:"\u274C Invalid IDL format. The provided JSON does not appear to be a valid Anchor IDL. It must have an `instructions` array and a `name` field (or `metadata.name` for newer formats)."}],isError:!0};let i;try{i=o.parse(n)}catch(a){return{content:[{type:"text",text:`\u274C Failed to parse IDL: ${a instanceof Error?a.message:String(a)}`}],isError:!0}}if(!i.name)return{content:[{type:"text",text:"\u274C The IDL does not contain a program name. Expected a `name` field (Anchor v0) or `metadata.name` (Anchor v0.30+)."}],isError:!0};let s=J(i.name,i.instructions);return{content:[{type:"text",text:`${i.address?`Program address: ${i.address}`:"\u26A0\uFE0F No `address` field found in IDL \u2014 you will need to provide the program address to `create_idl`."}
140
+
141
+ ${s}`},{type:"text",text:JSON.stringify({programName:i.name,programAddress:i.address,instructions:i.instructions.map(a=>({name:a.name,analysis:O(a),fields:a.fields,accounts:a.accounts}))})}]}}};import{z as A}from"zod";import or from"fs/promises";function ir(t){return t.replace(/_/g," ").replace(/\b\w/g,e=>e.toUpperCase())}var ze={name:"create_idl",description:"Upload a Solana Anchor IDL and create instruction tracking in a single call. Provide the IDL via `filePath` (preferred for large files) or inline `idl` JSON. The program address is extracted from the IDL's `address` field automatically \u2014 only provide `programAddress` to override it. IMPORTANT: Always use `parse_idl` first to show the user available instructions and let them select which to track before calling this tool. The IDL JSON is stored exactly as provided \u2014 it is never modified.",requires:["auth","activeProject"],inputSchema:{filePath:A.string().optional().describe("Absolute path to an Anchor IDL JSON file on disk. Preferred over inline `idl` for large files. Provide either `filePath` or `idl`, not both."),idl:A.record(A.string(),A.any()).optional().describe("Full Anchor IDL JSON content. For large IDLs, use `filePath` instead."),programAddress:A.string().optional().describe("On-chain Solana program address. If omitted, extracted from the IDL's `address` field."),displayName:A.string().optional().describe("User-friendly display name for the program"),description:A.string().optional().describe("Description of the program"),selectedInstructions:A.array(A.object({instructionName:A.string().describe("Name of the instruction from the IDL"),fields:A.array(A.object({fieldName:A.string().describe("Dot-path field name (e.g. 'amount' or 'config.feeRate')"),type:A.enum(["string","number","boolean"]).describe("Field data type"),label:A.string().nullish().describe("Display label"),description:A.string().nullish().describe("Field description")})).describe("Selected fields to track for this instruction"),accounts:A.array(A.string()).describe("Selected account names to track for this instruction")})).optional().describe("Instructions to track. Use `parse_idl` first to discover available instructions.")},handler:async(t,e)=>{let r=h(e);if(r)return r;let n=E(e);if(n)return n;let o=t.filePath,i=t.idl;if(o&&i)return{content:[{type:"text",text:"\u274C Provide either `filePath` or `idl`, not both."}],isError:!0};let s;if(o)try{let p=await or.readFile(o,"utf-8");s=JSON.parse(p)}catch(p){let y=p instanceof Error?p.message:String(p);return{content:[{type:"text",text:`\u274C ${y.includes("ENOENT")?`File not found: ${o}`:`Failed to read IDL file: ${y}`}`}],isError:!0}}else if(i)s=i;else return{content:[{type:"text",text:"\u274C No IDL provided. Supply either `filePath` (path to IDL JSON file) or `idl` (inline JSON)."}],isError:!0};let c=new x;if(!c.canParse(s))return{content:[{type:"text",text:"\u274C Invalid IDL format. The provided JSON does not appear to be a valid Anchor IDL. It must have an `instructions` array and a `name` field (or `metadata.name` for newer formats)."}],isError:!0};let a;try{a=c.parse(s)}catch(p){return{content:[{type:"text",text:`\u274C Failed to parse IDL: ${p instanceof Error?p.message:String(p)}`}],isError:!0}}if(!a.name)return{content:[{type:"text",text:"\u274C The IDL does not contain a program name. Expected a `name` field (Anchor v0) or `metadata.name` (Anchor v0.30+)."}],isError:!0};let u=t.programAddress??a.address;if(!u)return{content:[{type:"text",text:"\u274C No program address found. The IDL does not contain an `address` field and no `programAddress` was provided. Please supply the on-chain program address."}],isError:!0};let l=t.selectedInstructions,d=e.activeProject.id,m={programAddress:u,name:a.name,displayName:t.displayName??null,description:t.description??null,idl:s,types:s.types??null};l&&l.length>0&&(m.instructions=l.map(p=>({instructionName:p.instructionName,label:ir(p.instructionName),fields:p.fields,accounts:p.accounts})));try{let p=await e.apiRequest(`/projects/${d}/idl/create`,{method:"POST",body:m}),y=p.data,T=y?.instructionIds??[],v=[`\u2705 IDL uploaded successfully for program "${a.name}" (${u})`];return T.length>0&&v.push(`\u{1F4C1} ${T.length} instruction${T.length===1?"":"s"} configured for tracking.`),{content:[{type:"text",text:v.join(`
142
+ `)},{type:"text",text:JSON.stringify({idlId:y?.id??p.id,instructionIds:T})}]}}catch(p){let y=p instanceof Error?p.message:String(p);return y.includes("already exists")?{content:[{type:"text",text:`\u274C An IDL for program address ${u} already exists in this project.`}],isError:!0}:{content:[{type:"text",text:`\u274C Failed to upload IDL: ${y}`}],isError:!0}}}};import{z as _}from"zod";function sr(t){return t.replace(/_/g," ").replace(/\b\w/g,e=>e.toUpperCase())}var Je={name:"create_instruction",description:'Add an instruction to an existing IDL that is already uploaded to the project. Use this when the IDL exists but the specific instruction hasn\'t been set up for tracking yet. Use `parse_idl` first to inspect available instructions and their fields/accounts, then `list_idls` to find the IDL ID. Pipeline: parse_idl \u2192 list_idls \u2192 create_instruction \u2192 generate_incentive_query (source: "idl_instruction") \u2192 create_recurring_incentive.',requires:["auth","activeProject"],inputSchema:{idlId:_.string().describe("ID of the existing IDL to add the instruction to. Use `list_idls` to find this."),instructionName:_.string().describe("Name of the instruction from the IDL (e.g. 'buy_exact_in')."),label:_.string().optional().describe("Display label for the instruction. Defaults to title-cased instruction name."),fields:_.array(_.object({fieldName:_.string().describe("Dot-path field name (e.g. 'amount' or 'config.feeRate')"),type:_.enum(["string","number","boolean"]).describe("Field data type"),label:_.string().nullish().describe("Display label"),description:_.string().nullish().describe("Field description")})).describe("Fields to track for this instruction. Use `parse_idl` to discover available fields."),accounts:_.array(_.string()).describe("Account names to track for this instruction. Use `parse_idl` to discover available accounts.")},handler:async(t,e)=>{let r=h(e);if(r)return r;let n=E(e);if(n)return n;let o=e.activeProject.id,i=t.idlId,s=t.instructionName,c=t.label??sr(s),a=t.fields,u=t.accounts;try{let l=await e.apiRequest(`/projects/${o}/instruction/create`,{method:"POST",body:{idlId:i,instructionName:s,label:c,fields:a,accounts:u}}),m=l.data?.id??l.id;return{content:[{type:"text",text:`\u2705 Instruction "${s}" created successfully on the IDL.
143
+ \u{1F4C1} Instruction ID: \`${m}\`
144
+
145
+ You can now use \`generate_incentive_query\` with \`source: "idl_instruction"\` and this instruction ID to build a query for an incentive.`},{type:"text",text:JSON.stringify({instructionId:m,instructionName:s,idlId:i})}]}}catch(l){let d=l instanceof Error?l.message:String(l);return d.includes("already exists")?{content:[{type:"text",text:`\u274C An instruction named "${s}" already exists on this IDL.`}],isError:!0}:{content:[{type:"text",text:`\u274C Failed to create instruction: ${d}`}],isError:!0}}}};var Xe={name:"list_idls",description:'List all uploaded IDLs (Anchor programs) and their tracked instructions for the active project. Shows program name, address, status, and instruction details including field/account counts. Use this to find instruction IDs for `generate_incentive_query` (with `source: "idl_instruction"`). IMPORTANT: Always display the full list to the user in your response so they can see their uploaded programs.',requires:["auth","activeProject"],inputSchema:{},handler:async(t,e)=>{let r=h(e);if(r)return r;let n=E(e);if(n)return n;let o=e.activeProject.id;try{let i=await e.apiRequest(`/projects/${o}/idl`),s=Array.isArray(i)?i:i.data??[];if(s.length===0)return{content:[{type:"text",text:["\u{1F4CB} **No IDLs uploaded to this project**","","Use `parse_idl` to inspect an Anchor IDL file, then `create_idl` to upload it."].join(`
146
+ `)}]};let c=[`\u{1F4CB} **IDLs for "${e.activeProject.name}"** (${s.length})`,""];for(let a of s){let u=a.active?"\u2705 Active":"\u23F8\uFE0F Inactive",l=a.displayName?` \u2014 ${a.displayName}`:"";c.push(`**${a.name}** (\`${a.programAddress}\`)${l} ${u}`);let d=a.instructions??[];if(d.length===0)c.push(" No instructions tracked");else{c.push(` Instructions (${d.length}):`);for(let m of d){let p=m.instruction?.length??0,y=Object.keys(m.accountMapping??{}).length;c.push(` \u2022 ${m.instructionName} \u2014 ${p} field${p!==1?"s":""}, ${y} account${y!==1?"s":""}`)}}c.push("")}return{content:[{type:"text",text:c.join(`
147
+ `)},{type:"text",text:JSON.stringify(s)}]}}catch(i){return{content:[{type:"text",text:`\u274C Failed to list IDLs: ${i instanceof Error?i.message:String(i)}`}],isError:!0}}}};var Ze={name:"get_ai_context",requires:["auth","activeProject"],description:"Fetch context about the active project \u2014 documentation, program and token addresses, and domain knowledge provided by the project owner. AUTOMATICALLY call this after setting an active project and before building any incentives, custom events, or queries. Also call when the user asks for insights, analytics interpretation, or general information about their project. Results are cached per project for the session \u2014 subsequent calls are free.",inputSchema:{},handler:async(t,e)=>{let r=h(e);if(r)return r;let n=E(e);if(n)return n;let o=e.activeProject.id;if(e.aiContextCache?.projectId===o)return e.aiContextCache.data;try{let i=await e.apiRequest(`/ai-context/${o}`),s=[],c=i.addresses;(c?.programAddress||c?.tokenAddress)&&(s.push("## Addresses"),c.programAddress&&s.push(`- **Program:** \`${c.programAddress}\``),c.tokenAddress&&s.push(`- **Token:** \`${c.tokenAddress}\``),s.push(""));let a=i.files;if(a&&Object.keys(a).length>0)for(let[l,d]of Object.entries(a))s.push(`## ${l}`),s.push(d),s.push("");let u;return s.length===0?u={content:[{type:"text",text:`\u{1F4CB} No AI context available for project "${e.activeProject.name}".`}]}:u={content:[{type:"text",text:`\u{1F4CB} **AI Context for "${e.activeProject.name}"**
148
+
149
+ ${s.join(`
150
+ `)}`}]},e.aiContextCache={projectId:o,data:u},u}catch(i){return{content:[{type:"text",text:`Failed to fetch AI context: ${i instanceof Error?i.message:String(i)}`}],isError:!0}}}};var M={name:"create-incentive",config:{title:"Create Incentive",description:"Guided workflow to create any type of recurring incentive \u2014 token-based (swap, bonding curve, hold), custom event, or custom instruction."},handler:async()=>({messages:[{role:"user",content:{type:"text",text:"I want to create a new incentive program."}},{role:"assistant",content:{type:"text",text:`Great! I'll guide you through creating an incentive step by step. I'll ask one question at a time \u2014 you don't need to know all the details upfront.
151
+
152
+ **Before we start:** I need to check a few things. Let me verify your authentication, make sure you have a project selected, and fetch your project's context so I can make relevant suggestions.
153
+
154
+ \u2192 Call \`check_auth_status\` (authenticate if needed) \u2192 \`list_projects\` + \`set_active_project\` (if no active project) \u2192 \`get_ai_context\` (to understand your project's domain and tokens).
155
+
156
+ **If no projects exist:** Help the user create one with \`create_project\`. Ask them: Are you a **token team** (have a token to incentivize) or a **protocol/product** (have an on-chain program)? Token teams \u2192 collect their token mint as \`defaultTokenAddress\`. Protocols \u2192 collect their program address as \`defaultProgramAddress\`, and ask if they also have a token to set.
157
+
158
+ ## Step 1: Choose Your Action Type
159
+
160
+ What kind of user activity do you want to reward? If you're unsure, describe what you want to achieve and I'll recommend the right approach.
161
+
162
+ ### a) Token Incentive (on-chain token activity \u2014 zero setup required)
163
+ For incentivizing trading, buying, or holding a specific token. Data is already indexed from Solana DEX protocols and launchpads.
164
+
165
+ Pick a token action:
166
+ - **Swap** \u2014 Trade a token on any supported DEX (Jupiter, Raydium, Meteora, etc.)
167
+ - Params: token mint, direction (buy/sell/both), min USD amount, DEX protocol
168
+ - Tool: \`generate_incentive_query\` with \`source: "swap"\`
169
+ - **Bonding Curve Buy** \u2014 Buy a token on a launchpad before graduation
170
+ - Params: launchpad (Raydium LaunchLab or Metaplex Genesis), pool/genesis account, direction, min amount
171
+ - Tool: \`generate_incentive_query\` with \`source: "bonding_curve"\`
172
+ - **Hold** \u2014 Maintain a minimum token balance over time
173
+ - Params: token mint, minimum balance (required, must be > 0)
174
+ - Tool: \`generate_incentive_query\` with \`source: "hold"\`
175
+
176
+ ### b) Custom Event (off-chain activity \u2014 requires event definition)
177
+ For social actions, content creation, or anything not natively on-chain.
178
+ 1. Check existing events: \`list_project_events\`
179
+ 2. Use an existing event, or create a new one: \`create_custom_event\` \u2192 \`attach_custom_event\`
180
+ 3. Generate query: \`generate_incentive_query\` with \`source: "custom_event"\`
181
+
182
+ ### c) Custom Instruction (on-chain program interaction \u2014 requires IDL)
183
+ For tracking specific on-chain program instructions not covered by token actions.
184
+ 1. Check existing IDLs: \`list_idls\`
185
+ 2. Use an existing instruction, or upload an IDL: \`parse_idl\` \u2192 \`create_idl\` / \`create_instruction\`
186
+ 3. Generate query: \`generate_incentive_query\` with \`source: "idl_instruction"\`
187
+
188
+ ## Step 2: Generate the Query
189
+
190
+ Call \`generate_incentive_query\` with the appropriate \`source\` parameter and source-specific params. The tool returns a SQL query that will power the incentive.
191
+
192
+ ## Step 2.5: Preview Results (Optional)
193
+
194
+ Before configuring the incentive, you can preview the query against real data using \`preview_incentive_query\`.
195
+ Pass the SQL from the previous step. By default it previews the last 3 complete days of data.
196
+
197
+ This validates that the query:
198
+ - Returns data in the expected shape (address + value columns)
199
+ - Produces reasonable results (correct wallets, sensible values)
200
+ - Runs without errors against the actual database
201
+
202
+ If the preview looks wrong, go back and adjust the query parameters.
203
+
204
+ ## Step 3: Configure the Recipe
205
+
206
+ Now configure the incentive mechanics using \`create_recurring_incentive\`:
207
+
208
+ ### Primitive (incentive type):
209
+ | Type | Mechanic | Key Params |
210
+ |------|----------|------------|
211
+ | **Leaderboard** | Rank wallets by measure, distribute via formula. **Ask the user how they want rewards distributed** \u2014 fixed prizes per rank, tiered ranges, equal splits, etc. Build a \`customFormula\` from their intent. Do NOT silently default to \`N\` (which pays raw metric values). | customFormula (required \u2014 ask user) |
212
+ | **Rebate** | % back on action value. MCP constructs formula from rebatePercentage. | rebatePercentage |
213
+ | **Raffle** | Random winners from qualifying users. **Ask the user how tickets should be distributed:** weighted by metric (default \u2014 more activity = more tickets) or equal chances (1 ticket per qualifying user). | raffleBuckets (prize tiers), raffleWeighting |
214
+ | **Direct** | Fixed amounts to specific wallets. Provide allocations \u2014 MCP constructs the query internally. Recipients must claim. | allocations |
215
+
216
+ ### Leaderboard payout patterns (ask the user which they want):
217
+ - **Fixed rank prizes:** "1st gets 10,000, 2nd gets 5,000, 3rd gets 2,000" \u2192 \`RANK == 1 ? 10000 : RANK == 2 ? 5000 : RANK == 3 ? 2000 : 0\`
218
+ - **Tiered ranges:** "Top 10 get 5,000, ranks 11-50 get 1,000" \u2192 \`RANK <= 10 ? 5000 : RANK <= 50 ? 1000 : 0\`
219
+ - **Equal split:** "Split the pool among everyone" \u2192 \`TOTAL_REWARD_POOL / TOTAL_PARTICIPANTS\`
220
+ - **Capped metric:** "10% of their volume, max 500" \u2192 \`min(N * 0.1, 500)\`
221
+
222
+ ### Emission type:
223
+ - **TOKENS** \u2014 requires tokenAddress + tokenDecimals
224
+ - **SOL** \u2014 native SOL rewards
225
+
226
+ ### Other params:
227
+ - name, description
228
+ - totalFundAmount \u2014 total reward budget (for fixed rank prizes, must be >= sum of all prizes)
229
+ - evalDurationDays or interval (DAILY/WEEKLY/MONTHLY)
230
+ - startDate
231
+ - maxIterations (how many evaluation cycles)
232
+ - maxPerParticipant (optional cap)
233
+
234
+ ## Action-Measure Compatibility
235
+
236
+ Not all measures work with all actions. Use this matrix:
237
+
238
+ | Action | Volume | Count | Duration | Balance |
239
+ |--------|--------|-------|----------|---------|
240
+ | Swap | YES | YES | no | no |
241
+ | Bonding Curve Buy | YES | YES | no | no |
242
+ | Hold | no | no | YES | YES |
243
+ | Custom Event | depends | YES | no | no |
244
+ | Custom Instruction | depends | YES | no | no |
245
+
246
+ The measure determines what the \`value\` column in the query represents:
247
+ - **Leaderboard** \u2014 value is used for ranking; actual payouts come from \`customFormula\` (using RANK, N, TOTAL_REWARD_POOL, etc.)
248
+ - **Rebate** \u2014 value is the base for percentage calculation
249
+ - **Raffle** \u2014 value determines ticket weight by default (higher value = more tickets). With \`raffleWeighting: "EQUAL_CHANCES"\`, value only determines qualification.
250
+ - **Direct** \u2014 no query needed, allocations define exact amounts per wallet
251
+
252
+ ## Step 4: Preview & Confirm
253
+
254
+ Call \`create_recurring_incentive\` with \`confirmed: false\` first to preview. Review with the user, then call again with \`confirmed: true\` to create.
255
+
256
+ ## Example Recipes
257
+
258
+ | Recipe | Action | Measure | Primitive |
259
+ |--------|--------|---------|-----------|
260
+ | Volume Leaderboard | Swap (buy, min 0.1 SOL) | Volume | Leaderboard |
261
+ | Trader Cashback | Swap (buy, any DEX) | Volume | Rebate (3%) |
262
+ | Diamond Hands | Hold (min 100 tokens) | Duration | Leaderboard |
263
+ | Lucky Holder | Hold (min 1 token) | Count | Raffle |
264
+ | Curve Rush | Bonding Curve Buy (LaunchLab) | Volume | Leaderboard |
265
+ | Early Bird Raffle | Bonding Curve Buy (any) | Count | Raffle |
266
+ | Content Creators | Custom Event (x_post) | Count | Leaderboard |
267
+
268
+ ## Error Recovery
269
+
270
+ - **Auth expired mid-flow:** Call \`check_auth_status\` then \`authenticate\` again before retrying the failed step.
271
+ - **Query returns no data:** Verify the token mint address, event ID, or instruction ID is correct. For custom events, check that data has been ingested at least once via \`list_project_events\`.
272
+ - **Preview looks wrong:** Go back to Step 2 and adjust the query parameters (filters, measure, direction).
273
+ - **Incentive creation fails:** Check the error message \u2014 common causes are missing required fields for the chosen type (e.g., \`rebatePercentage\` for rebate, \`raffleBuckets\` for raffle).`}}]})};var ar='# Torque MCP Workflows\n\n## Creating an Incentive (Unified Flow)\n\nUse the `create-incentive` prompt to start the guided workflow. The flow:\n\n### Path A: Token Incentive (zero setup)\nFor incentivizing on-chain token activity. No custom event or IDL setup required.\n\n1. **Select project** \u2192 `list_projects` + `set_active_project` (if no projects exist, create one with `create_project` \u2014 ask if they are a token team, protocol, or both, and collect the appropriate address)\n2. **Choose token action:**\n - **Swap** \u2192 `generate_incentive_query` with `source: "swap"` (token mint, direction, min USD, DEX protocol)\n - **Bonding Curve Buy** \u2192 `generate_incentive_query` with `source: "bonding_curve"` (launchpad, pool/genesis account)\n - **Hold** \u2192 `generate_incentive_query` with `source: "hold"` (token mint, min balance > 0)\n3. **(Optional) Preview** \u2192 `preview_incentive_query` to validate results with real data\n4. **Configure recipe** \u2192 `create_recurring_incentive` (primitive, emission, rewards)\n5. **Preview & confirm**\n\n### Path B: Custom Event Incentive\nFor off-chain activity (social, content, etc.).\n\n1. **Select project** \u2192 `list_projects` + `set_active_project`\n2. **Set up event** \u2192 `list_project_events` (use existing or `create_custom_event` + `attach_custom_event`)\n3. **Generate query** \u2192 `generate_incentive_query` with `source: "custom_event"`\n4. **(Optional) Preview** \u2192 `preview_incentive_query` to validate results with real data\n5. **Configure recipe** \u2192 `create_recurring_incentive`\n6. **Preview & confirm**\n\n### Path C: Custom Instruction Incentive\nFor on-chain program interactions not covered by token actions.\n\n1. **Select project** \u2192 `list_projects` + `set_active_project`\n2. **Set up instruction** \u2192 `list_idls` (use existing or `parse_idl` + `create_idl` / `create_instruction`)\n3. **Generate query** \u2192 `generate_incentive_query` with `source: "idl_instruction"`\n4. **(Optional) Preview** \u2192 `preview_incentive_query` to validate results with real data\n5. **Configure recipe** \u2192 `create_recurring_incentive`\n6. **Preview & confirm**\n\n## Reviewing Existing Incentives\n\n1. `list_recurring_incentives` \u2192 see all incentives for active project\n2. `get_recurring_incentive` \u2192 full details of a specific incentive\n3. `get_recurring_incentive_analytics` \u2192 performance metrics\n\n## Terminology\n\n- **Primitive** \u2014 Incentive mechanic: Leaderboard, Rebate, Raffle, or Direct (fixed wallet allocations)\n- **Action** \u2014 What users do to qualify: Swap, Bonding Curve Buy, Hold, Custom Event, Custom Instruction\n- **Measure** \u2014 How actions are scored: Volume, Count, Duration, Balance\n- **Recipe** \u2014 Primitive + Action + Measure = a complete incentive configuration\n- **Evaluation period** \u2014 Time window for measuring activity (daily, weekly, monthly)\n\n## Tips\n\n- Token incentives are the fastest path \u2014 data is already indexed, no setup needed\n- For custom events, the event must be ingested at least once before query generation works (columnMapping must be non-null)\n- Use `get_ai_context` to fetch project-specific documentation and domain knowledge\n- Always preview before confirming: `create_recurring_incentive` with `confirmed: false`\n- **For leaderboards:** Always ask how the user wants rewards distributed (fixed prizes per rank, tiered ranges, equal splits, etc.) and build a `customFormula` \u2014 the default `N` pays raw metric values which is rarely intended\n- **For raffles:** Default weighting is WEIGHTED_BY_METRIC (query metric = ticket count). Use `raffleWeighting: "EQUAL_CHANCES"` if the user wants equal odds for all participants.\n';function et(t){t.registerResource("workflows-guide","torque://workflows",{description:"Guide to common Torque workflows including custom event incentives, IDL instruction incentives, and simple incentives. Read this to understand the tool pipeline and available guided prompts.",mimeType:"text/markdown"},async()=>({contents:[{uri:"torque://workflows",mimeType:"text/markdown",text:ar}]}))}var cr=`# Torque MCP \u2014 Capabilities
274
+
275
+ ## What is Torque MCP?
276
+
277
+ Torque MCP connects AI assistants to the **Torque incentive platform** on Solana. You can create and manage reward programs, track on-chain and off-chain user activity, and distribute tokens or SOL \u2014 all through natural conversation.
278
+
279
+ ## Prerequisites
280
+
281
+ Most tools require **authentication** \u2014 call \`check_auth_status\` first, then \`authenticate\` if needed. Wait for authentication to succeed before calling any other tool.
282
+
283
+ Most tools also require an **active project** \u2014 call \`list_projects\` then \`set_active_project\` after authenticating. Wait for the project to be set before calling project-scoped tools.
284
+
285
+ ## Available Tools
286
+
287
+ ### Authentication
288
+ | Tool | Description |
289
+ |------|-------------|
290
+ | \`authenticate\` | Connect to Torque with an auth token or wallet private key |
291
+ | \`check_auth_status\` | Verify your current authentication |
292
+ | \`logout\` | Disconnect from Torque |
293
+
294
+ ### API Keys
295
+ | Tool | Description | Requires |
296
+ |------|-------------|----------|
297
+ | \`list_api_keys\` | List your API keys for event ingestion | auth |
298
+ | \`create_api_key\` | Create a new API key for event ingestion | auth |
299
+
300
+ ### Projects
301
+ | Tool | Description | Requires |
302
+ |------|-------------|----------|
303
+ | \`list_projects\` | View all your Torque projects | auth |
304
+ | \`create_project\` | Create a new project \u2014 ask if they are a token team, protocol, or both, then collect the appropriate address(es) | auth |
305
+ | \`set_active_project\` | Select a project to work in | auth |
306
+
307
+ ### Custom Events (off-chain tracking)
308
+ | Tool | Description | Requires |
309
+ |------|-------------|----------|
310
+ | \`create_custom_event\` | Define a new event schema for tracking off-chain activity | auth |
311
+ | \`list_project_events\` | View events attached to the active project | auth, project |
312
+ | \`list_custom_events\` | Browse your own custom events across all projects | auth |
313
+ | \`attach_custom_event\` | Attach an event to the active project | auth, project |
314
+
315
+ ### IDL & On-Chain Programs
316
+ | Tool | Description | Requires |
317
+ |------|-------------|----------|
318
+ | \`parse_idl\` | Parse a Solana Anchor IDL and show available instructions | \u2014 |
319
+ | \`create_idl\` | Upload an Anchor IDL and configure instruction tracking | auth, project |
320
+ | \`create_instruction\` | Add an instruction to an already-uploaded IDL | auth, project |
321
+ | \`list_idls\` | List uploaded IDLs and their tracked instructions | auth, project |
322
+
323
+ ### Incentive Query Builder
324
+ | Tool | Description | Requires |
325
+ |------|-------------|----------|
326
+ | \`generate_incentive_query\` | Generate SQL for any incentive \u2014 swap, bonding curve, hold, custom event, or IDL instruction | auth, project |
327
+ | \`preview_incentive_query\` | Preview query results against real indexed data before creating an incentive | auth, project |
328
+
329
+ ### Incentives
330
+ | Tool | Description | Requires |
331
+ |------|-------------|----------|
332
+ | \`create_recurring_incentive\` | Create a recurring incentive (leaderboard, rebate, raffle, or direct) | auth, project |
333
+ | \`list_recurring_incentives\` | View all incentives on the active project | auth, project |
334
+ | \`get_recurring_incentive\` | Get details of a specific incentive | auth, project |
335
+ | \`get_recurring_incentive_analytics\` | View performance analytics for an incentive | auth, project |
336
+
337
+ ### Context
338
+ | Tool | Description | Requires |
339
+ |------|-------------|----------|
340
+ | \`get_ai_context\` | Fetch project documentation, domain knowledge, and configuration | auth, project |
341
+
342
+ ## Guided Workflows
343
+
344
+ | Prompt | Description |
345
+ |--------|-------------|
346
+ | \`create-incentive\` | Unified guide to create any incentive \u2014 token-based, custom event, or custom instruction |
347
+
348
+ ## Quick Start
349
+
350
+ 1. **Authenticate** \u2014 Use \`check_auth_status\`, then \`authenticate\` if needed. **Wait for auth to succeed before proceeding.**
351
+ 2. **Select a project** \u2014 Use \`list_projects\` then \`set_active_project\`. **Wait for project to be set before proceeding.**
352
+ 3. **Create an incentive** \u2014 Use the \`create-incentive\` prompt for a guided walkthrough, or call individual tools directly
353
+
354
+ ## Common Tasks
355
+
356
+ | I want to... | Do this |
357
+ |---------------|---------|
358
+ | Create an incentive for token activity (swap, hold, bonding curve) | Use the \`create-incentive\` prompt \u2192 choose Token path |
359
+ | Create an incentive based on off-chain activity | Use the \`create-incentive\` prompt \u2192 choose Custom Event path |
360
+ | Create an incentive based on on-chain activity | Use the \`create-incentive\` prompt \u2192 choose Custom Instruction path |
361
+ | See what incentives exist | \`list_recurring_incentives\` |
362
+ | Check how an incentive is performing | \`get_recurring_incentive_analytics\` |
363
+ | Track off-chain user activity | \`create_custom_event\` |
364
+ | Track on-chain program instructions | \`parse_idl\` \u2192 \`create_idl\` |
365
+ | See what events a project has | \`list_project_events\` |
366
+ | See what on-chain programs are tracked | \`list_idls\` |
367
+ | Manage API keys for event ingestion | \`list_api_keys\` or \`create_api_key\` |
368
+ `;function tt(t){t.registerResource("capabilities-guide","torque://capabilities",{description:"Complete guide to all Torque MCP tools, prompts, and capabilities. Read this when the user asks what they can do, how to use Torque, or needs help getting started.",mimeType:"text/markdown"},async()=>({contents:[{uri:"torque://capabilities",mimeType:"text/markdown",text:cr}]}))}var ur='# Torque \u2014 Incentive Types & Formulas\n\n## What Are Incentive Types?\n\nIncentive types determine the **reward distribution shape** \u2014 how rewards are calculated and distributed to qualifying users. Every recurring incentive has a type that controls what additional inputs are needed and how the reward formula works.\n\nAny query (custom event-based or otherwise) can use any type.\n\n## Available Types\n\n### Leaderboard (\\`leaderboard\\`)\nRank users by a metric and reward based on position. Uses \\`distributionType: "FORMULA"\\` under the hood.\n\n**IMPORTANT:** Always ask the user how they want rewards distributed before creating a leaderboard. The fallback formula \\`N\\` pays each user their raw metric value (e.g., a trader with $10,000 volume would receive 10,000 tokens \u2014 essentially doubling their money). This is rarely the desired behavior. Build a \\`customFormula\\` from the user\'s stated payout intent.\n\n**Common formula patterns:**\n- **Fixed rank prizes:** \\`RANK == 1 ? 10000 : RANK == 2 ? 5000 : RANK == 3 ? 2000 : 0\\` \u2014 set \\`totalFundAmount\\` >= sum of all prizes\n- **Tiered ranges:** \\`RANK <= 3 ? 10000 : RANK <= 10 ? 5000 : RANK <= 50 ? 1000 : 0\\` \u2014 top 3 get 10k, 4-10 get 5k, 11-50 get 1k\n- **Equal split:** \\`TOTAL_REWARD_POOL / TOTAL_PARTICIPANTS\\` \u2014 equal share for everyone who qualifies\n- **Diminishing by rank:** \\`TOTAL_REWARD_POOL / pow(RANK, 0.5)\\` \u2014 top performers get more, lower ranks get reduced amounts as pool depletes\n- **Capped payouts:** \\`min(N * 0.1, 500)\\` \u2014 10% of metric value, capped at 500 per user\n\n**Required inputs:** \\`customFormula\\` (strongly recommended \u2014 ask the user)\n\n### Rebate (\\`rebate\\`)\nReturn a percentage of a tracked value to users. Uses \\`distributionType: "FORMULA"\\` under the hood \u2014 the MCP constructs the formula from \\`rebatePercentage\\`.\n\n**Formula:** \\`VALUE * (rebatePercentage / 100)\\` \u2014 e.g., 5% rebate becomes \\`VALUE * 0.05\\`. The MCP builds this formula automatically from the \\`rebatePercentage\\` parameter.\n\n**Custom formula examples:**\n- Tiered: \\`VALUE > 1000 ? VALUE * 0.1 : VALUE * 0.05\\`\n- Capped: \\`min(VALUE * 0.05, 500)\\`\n\n**Required inputs:** \\`rebatePercentage\\`\n\n### Raffle (\\`raffle\\`)\nRandomly select winners from qualifying users. **Always ask the user how they want raffle tickets distributed** before creating.\n\n**No formula** \u2014 winners are selected randomly based on ticket weight. Prize distribution is defined by \\`raffleBuckets\\`.\n\n**Weighting modes (\\`raffleWeighting\\`) \u2014 ask the user which they want:**\n- **\\`WEIGHTED_BY_METRIC\\`** (default) \u2014 ticket count = user\'s query metric value. A user with 100 volume gets 10x more chances than a user with 10 volume. The query results must be sorted by value (highest first) for this to work correctly.\n- **\\`EQUAL_CHANCES\\`** \u2014 every qualifying user gets exactly 1 ticket. All qualifying users have equal odds regardless of their metric value \u2014 the query value only determines whether they meet the threshold to enter.\n\n**Required inputs:** \\`raffleBuckets\\` (array of {amount, count})\n**Optional inputs:** \\`raffleWeighting\\` (default: WEIGHTED_BY_METRIC)\n\n### Direct (\\`direct\\`)\nSend specific amounts to specific wallet addresses.\n\nProvide a list of wallet addresses and amounts. The MCP constructs an inline SQL query from the allocations and uses \\`distributionType: "FORMULA"\\` with formula \\`N\\` internally, so each wallet receives exactly the specified amount. Direct incentives are created as recurring offers and appear in \\`list_recurring_incentives\\`.\n\n**Required inputs:** \\`allocations\\` (CSV string or array of {address, amount})\n\n## How Formulas Work\n\nFormulas produce **direct payout amounts** \u2014 each user receives exactly what the formula evaluates to, subject to the reward pool cap.\n\n**Processing order:** Users are processed in SQL query result order. The MCP\'s query builders sort by \\`value DESC\\`, so RANK 1 = highest metric value.\n\n**Pool exhaustion:** \\`totalFundAmount\\` is a sequential cap. Each user gets their formula result if the pool has enough remaining. When remaining pool < formula result, the user receives only what\'s left. Remaining users receive nothing. Negative formula results are converted to 0.\n\n## Formula Variables\n\nWhen writing a \\`customFormula\\`, these variables are available:\n\n| Variable | Description |\n|----------|-------------|\n| \\`N\\` / \\`VALUE\\` | The metric value from the SQL query for each user (e.g., total spend, score, event count) |\n| \\`RANK\\` | 1-based position in query results (1 = first/highest row). Depends on SQL ORDER BY. |\n| \\`INDEX\\` | 0-based position (RANK - 1) |\n| \\`TOTAL_PARTICIPANTS\\` | Number of qualifying users in this epoch |\n| \\`TOTAL_REWARD_POOL\\` | The \\`totalFundAmount\\` for this epoch \u2014 caps total payouts sequentially |\n\n## Formula Functions\n\n\\`sqrt\\`, \\`pow\\`, \\`abs\\`, \\`floor\\`, \\`ceil\\`, \\`round\\`, \\`min\\`, \\`max\\`, \\`log\\`, \\`exp\\`\n\n## How Types Connect to Queries\n\n- **Type** = how to distribute rewards (the formula shape)\n- **Query** = who qualifies and what\'s measured (the SQL)\n\nAny query can use any type. For example:\n- A "total purchases" query + leaderboard type = reward top spenders by rank\n- The same query + rebate type = give everyone back a % of what they spent\n- A "game score" query + raffle type = enter everyone who played into a prize draw\n\n## Intent-to-Formula Examples\n\n| User says... | Formula |\n|-------------|---------|\n| "1st place gets 10000, 2nd gets 5000, 3rd gets 2000" | \\`RANK == 1 ? 10000 : RANK == 2 ? 5000 : RANK == 3 ? 2000 : 0\\` |\n| "Top 10 get 5000 each, ranks 11-50 get 1000" | \\`RANK <= 10 ? 5000 : RANK <= 50 ? 1000 : 0\\` |\n| "Split the reward pool equally among everyone" | \\`TOTAL_REWARD_POOL / TOTAL_PARTICIPANTS\\` |\n| "Top players get more, with diminishing returns" | \\`TOTAL_REWARD_POOL / pow(RANK, 0.5)\\` |\n| "Give them back 5% of what they spent" | \\`N * 0.05\\` (or use rebate type with rebatePercentage: 5) |\n| "Reward based on how much they bought but cap it at 500" | \\`min(N * 0.1, 500)\\` |\n| "Bonus tokens for every 10 games completed" | \\`floor(N / 10) * 5\\` |\n| "Flat reward for everyone who qualifies" | Use \\`valueExpression: "1"\\` in the query instead |\n';function rt(t){t.registerResource("incentive-types-guide","torque://incentive-types",{description:"Guide to Torque incentive types, formulas, and how they connect to queries. Read this when configuring reward distribution for an incentive.",mimeType:"text/markdown"},async()=>({contents:[{uri:"torque://incentive-types",mimeType:"text/markdown",text:ur}]}))}function pr(t){if(!t.requires?.length)return t.description;let e=[];return t.requires.includes("auth")&&e.push("requires authentication"),t.requires.includes("activeProject")&&e.push("requires an active project via `set_active_project`"),`${t.description} [PREREQUISITE: ${e.join("; ")}. Call these first and wait for success.]`}function w(t,e,r){t.registerTool(e.name,{description:pr(e),inputSchema:e.inputSchema},async n=>e.handler(n,r))}async function nt(t){let e=new lr({name:"torque-mcp",version:"0.2.0"}),r=F(t);await r.auth.loadPersistedToken(),w(e,he,r),w(e,ye,r),w(e,ge,r),w(e,ve,r),w(e,we,r),w(e,be,r),w(e,Te,r),w(e,Ee,r),w(e,Ie,r),w(e,xe,r),w(e,Pe,r),w(e,Se,r),w(e,De,r),w(e,$e,r),w(e,je,r),w(e,Ce,r),w(e,Ve,r),w(e,ze,r),w(e,Je,r),w(e,Xe,r),w(e,Ge,r),w(e,Ye,r),w(e,Ze,r),e.registerPrompt(M.name,M.config,M.handler),et(e),tt(e),rt(e);let n=new dr;await e.connect(n)}var{action:ot,params:mr}=Z(process.argv.slice(2));ot==="help"&&(ee(),process.exit(0));ot==="version"&&(te(),process.exit(0));var fr=re(mr);nt(fr).catch(t=>{console.error("Failed to start Torque MCP server:",t),process.exit(1)});
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@torque-labs/mcp",
3
+ "version": "0.2.0",
4
+ "description": "MCP server for the Torque incentive platform on Solana — create and manage token reward programs through AI assistants",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "torque-mcp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsup",
15
+ "dev": "tsup --watch",
16
+ "start": "node dist/index.js",
17
+ "lint": "eslint src/ tests/",
18
+ "lint:fix": "eslint src/ tests/ --fix",
19
+ "typecheck": "tsc --noEmit && tsc --noEmit --project tsconfig.test.json",
20
+ "format": "prettier --write src/ tests/",
21
+ "format:check": "prettier --check src/ tests/",
22
+ "test": "vitest run",
23
+ "test:watch": "vitest",
24
+ "eval": "node evals/run.js",
25
+ "docs:check": "npx tsx scripts/generate-docs.ts",
26
+ "prepublishOnly": "npm run build && npm run typecheck && npm run lint && npm test"
27
+ },
28
+ "engines": {
29
+ "node": ">=20"
30
+ },
31
+ "keywords": [
32
+ "mcp",
33
+ "solana",
34
+ "torque",
35
+ "incentives",
36
+ "rewards",
37
+ "model-context-protocol"
38
+ ],
39
+ "imports": {
40
+ "#/*": "./src/*"
41
+ },
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/torque-labs/mcp.git"
45
+ },
46
+ "homepage": "https://github.com/torque-labs/mcp#readme",
47
+ "bugs": {
48
+ "url": "https://github.com/torque-labs/mcp/issues"
49
+ },
50
+ "author": "Torque Labs (https://torque.so)",
51
+ "license": "MIT",
52
+ "publishConfig": {
53
+ "access": "public"
54
+ },
55
+ "dependencies": {
56
+ "@modelcontextprotocol/sdk": "^1.27.1",
57
+ "@solana/kit": "^6.2.0",
58
+ "zod": "^3.25.67"
59
+ },
60
+ "devDependencies": {
61
+ "@eslint/js": "^10.0.1",
62
+ "@types/node": "^22.15.0",
63
+ "eslint": "^10.0.3",
64
+ "eslint-config-prettier": "^10.1.8",
65
+ "mcp-evals": "^2.0.1",
66
+ "prettier": "^3.8.1",
67
+ "tsup": "^8.5.1",
68
+ "typescript": "^5.9.3",
69
+ "typescript-eslint": "^8.57.0",
70
+ "vitest": "^4.0.18"
71
+ }
72
+ }