helius-mcp 1.2.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/README.md +42 -30
- package/dist/http.d.ts +1 -1
- package/dist/index.js +2 -56
- package/dist/results/store.d.ts +8 -0
- package/dist/results/store.js +72 -0
- package/dist/results/types.d.ts +47 -0
- package/dist/results/types.js +1 -0
- package/dist/router/action-groups.d.ts +6 -0
- package/dist/router/action-groups.js +32 -0
- package/dist/router/action-handlers.d.ts +20 -0
- package/dist/router/action-handlers.js +125 -0
- package/dist/router/actions.d.ts +12 -0
- package/dist/router/actions.js +123 -0
- package/dist/router/catalog.d.ts +6 -0
- package/dist/router/catalog.js +388 -0
- package/dist/router/context.d.ts +5 -0
- package/dist/router/context.js +10 -0
- package/dist/router/dispatch.d.ts +4 -0
- package/dist/router/dispatch.js +276 -0
- package/dist/router/instructions.d.ts +1 -0
- package/dist/router/instructions.js +25 -0
- package/dist/router/register.d.ts +2 -0
- package/dist/router/register.js +15 -0
- package/dist/router/required-params.d.ts +9 -0
- package/dist/router/required-params.js +66 -0
- package/dist/router/responses.d.ts +29 -0
- package/dist/router/responses.js +186 -0
- package/dist/router/schemas.d.ts +216 -0
- package/dist/router/schemas.js +195 -0
- package/dist/router/telemetry.d.ts +27 -0
- package/dist/router/telemetry.js +52 -0
- package/dist/router/types.d.ts +46 -0
- package/dist/router/types.js +1 -0
- package/dist/scripts/validate-catalog.d.ts +2 -2
- package/dist/scripts/validate-catalog.js +10 -10
- package/dist/tools/accounts.js +5 -5
- package/dist/tools/assets.js +5 -5
- package/dist/tools/auth.js +392 -288
- package/dist/tools/config.js +3 -3
- package/dist/tools/das-extras.js +6 -6
- package/dist/tools/docs.js +55 -41
- package/dist/tools/enhanced-websockets.js +13 -13
- package/dist/tools/fees.js +3 -3
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +2 -80
- package/dist/tools/laserstream.js +20 -23
- package/dist/tools/network.js +41 -2
- package/dist/tools/plans.d.ts +0 -5
- package/dist/tools/plans.js +167 -12
- package/dist/tools/product-catalog.d.ts +1 -0
- package/dist/tools/product-catalog.js +51 -16
- package/dist/tools/recommend.d.ts +0 -1
- package/dist/tools/recommend.js +9 -28
- package/dist/tools/shared.d.ts +1 -0
- package/dist/tools/shared.js +10 -2
- package/dist/tools/solana-knowledge.js +23 -7
- package/dist/tools/staking.d.ts +2 -0
- package/dist/tools/staking.js +268 -0
- package/dist/tools/transactions.js +167 -3
- package/dist/tools/transfers.js +38 -43
- package/dist/tools/wallet.js +27 -16
- package/dist/tools/webhooks.js +3 -3
- package/dist/tools/zk-compression.d.ts +2 -0
- package/dist/tools/zk-compression.js +781 -0
- package/dist/utils/config.d.ts +2 -2
- package/dist/utils/config.js +68 -6
- package/dist/utils/errors.d.ts +10 -1
- package/dist/utils/errors.js +46 -12
- package/dist/utils/feedback.js +1 -4
- package/dist/utils/helius.js +2 -1
- package/dist/utils/ows.d.ts +74 -0
- package/dist/utils/ows.js +155 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/system-prompts/helius/claude.system.md +56 -25
- package/system-prompts/helius/full.md +474 -130
- package/system-prompts/helius/openai.developer.md +56 -25
- package/system-prompts/helius-dflow/claude.system.md +41 -6
- package/system-prompts/helius-dflow/full.md +581 -92
- package/system-prompts/helius-dflow/openai.developer.md +41 -6
- package/system-prompts/helius-jupiter/claude.system.md +333 -0
- package/system-prompts/helius-jupiter/full.md +5109 -0
- package/system-prompts/helius-jupiter/openai.developer.md +333 -0
- package/system-prompts/helius-okx/claude.system.md +182 -0
- package/system-prompts/helius-okx/full.md +584 -0
- package/system-prompts/helius-okx/openai.developer.md +182 -0
- package/system-prompts/helius-phantom/claude.system.md +15 -2
- package/system-prompts/helius-phantom/full.md +254 -101
- package/system-prompts/helius-phantom/openai.developer.md +15 -2
- package/system-prompts/svm/claude.system.md +1 -0
- package/system-prompts/svm/full.md +1 -0
- package/system-prompts/svm/openai.developer.md +1 -0
package/dist/tools/auth.js
CHANGED
|
@@ -2,53 +2,57 @@ import { z } from 'zod';
|
|
|
2
2
|
import { generateKeypair } from 'helius-sdk/auth/generateKeypair';
|
|
3
3
|
import { loadKeypair } from 'helius-sdk/auth/loadKeypair';
|
|
4
4
|
import { getAddress } from 'helius-sdk/auth/getAddress';
|
|
5
|
-
import { checkSolBalance, checkUsdcBalance } from 'helius-sdk/auth/checkBalances';
|
|
6
|
-
import { agenticSignup } from 'helius-sdk/auth/agenticSignup';
|
|
7
|
-
import { getCheckoutPreview, executeCheckout, executeRenewal } from 'helius-sdk/auth/checkout';
|
|
8
5
|
import { listProjects } from 'helius-sdk/auth/listProjects';
|
|
9
6
|
import { getProject } from 'helius-sdk/auth/getProject';
|
|
10
|
-
import {
|
|
7
|
+
import { getCheckoutPreview } from 'helius-sdk/auth/checkout';
|
|
8
|
+
import { signup } from 'helius-sdk/auth/signup';
|
|
9
|
+
import { signupAndPay } from 'helius-sdk/auth/signupAndPay';
|
|
10
|
+
import { upgradePlan, upgradePlanAndPay } from 'helius-sdk/auth/upgradePlan';
|
|
11
|
+
import { purchaseCredits as sdkPurchaseCredits, purchaseCreditsAndPay, } from 'helius-sdk/auth/purchaseCredits';
|
|
12
|
+
import { payRenewal, payRenewalAndPay } from 'helius-sdk/auth/payRenewal';
|
|
11
13
|
import { MCP_USER_AGENT } from '../http.js';
|
|
12
|
-
import { setApiKey, hasApiKey, setSessionSecretKey, setSessionWalletAddress,
|
|
14
|
+
import { setApiKey, hasApiKey, setSessionSecretKey, setSessionWalletAddress, loadSignerOrFail, } from '../utils/helius.js';
|
|
13
15
|
import { mcpText, mcpError, handleToolError } from '../utils/errors.js';
|
|
14
16
|
import { fetchDoc, extractSections } from '../utils/docs.js';
|
|
15
17
|
import { sendFeedbackEvent, captureWalletAddress } from '../utils/feedback.js';
|
|
16
|
-
import { setSharedApiKey, setJwt, getJwt, SHARED_CONFIG_PATH, KEYPAIR_PATH, loadKeypairFromDisk, saveKeypairToDisk, keypairExistsOnDisk } from '../utils/config.js';
|
|
18
|
+
import { setSharedApiKey, setJwt, getJwt, SHARED_CONFIG_PATH, KEYPAIR_PATH, loadKeypairFromDisk, saveKeypairToDisk, keypairExistsOnDisk, } from '../utils/config.js';
|
|
17
19
|
import { HELIUS_PLANS } from './plans.js';
|
|
18
|
-
const
|
|
20
|
+
const renderPaymentLink = (paymentLink, flowName, resumeCmd) => `**${flowName}: payment required**\n\n` +
|
|
21
|
+
`Open this link in a browser to pay:\n\n` +
|
|
22
|
+
`\`${paymentLink.paymentUrl}\`\n\n` +
|
|
23
|
+
`Or send USDC + memo manually:\n` +
|
|
24
|
+
`- **Amount:** ${paymentLink.amountCents / 100} USDC\n` +
|
|
25
|
+
`- **Treasury:** \`${paymentLink.destinationWallet}\`\n` +
|
|
26
|
+
`- **Memo:** \`${paymentLink.memo}\`\n` +
|
|
27
|
+
`- **Plan:** ${paymentLink.planName}\n\n` +
|
|
28
|
+
`After paying, call \`${resumeCmd}\` to confirm activation locally.`;
|
|
19
29
|
export function registerAuthTools(server) {
|
|
20
|
-
// ── Getting Started
|
|
30
|
+
// ── Getting Started ──
|
|
21
31
|
server.tool('getStarted', 'Get setup instructions for Helius. Checks whether an API key is configured (not validated), whether a keypair exists on disk, and whether a JWT session is present, then tells you exactly what to do next. Call this when a user asks "how do I get started?" or needs onboarding help.', {}, async () => {
|
|
22
32
|
const lines = ['# Getting Started with Helius'];
|
|
23
33
|
const apiKeyConfigured = hasApiKey();
|
|
24
34
|
const hasKeypair = keypairExistsOnDisk();
|
|
25
35
|
const jwt = getJwt();
|
|
26
|
-
//
|
|
36
|
+
// Already fully set up
|
|
27
37
|
if (apiKeyConfigured && jwt) {
|
|
28
|
-
lines.push('', 'You\'re all set! Your API key and account session are configured.', '', '**What you can do:**', '- Query NFTs and tokens: `getAssetsByOwner`, `searchAssets`', '- Check balances: `getBalance`, `getTokenAccounts`', '- Parse transactions: `parseTransactions`', '- Manage webhooks: `createWebhook`, `getAllWebhooks`', '- Check your account: `getAccountStatus`', '', 'Just ask a question in plain English and the right tool will be used automatically.', '', '**IMPORTANT — if the user described a project they want to build, call `recommendStack` now
|
|
38
|
+
lines.push('', 'You\'re all set! Your API key and account session are configured.', '', '**What you can do:**', '- Query NFTs and tokens: `getAssetsByOwner`, `searchAssets`', '- Check balances: `getBalance`, `getTokenAccounts`', '- Parse transactions: `parseTransactions`', '- Manage webhooks: `createWebhook`, `getAllWebhooks`', '- Check your account: `getAccountStatus`', '', 'Just ask a question in plain English and the right tool will be used automatically.', '', '**IMPORTANT — if the user described a project they want to build, call `recommendStack` now.** It returns architecture recommendations with Helius products, MCP tools, credit costs, and reference files.');
|
|
29
39
|
return mcpText(lines.join('\n'));
|
|
30
40
|
}
|
|
31
|
-
//
|
|
41
|
+
// API key set but no JWT
|
|
32
42
|
if (apiKeyConfigured) {
|
|
33
|
-
lines.push('', 'Your API key is configured — all Helius tools are ready to use.', '', '
|
|
43
|
+
lines.push('', 'Your API key is configured — all Helius tools are ready to use.', '', 'To enable account-status info (plan, credits, rate limits), call `signup`. It will detect your existing account, no payment required.');
|
|
34
44
|
return mcpText(lines.join('\n'));
|
|
35
45
|
}
|
|
36
|
-
//
|
|
37
|
-
lines.push('', 'You need a Helius API key to use these tools.
|
|
38
|
-
// Adapt steps based on whether a keypair already exists
|
|
46
|
+
// No API key — need to set up
|
|
47
|
+
lines.push('', 'You need a Helius API key to use these tools. Three paths:', '', '## Path A — I already have an API key', '', 'Call `setHeliusApiKey` with your key from https://dashboard.helius.dev. All tools immediately available.', '', '## Path B — I have an existing dashboard.helius.dev account', '', 'If you already signed up at https://dashboard.helius.dev (email, Google, GitHub, or SSO), run `helius login` in your terminal:', '', '```bash', 'npx helius-cli@latest login', '```', '', 'This opens your default browser to authenticate via OAuth/PKCE and writes a session token to `~/.helius/config.json`. This MCP picks it up automatically — no further setup. Then call `setHeliusApiKey` with one of your keys (visible via `helius apikeys`), or just call `getAccountStatus` to confirm the session.', '', '## Path C — Create a new account via crypto checkout', '', '1. Call `generateKeypair` to create (or load) a Solana wallet — required for every signup mode, including link mode (the wallet address is bound to the payment intent).', '2. Call `signup` with your email/name. By default (`mode: "link"`) it returns a hosted-checkout URL the user can open in a browser to pay with any wallet.', '3. After the user pays, call `signup` again with `mode: "resume"` to poll the intent and provision the API key.', '', 'For an autopay flow that pays USDC directly from the local keypair, pass `mode: "autopay"` to `signup`. The wallet must hold ~0.001 SOL for fees and the plan amount in USDC.', '');
|
|
39
48
|
if (hasKeypair) {
|
|
40
|
-
lines.push(
|
|
49
|
+
lines.push(`_(A keypair already exists at \`${KEYPAIR_PATH}\` — \`signup\` will reuse it; you can skip step 1.)_`, '');
|
|
41
50
|
}
|
|
42
|
-
else {
|
|
43
|
-
lines.push('### Step 1: Generate a keypair', 'Call the `generateKeypair` tool. It creates a Solana wallet and returns the address.', '');
|
|
44
|
-
}
|
|
45
|
-
lines.push('### Step 2: Fund the wallet', 'Send the following to the wallet address from Step 1:', '- **~0.001 SOL** — covers transaction fees', '- **1 USDC** — pays for the basic plan ($1)', '', 'You can send from any Solana wallet, exchange, or on-ramp.', 'The USDC token mint on Solana is: `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`', '', `For paid plans, send more USDC instead: ${PAID_PLAN_ORDER.filter(k => k in PLAN_CATALOG).map(k => `$${PLAN_CATALOG[k].monthlyPrice / 100} (${PLAN_CATALOG[k].name})`).join(', ')}.`, '', '### Step 3: Verify funding', 'Call `checkSignupBalance` to confirm your SOL and USDC balances are sufficient.', '', '### Step 4: Create the account', 'Call `agenticSignup` to process the payment and create your Helius account.', 'Your API key will be configured automatically — no extra steps needed.', '', '> **Paid plans only:** `agenticSignup` requires `email`, `firstName`, and `lastName` for developer/business/professional plans. Basic plan ($1) does not require them.', '', '---', '', '## Path C — Use the Helius CLI', '', 'Same flow from the terminal:', '```', 'npx helius-cli@latest keygen # Generate keypair', '# Fund the wallet address shown above', 'npx helius-cli@latest signup # Verify balance + create account', '```', '', '---', '', '**After setup:** Use `recommendStack` to plan your project — describe what you\'re building and get architecture recommendations at different cost and complexity levels.');
|
|
46
51
|
return mcpText(lines.join('\n'));
|
|
47
52
|
});
|
|
48
53
|
// ── Keypair Generation ──
|
|
49
|
-
server.tool('generateKeypair', 'Generate a new Solana keypair
|
|
54
|
+
server.tool('generateKeypair', 'Generate a new Solana keypair (or load the existing one from disk). Returns the wallet address. Required before any `signup` call — the wallet address is bound to the payment intent in both link and autopay modes.', {}, async () => {
|
|
50
55
|
try {
|
|
51
|
-
// Check disk first — reuse existing keypair if available
|
|
52
56
|
const existingKey = loadKeypairFromDisk();
|
|
53
57
|
if (existingKey) {
|
|
54
58
|
const walletKeypair = loadKeypair(existingKey);
|
|
@@ -57,13 +61,8 @@ export function registerAuthTools(server) {
|
|
|
57
61
|
setSessionWalletAddress(address);
|
|
58
62
|
captureWalletAddress(address);
|
|
59
63
|
return mcpText(`**Existing Keypair Loaded** from \`${KEYPAIR_PATH}\`\n\n` +
|
|
60
|
-
`**Wallet Address:** \`${address}
|
|
61
|
-
`To create a Helius account, fund this wallet with:\n` +
|
|
62
|
-
`- **~0.001 SOL** for transaction fees\n` +
|
|
63
|
-
`- **1 USDC** for basic plan (or more for paid plans)\n\n` +
|
|
64
|
-
`Then call \`agenticSignup\` to complete account creation.`);
|
|
64
|
+
`**Wallet Address:** \`${address}\``);
|
|
65
65
|
}
|
|
66
|
-
// Generate new keypair and persist to disk
|
|
67
66
|
const keypair = await generateKeypair();
|
|
68
67
|
const walletKeypair = loadKeypair(keypair.secretKey);
|
|
69
68
|
const address = await getAddress(walletKeypair);
|
|
@@ -73,69 +72,33 @@ export function registerAuthTools(server) {
|
|
|
73
72
|
captureWalletAddress(address);
|
|
74
73
|
return mcpText(`**Keypair Generated**\n\n` +
|
|
75
74
|
`**Wallet Address:** \`${address}\`\n` +
|
|
76
|
-
`**Saved to:** \`${KEYPAIR_PATH}
|
|
77
|
-
`To create a Helius account, fund this wallet with:\n` +
|
|
78
|
-
`- **~0.001 SOL** for transaction fees\n` +
|
|
79
|
-
`- **1 USDC** for basic plan (or more for paid plans)\n\n` +
|
|
80
|
-
`Then call \`agenticSignup\` to complete account creation.`);
|
|
75
|
+
`**Saved to:** \`${KEYPAIR_PATH}\``);
|
|
81
76
|
}
|
|
82
77
|
catch (err) {
|
|
83
78
|
return handleToolError(err, 'Error generating keypair');
|
|
84
79
|
}
|
|
85
80
|
});
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const usdcOk = usdcBalance >= 1000000n; // 1 USDC for basic plan
|
|
108
|
-
let status;
|
|
109
|
-
if (solOk && usdcOk) {
|
|
110
|
-
status = 'Ready for signup (basic plan). For paid plans, ensure sufficient USDC for the plan price.';
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
const missing = [];
|
|
114
|
-
if (!solOk)
|
|
115
|
-
missing.push(`~0.001 SOL (have ${solAmount.toFixed(6)})`);
|
|
116
|
-
if (!usdcOk)
|
|
117
|
-
missing.push(`1 USDC (have ${usdcAmount.toFixed(2)})`);
|
|
118
|
-
status = `Need more funds: ${missing.join(', ')}`;
|
|
119
|
-
}
|
|
120
|
-
return mcpText(`**Signup Wallet Balance** (\`${address}\`)\n\n` +
|
|
121
|
-
`- **SOL:** ${solAmount.toFixed(6)} ${solOk ? '(sufficient)' : '(insufficient)'}\n` +
|
|
122
|
-
`- **USDC:** ${usdcAmount.toFixed(2)} ${usdcOk ? '(sufficient for basic)' : '(insufficient)'}\n\n` +
|
|
123
|
-
`**Status:** ${status}`);
|
|
124
|
-
}
|
|
125
|
-
catch (err) {
|
|
126
|
-
return handleToolError(err, 'Error checking balances');
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
server.tool('agenticSignup', 'Create a Helius account using the generated keypair. Default: basic plan ($1 USDC). For paid plans (developer/business/professional), email, firstName, and lastName are required. On success, automatically configures the API key for this session.', {
|
|
130
|
-
plan: z.string().optional().describe('Plan to sign up for: "basic" ($1, default), "developer", "business", or "professional"'),
|
|
131
|
-
period: z.enum(["monthly", "yearly"]).optional().describe('Billing period for paid plans (default: monthly)'),
|
|
132
|
-
email: z.string().email().optional().describe('Email address (required for paid plans)'),
|
|
133
|
-
firstName: z.string().optional().describe('First name (required for paid plans)'),
|
|
134
|
-
lastName: z.string().optional().describe('Last name (required for paid plans)'),
|
|
135
|
-
couponCode: z.string().optional().describe('Coupon code for paid plans'),
|
|
136
|
-
discoveryPath: z.string().optional().describe('How did you discover Helius? Helps us improve agent onboarding'),
|
|
137
|
-
frictionPoints: z.string().optional().describe('What friction did you hit finding or setting up Helius? Free-form feedback'),
|
|
138
|
-
}, async ({ plan, period, email, firstName, lastName, couponCode, discoveryPath, frictionPoints }) => {
|
|
81
|
+
// ── Signup ──
|
|
82
|
+
server.tool('signup', 'Create a Helius account via crypto checkout. Requires `generateKeypair` to have been called first (the wallet address is bound to the payment intent in every mode). Default `mode: "link"` returns a hosted-checkout URL the user opens in a browser to pay USDC. `mode: "autopay"` sends USDC + memo directly from the local keypair. `mode: "resume"` polls a pending payment intent and provisions the API key after the user has paid in a browser. NOTE: resume strictly polls the JWT-bound project list and cannot distinguish "pending signup awaiting browser pay" from "stale JWT left over from a long-completed signup" — if the wallet already has an API key, prefer `setHeliusApiKey` over re-resuming.', {
|
|
83
|
+
mode: z
|
|
84
|
+
.enum(['link', 'autopay', 'resume'])
|
|
85
|
+
.default('link')
|
|
86
|
+
.describe('link (default): print payment URL and exit. autopay: send USDC + memo from local keypair. resume: poll an in-flight payment intent.'),
|
|
87
|
+
plan: z
|
|
88
|
+
.enum(['agent', 'developer', 'business', 'professional'])
|
|
89
|
+
.default('agent')
|
|
90
|
+
.describe('Plan: agent ($10 one-time, default), developer, business, professional'),
|
|
91
|
+
period: z
|
|
92
|
+
.enum(['monthly', 'yearly'])
|
|
93
|
+
.optional()
|
|
94
|
+
.describe('Billing period for subscription plans (default: monthly). Ignored for agent.'),
|
|
95
|
+
email: z.string().email().optional().describe('Email (required for fresh signup; not needed for resume)'),
|
|
96
|
+
firstName: z.string().optional().describe('First name (required for fresh signup)'),
|
|
97
|
+
lastName: z.string().optional().describe('Last name (required for fresh signup)'),
|
|
98
|
+
couponCode: z.string().optional().describe('Coupon code'),
|
|
99
|
+
discoveryPath: z.string().optional().describe('How did you discover Helius?'),
|
|
100
|
+
frictionPoints: z.string().optional().describe('What friction did you hit?'),
|
|
101
|
+
}, async ({ mode, plan, period, email, firstName, lastName, couponCode, discoveryPath, frictionPoints }) => {
|
|
139
102
|
if (discoveryPath || frictionPoints) {
|
|
140
103
|
sendFeedbackEvent({
|
|
141
104
|
type: 'discovery',
|
|
@@ -144,69 +107,135 @@ export function registerAuthTools(server) {
|
|
|
144
107
|
});
|
|
145
108
|
}
|
|
146
109
|
try {
|
|
110
|
+
// Resume mode: just poll the existing JWT/intent. Caller must have
|
|
111
|
+
// already gone through link mode in a previous invocation; the JWT
|
|
112
|
+
// from that invocation is on disk.
|
|
113
|
+
if (mode === 'resume') {
|
|
114
|
+
return await handleResumeSignup();
|
|
115
|
+
}
|
|
147
116
|
let signerData;
|
|
148
117
|
try {
|
|
149
118
|
signerData = await loadSignerOrFail();
|
|
150
119
|
}
|
|
151
120
|
catch {
|
|
152
|
-
return mcpError('No signup keypair found. Call `generateKeypair` first to create a wallet,
|
|
153
|
-
}
|
|
154
|
-
|
|
121
|
+
return mcpError('No signup keypair found. Call `generateKeypair` first to create a wallet, then retry.', { type: 'AUTH', code: 'NO_KEYPAIR', retryable: false, recovery: 'Call `generateKeypair`.' });
|
|
122
|
+
}
|
|
123
|
+
if (mode === 'autopay') {
|
|
124
|
+
const result = await signupAndPay({
|
|
125
|
+
secretKey: signerData.secretKey,
|
|
126
|
+
plan: plan,
|
|
127
|
+
period,
|
|
128
|
+
email,
|
|
129
|
+
firstName,
|
|
130
|
+
lastName,
|
|
131
|
+
couponCode,
|
|
132
|
+
});
|
|
133
|
+
return renderSignupAndPayResult(result);
|
|
134
|
+
}
|
|
135
|
+
// mode === "link" (default)
|
|
136
|
+
const result = await signup({
|
|
155
137
|
secretKey: signerData.secretKey,
|
|
156
|
-
|
|
157
|
-
plan,
|
|
138
|
+
plan: plan,
|
|
158
139
|
period,
|
|
159
140
|
email,
|
|
160
141
|
firstName,
|
|
161
142
|
lastName,
|
|
162
143
|
couponCode,
|
|
163
144
|
});
|
|
164
|
-
|
|
165
|
-
if (result.apiKey) {
|
|
145
|
+
if (result.kind === 'already_subscribed') {
|
|
166
146
|
setApiKey(result.apiKey);
|
|
167
147
|
setSharedApiKey(result.apiKey);
|
|
168
|
-
}
|
|
169
|
-
// Persist JWT to disk
|
|
170
|
-
if (result.jwt) {
|
|
171
148
|
setJwt(result.jwt);
|
|
172
|
-
}
|
|
173
|
-
const saveNote = result.apiKey
|
|
174
|
-
? `\nAPI key configured for this session and saved to \`${SHARED_CONFIG_PATH}\`. All Helius tools are now ready to use.`
|
|
175
|
-
: '';
|
|
176
|
-
if (result.status === 'existing_project') {
|
|
177
149
|
return mcpText(`**Helius Account Found**\n\n` +
|
|
178
|
-
`You already have a Helius account. No payment was needed.\n\n` +
|
|
179
|
-
`- **Wallet:** \`${result.walletAddress}\`\n` +
|
|
150
|
+
`You already have a Helius account on this plan. No payment was needed.\n\n` +
|
|
180
151
|
`- **Project ID:** \`${result.projectId}\`\n` +
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
152
|
+
`- **API Key:** \`${result.apiKey}\`\n` +
|
|
153
|
+
`- **Mainnet RPC:** \`${result.endpoints.mainnet}\`\n` +
|
|
154
|
+
`- **Devnet RPC:** \`${result.endpoints.devnet}\`\n\n` +
|
|
155
|
+
`API key configured for this session and saved to \`${SHARED_CONFIG_PATH}\`.`);
|
|
156
|
+
}
|
|
157
|
+
if (result.kind === 'upgrade_required') {
|
|
158
|
+
return mcpText(`**Plan change required**\n\n` +
|
|
159
|
+
`Your wallet is already on plan \`${result.currentPlan}\`. ` +
|
|
160
|
+
`Use the \`upgradePlan\` tool to switch to \`${result.requestedPlan}\`.`);
|
|
161
|
+
}
|
|
162
|
+
// payment_required — store JWT so resume mode can use it.
|
|
163
|
+
setJwt(result.jwt);
|
|
164
|
+
return mcpText(renderPaymentLink(result.paymentLink, 'Sign up', 'signup with mode: "resume"'));
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
return handleToolError(err, 'Error during signup');
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
// ── Account Status ──
|
|
171
|
+
server.tool('getAccountStatus', 'Check your Helius account status: current plan, remaining credits, rate limits, and billing cycle. Requires a JWT session (i.e., you signed up via `signup`). If you only have an API key configured, auth status is confirmed but credit data is unavailable — call `signup` to enable full status.', {}, async () => {
|
|
172
|
+
try {
|
|
173
|
+
if (!hasApiKey()) {
|
|
174
|
+
return mcpText(`## Account Status\n\n` +
|
|
175
|
+
`**Auth:** Not authenticated\n\n` +
|
|
176
|
+
`No API key or session found. To get started:\n` +
|
|
177
|
+
`- If you have a key: use the \`setHeliusApiKey\` tool\n` +
|
|
178
|
+
`- If you need an account: use \`signup\``);
|
|
186
179
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
saveNote);
|
|
180
|
+
const jwt = getJwt();
|
|
181
|
+
if (!jwt) {
|
|
182
|
+
return mcpText(`## Account Status\n\n` +
|
|
183
|
+
`**Auth:** Authenticated (API key configured)\n` +
|
|
184
|
+
`**Credit usage:** Not available — no JWT session found\n\n` +
|
|
185
|
+
`To see your plan, rate limits, and credit balance, call \`signup\`. Your existing account will be detected automatically — no payment needed.`);
|
|
194
186
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
187
|
+
const projects = await listProjects(jwt, MCP_USER_AGENT);
|
|
188
|
+
if (projects.length === 0) {
|
|
189
|
+
return mcpError('No projects found. Call `signup` to create an account first.', { type: 'AUTH', code: 'NO_PROJECT', retryable: false, recovery: 'Call `signup`.' });
|
|
190
|
+
}
|
|
191
|
+
const projectId = projects[0].id;
|
|
192
|
+
const details = await getProject(jwt, projectId, MCP_USER_AGENT);
|
|
193
|
+
const planKey = details.subscriptionPlanDetails?.currentPlan ?? 'unknown';
|
|
194
|
+
const upcomingPlan = details.subscriptionPlanDetails?.upcomingPlan;
|
|
195
|
+
const isUpgrading = details.subscriptionPlanDetails?.isUpgrading ?? false;
|
|
196
|
+
const planInfo = HELIUS_PLANS[planKey];
|
|
197
|
+
const usage = details.creditsUsage;
|
|
198
|
+
const cycle = details.billingCycle;
|
|
199
|
+
const lines = [`## Account Status`, ''];
|
|
200
|
+
lines.push(`**Auth:** Authenticated`);
|
|
201
|
+
lines.push(`**Plan:** ${planInfo ? planInfo.name : planKey} | **Project:** \`${projectId}\``);
|
|
202
|
+
if (isUpgrading && upcomingPlan && upcomingPlan !== planKey) {
|
|
203
|
+
lines.push(`**Upcoming plan:** ${upcomingPlan} (takes effect at next billing cycle)`);
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const billingDoc = await fetchDoc('billing');
|
|
207
|
+
const rateLimits = extractSections(billingDoc, ['standard rate limits', 'rate limits'], {
|
|
208
|
+
includeLooseMatches: false,
|
|
209
|
+
});
|
|
210
|
+
if (rateLimits) {
|
|
211
|
+
lines.push('', '### Rate Limits (live)', '', rateLimits);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// best-effort
|
|
216
|
+
}
|
|
217
|
+
if (usage) {
|
|
218
|
+
const total = usage.remainingCredits + usage.totalCreditsUsed;
|
|
219
|
+
const pctUsed = total > 0 ? ((usage.totalCreditsUsed / total) * 100).toFixed(1) : '0.0';
|
|
220
|
+
const pctRemaining = total > 0 ? (100 - parseFloat(pctUsed)).toFixed(1) : '100.0';
|
|
221
|
+
let cycleStr = '';
|
|
222
|
+
if (cycle) {
|
|
223
|
+
const end = new Date(cycle.end);
|
|
224
|
+
const now = new Date();
|
|
225
|
+
const daysLeft = Math.ceil((end.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
|
226
|
+
cycleStr = ` (${cycle.start} - ${cycle.end}, ${daysLeft} day${daysLeft !== 1 ? 's' : ''} remaining)`;
|
|
227
|
+
}
|
|
228
|
+
lines.push('', `### Credits — Billing Cycle${cycleStr}`);
|
|
229
|
+
lines.push(`- **Remaining:** ${usage.remainingCredits.toLocaleString()} / ${total.toLocaleString()} (${pctRemaining}%)`);
|
|
230
|
+
lines.push(`- **Used:** ${usage.totalCreditsUsed.toLocaleString()} (${pctUsed}%)`);
|
|
231
|
+
}
|
|
232
|
+
return mcpText(lines.join('\n'));
|
|
204
233
|
}
|
|
205
234
|
catch (err) {
|
|
206
|
-
return handleToolError(err, 'Error
|
|
235
|
+
return handleToolError(err, 'Error fetching account status');
|
|
207
236
|
}
|
|
208
237
|
});
|
|
209
|
-
// ── Upgrade
|
|
238
|
+
// ── Upgrade ──
|
|
210
239
|
server.tool('previewUpgrade', 'Preview pricing for a plan upgrade with proration details. Shows current plan, new plan cost, prorated credits, and amount due today.', {
|
|
211
240
|
plan: z.enum(['developer', 'business', 'professional']).describe('Target plan name'),
|
|
212
241
|
period: z.enum(['monthly', 'yearly']).default('monthly').describe('Billing period'),
|
|
@@ -215,11 +244,21 @@ export function registerAuthTools(server) {
|
|
|
215
244
|
try {
|
|
216
245
|
const jwt = getJwt();
|
|
217
246
|
if (!jwt) {
|
|
218
|
-
return mcpError('Not authenticated. Call `
|
|
247
|
+
return mcpError('Not authenticated. Call `signup`, or run `helius login` in your terminal if you have a dashboard.helius.dev account.', {
|
|
248
|
+
type: 'AUTH',
|
|
249
|
+
code: 'NOT_AUTHENTICATED',
|
|
250
|
+
retryable: false,
|
|
251
|
+
recovery: 'Call `signup`, or run `helius login` in your terminal if you already have a dashboard.helius.dev account.',
|
|
252
|
+
});
|
|
219
253
|
}
|
|
220
254
|
const projects = await listProjects(jwt, MCP_USER_AGENT);
|
|
221
255
|
if (projects.length === 0) {
|
|
222
|
-
return mcpError('No projects found. Call `
|
|
256
|
+
return mcpError('No projects found. Call `signup` to create an account first.', {
|
|
257
|
+
type: 'AUTH',
|
|
258
|
+
code: 'NO_PROJECT',
|
|
259
|
+
retryable: false,
|
|
260
|
+
recovery: 'Call `signup`.',
|
|
261
|
+
});
|
|
223
262
|
}
|
|
224
263
|
const projectId = projects[0].id;
|
|
225
264
|
const projectDetails = await getProject(jwt, projectId, MCP_USER_AGENT);
|
|
@@ -242,13 +281,7 @@ export function registerAuthTools(server) {
|
|
|
242
281
|
if (preview.coupon?.valid) {
|
|
243
282
|
text += `- Coupon (${preview.coupon.code}): ${preview.coupon.description || 'Applied'}\n`;
|
|
244
283
|
}
|
|
245
|
-
else if (preview.coupon && !preview.coupon.valid) {
|
|
246
|
-
text += `- Coupon (${preview.coupon.code}): **Invalid** — ${preview.coupon.invalidReason || 'not applicable'}\n`;
|
|
247
|
-
}
|
|
248
284
|
text += `\n**Due Today: $${(preview.dueToday / 100).toFixed(2)}**\n`;
|
|
249
|
-
if (preview.note) {
|
|
250
|
-
text += `\n_${preview.note}_\n`;
|
|
251
|
-
}
|
|
252
285
|
text += `\nTo proceed, use the \`upgradePlan\` tool with plan: '${plan}'.`;
|
|
253
286
|
return mcpText(text);
|
|
254
287
|
}
|
|
@@ -256,204 +289,275 @@ export function registerAuthTools(server) {
|
|
|
256
289
|
return handleToolError(err, 'Error previewing upgrade');
|
|
257
290
|
}
|
|
258
291
|
});
|
|
259
|
-
server.tool('upgradePlan', 'Upgrade your Helius plan.
|
|
292
|
+
server.tool('upgradePlan', 'Upgrade your Helius plan via crypto checkout. Default `mode: "link"` returns a hosted-checkout URL. `mode: "autopay"` pays USDC from the local keypair and polls. Contact info (email/firstName/lastName) is only needed for first-time upgrades — backend auto-fetches from existing customer otherwise.', {
|
|
293
|
+
mode: z.enum(['link', 'autopay']).default('link').describe('link (default) or autopay'),
|
|
260
294
|
plan: z.enum(['developer', 'business', 'professional']).describe('Target plan name'),
|
|
261
295
|
period: z.enum(['monthly', 'yearly']).default('monthly').describe('Billing period'),
|
|
262
296
|
couponCode: z.string().optional().describe('Optional coupon code'),
|
|
263
|
-
email: z.string().email().optional().describe('Email
|
|
264
|
-
firstName: z.string().optional().describe('First name (
|
|
265
|
-
lastName: z.string().optional().describe('Last name (
|
|
266
|
-
}, async ({ plan, period, couponCode, email, firstName, lastName }) => {
|
|
297
|
+
email: z.string().email().optional().describe('Email (only for first-time upgrades)'),
|
|
298
|
+
firstName: z.string().optional().describe('First name (only for first-time upgrades)'),
|
|
299
|
+
lastName: z.string().optional().describe('Last name (only for first-time upgrades)'),
|
|
300
|
+
}, async ({ mode, plan, period, couponCode, email, firstName, lastName }) => {
|
|
267
301
|
try {
|
|
268
|
-
// All-or-none customer info validation
|
|
269
|
-
const hasAny = email || firstName || lastName;
|
|
270
|
-
if (hasAny && (!email || !firstName || !lastName)) {
|
|
271
|
-
const missing = [
|
|
272
|
-
!email && 'email',
|
|
273
|
-
!firstName && 'firstName',
|
|
274
|
-
!lastName && 'lastName',
|
|
275
|
-
].filter(Boolean);
|
|
276
|
-
return mcpError(`Partial customer info provided. If any of email/firstName/lastName is given, all three are required. Missing: ${missing.join(', ')}`);
|
|
277
|
-
}
|
|
278
|
-
let signerData;
|
|
279
|
-
try {
|
|
280
|
-
signerData = await loadSignerOrFail();
|
|
281
|
-
}
|
|
282
|
-
catch {
|
|
283
|
-
return mcpError('No keypair found. Call `generateKeypair` first.');
|
|
284
|
-
}
|
|
285
302
|
const jwt = getJwt();
|
|
286
303
|
if (!jwt) {
|
|
287
|
-
return mcpError('Not authenticated. Call `
|
|
304
|
+
return mcpError('Not authenticated. Call `signup`, or run `helius login` in your terminal if you have a dashboard.helius.dev account.', {
|
|
305
|
+
type: 'AUTH',
|
|
306
|
+
code: 'NOT_AUTHENTICATED',
|
|
307
|
+
retryable: false,
|
|
308
|
+
recovery: 'Call `signup`, or run `helius login` in your terminal if you already have a dashboard.helius.dev account.',
|
|
309
|
+
});
|
|
288
310
|
}
|
|
289
311
|
const projects = await listProjects(jwt, MCP_USER_AGENT);
|
|
290
312
|
if (projects.length === 0) {
|
|
291
|
-
return mcpError('No projects found. Call `
|
|
313
|
+
return mcpError('No projects found. Call `signup` first.', {
|
|
314
|
+
type: 'AUTH',
|
|
315
|
+
code: 'NO_PROJECT',
|
|
316
|
+
retryable: false,
|
|
317
|
+
recovery: 'Call `signup`.',
|
|
318
|
+
});
|
|
292
319
|
}
|
|
293
320
|
const projectId = projects[0].id;
|
|
294
|
-
|
|
321
|
+
if (mode === 'autopay') {
|
|
322
|
+
let signerData;
|
|
323
|
+
try {
|
|
324
|
+
signerData = await loadSignerOrFail();
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
return mcpError('No keypair. Autopay needs one — call `generateKeypair` first.', {
|
|
328
|
+
type: 'AUTH',
|
|
329
|
+
code: 'NO_KEYPAIR',
|
|
330
|
+
retryable: false,
|
|
331
|
+
recovery: 'Call `generateKeypair`.',
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
const result = await upgradePlanAndPay({
|
|
335
|
+
secretKey: signerData.secretKey,
|
|
336
|
+
jwt,
|
|
337
|
+
projectId,
|
|
338
|
+
plan,
|
|
339
|
+
period,
|
|
340
|
+
couponCode,
|
|
341
|
+
email,
|
|
342
|
+
firstName,
|
|
343
|
+
lastName,
|
|
344
|
+
});
|
|
345
|
+
return renderUpgradeOrCreditsAndPayResult(result, 'Upgrade');
|
|
346
|
+
}
|
|
347
|
+
// mode === "link"
|
|
348
|
+
const result = await upgradePlan({
|
|
349
|
+
jwt,
|
|
350
|
+
projectId,
|
|
295
351
|
plan,
|
|
296
352
|
period,
|
|
297
|
-
refId: projectId,
|
|
298
353
|
couponCode,
|
|
299
354
|
email,
|
|
300
355
|
firstName,
|
|
301
356
|
lastName,
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
return mcpError(`**Upgrade ${result.status}**\n\n` +
|
|
305
|
-
(result.error ? `Error: ${result.error}\n` : '') +
|
|
306
|
-
(result.txSignature ? `TX: \`${result.txSignature}\`\n` : '') +
|
|
307
|
-
`\nIf you need help, contact support with the payment intent ID: \`${result.paymentIntentId}\``);
|
|
308
|
-
}
|
|
309
|
-
const planInfo = PLAN_CATALOG[plan];
|
|
310
|
-
return mcpText(`**Plan Upgraded Successfully**\n\n` +
|
|
311
|
-
`- **New Plan:** ${planInfo.name} (${period})\n` +
|
|
312
|
-
`- **Project ID:** \`${projectId}\`\n` +
|
|
313
|
-
(result.txSignature ? `- **Payment TX:** \`${result.txSignature}\`\n` : '') +
|
|
314
|
-
`\nYour new plan is now active with ${(planInfo.credits / 1_000_000).toFixed(0)}M credits and ${planInfo.requestsPerSecond} RPS.`);
|
|
357
|
+
});
|
|
358
|
+
return mcpText(renderPaymentLink(result.paymentLink, 'Upgrade', 'getAccountStatus'));
|
|
315
359
|
}
|
|
316
360
|
catch (err) {
|
|
317
361
|
return handleToolError(err, 'Error upgrading plan');
|
|
318
362
|
}
|
|
319
363
|
});
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
'
|
|
323
|
-
'
|
|
364
|
+
// ── Prepaid Credits ──
|
|
365
|
+
server.tool('purchaseCredits', 'Buy prepaid credits as a one-time USDC top-up. AGENT PLAN ONLY: each unit of `qty` is 1,000,000 credits at $10 USDC, no recurring sub. Subscription plans (Developer / Business / Professional) get monthly allotments and overage is auto-billed at $5/M on the next invoice — they can NOT use this tool; this tool will reject the call with UNSUPPORTED_PLAN. Default `mode: "link"` returns a hosted-checkout URL; `mode: "autopay"` pays USDC from the local keypair and polls.', {
|
|
366
|
+
mode: z.enum(['link', 'autopay']).default('link').describe('link (default) or autopay'),
|
|
367
|
+
qty: z.number().int().min(1).default(1).describe('Quantity multiplier (each unit = 1M credits at $10 USDC)'),
|
|
368
|
+
couponCode: z.string().optional().describe('Optional coupon code'),
|
|
369
|
+
}, async ({ mode, qty, couponCode }) => {
|
|
324
370
|
try {
|
|
325
|
-
// ── Tier 1: not authenticated at all ──
|
|
326
|
-
if (!hasApiKey()) {
|
|
327
|
-
return mcpText(`## Account Status\n\n` +
|
|
328
|
-
`**Auth:** Not authenticated\n\n` +
|
|
329
|
-
`No API key or session found. To get started:\n` +
|
|
330
|
-
`- If you have a key: use the \`setHeliusApiKey\` tool\n` +
|
|
331
|
-
`- If you need an account: use \`generateKeypair\` → fund wallet → \`agenticSignup\``);
|
|
332
|
-
}
|
|
333
|
-
// ── Tier 2: API key present but no JWT — can't reach dashboard API ──
|
|
334
371
|
const jwt = getJwt();
|
|
335
372
|
if (!jwt) {
|
|
336
|
-
return
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
`
|
|
373
|
+
return mcpError('Not authenticated. Call `signup`, or run `helius login` in your terminal if you have a dashboard.helius.dev account.', {
|
|
374
|
+
type: 'AUTH',
|
|
375
|
+
code: 'NOT_AUTHENTICATED',
|
|
376
|
+
retryable: false,
|
|
377
|
+
recovery: 'Call `signup`, or run `helius login` in your terminal if you already have a dashboard.helius.dev account.',
|
|
378
|
+
});
|
|
341
379
|
}
|
|
342
|
-
// ── Tier 3: full status via JWT ──
|
|
343
380
|
const projects = await listProjects(jwt, MCP_USER_AGENT);
|
|
344
381
|
if (projects.length === 0) {
|
|
345
|
-
return mcpError('No projects found. Call `
|
|
382
|
+
return mcpError('No projects found. Call `signup` first.', {
|
|
383
|
+
type: 'AUTH',
|
|
384
|
+
code: 'NO_PROJECT',
|
|
385
|
+
retryable: false,
|
|
386
|
+
recovery: 'Call `signup`.',
|
|
387
|
+
});
|
|
346
388
|
}
|
|
347
389
|
const projectId = projects[0].id;
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
}
|
|
369
|
-
else {
|
|
370
|
-
lines.push('', '_Rate limit details: use `getRateLimitInfo` or visit https://www.helius.dev/docs/billing_');
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
catch {
|
|
374
|
-
lines.push('', '_Rate limit details: use `getRateLimitInfo` or visit https://www.helius.dev/docs/billing_');
|
|
375
|
-
}
|
|
376
|
-
// ── Credits ──
|
|
377
|
-
if (usage) {
|
|
378
|
-
const total = usage.remainingCredits + usage.totalCreditsUsed;
|
|
379
|
-
const pctUsed = total > 0 ? ((usage.totalCreditsUsed / total) * 100).toFixed(1) : '0.0';
|
|
380
|
-
const pctRemaining = total > 0 ? (100 - parseFloat(pctUsed)).toFixed(1) : '100.0';
|
|
381
|
-
// Billing cycle + days remaining
|
|
382
|
-
let cycleStr = '';
|
|
383
|
-
let daysNote = '';
|
|
384
|
-
if (cycle) {
|
|
385
|
-
const end = new Date(cycle.end);
|
|
386
|
-
const now = new Date();
|
|
387
|
-
const daysLeft = Math.ceil((end.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
|
388
|
-
cycleStr = ` (${cycle.start} - ${cycle.end}, ${daysLeft} day${daysLeft !== 1 ? 's' : ''} remaining)`;
|
|
389
|
-
// Burn-rate warning: project usage over elapsed days to end of cycle
|
|
390
|
-
const start = new Date(cycle.start);
|
|
391
|
-
const totalDays = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24));
|
|
392
|
-
const elapsedDays = totalDays - daysLeft;
|
|
393
|
-
if (elapsedDays > 0 && daysLeft > 0) {
|
|
394
|
-
const projectedTotal = Math.round((usage.totalCreditsUsed / elapsedDays) * totalDays);
|
|
395
|
-
if (projectedTotal > total) {
|
|
396
|
-
const overageM = ((projectedTotal - total) / 1_000_000).toFixed(1);
|
|
397
|
-
daysNote = `\n> At current burn rate you're projected to use ~${(projectedTotal / 1_000_000).toFixed(1)}M credits this cycle — ${overageM}M over your ${(total / 1_000_000).toFixed(0)}M limit. Consider upgrading or reducing usage.`;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
lines.push('', `### Credits — Billing Cycle${cycleStr}`);
|
|
402
|
-
lines.push(`- **Remaining:** ${usage.remainingCredits.toLocaleString()} / ${total.toLocaleString()} (${pctRemaining}%)`);
|
|
403
|
-
lines.push(`- **Used:** ${usage.totalCreditsUsed.toLocaleString()} (${pctUsed}%)`);
|
|
404
|
-
lines.push(` - API: ${usage.apiUsage.toLocaleString()}`);
|
|
405
|
-
lines.push(` - RPC: ${usage.rpcUsage.toLocaleString()} | RPC GPA: ${usage.rpcGPAUsage.toLocaleString()} | Webhooks: ${usage.webhookUsage.toLocaleString()}`);
|
|
406
|
-
if (usage.overageCreditsUsed > 0) {
|
|
407
|
-
lines.push(`- **Overage:** ${usage.overageCreditsUsed.toLocaleString()} credits ($${usage.overageCost.toFixed(2)})`);
|
|
390
|
+
// Pre-flight plan check. SDK will reject non-Agent plans, but its
|
|
391
|
+
// error path surfaces as a generic SDK_ERROR; intercepting here gives
|
|
392
|
+
// the agent a clean, structured error with the correct semantics.
|
|
393
|
+
const projectPlan = projects[0].subscription?.plan;
|
|
394
|
+
const normalizedPlan = projectPlan?.replace(/_v\d+$/, '');
|
|
395
|
+
if (normalizedPlan && normalizedPlan !== 'agent') {
|
|
396
|
+
return mcpError(`Prepaid credits via \`purchaseCredits\` are only available on the Agent plan. ` +
|
|
397
|
+
`Project ${projectId} is on the ${normalizedPlan} plan, where credit overage ` +
|
|
398
|
+
`is auto-billed at $5 per 1,000,000 credits on the next invoice — there's no ` +
|
|
399
|
+
`manual top-up flow. To preview your usage, call \`getAccountStatus\`.`, {
|
|
400
|
+
type: 'UNSUPPORTED',
|
|
401
|
+
code: 'UNSUPPORTED_PLAN',
|
|
402
|
+
retryable: false,
|
|
403
|
+
recovery: 'Use the dashboard to manage subscription overage, or call `getAccountStatus` to inspect usage.',
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
if (mode === 'autopay') {
|
|
407
|
+
let signerData;
|
|
408
|
+
try {
|
|
409
|
+
signerData = await loadSignerOrFail();
|
|
408
410
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
+
catch {
|
|
412
|
+
return mcpError('No keypair. Autopay needs one — call `generateKeypair` first.', {
|
|
413
|
+
type: 'AUTH',
|
|
414
|
+
code: 'NO_KEYPAIR',
|
|
415
|
+
retryable: false,
|
|
416
|
+
recovery: 'Call `generateKeypair`.',
|
|
417
|
+
});
|
|
411
418
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
}
|
|
422
|
-
return mcpText(
|
|
419
|
+
const result = await purchaseCreditsAndPay({
|
|
420
|
+
secretKey: signerData.secretKey,
|
|
421
|
+
jwt,
|
|
422
|
+
projectId,
|
|
423
|
+
qty,
|
|
424
|
+
couponCode,
|
|
425
|
+
});
|
|
426
|
+
return renderUpgradeOrCreditsAndPayResult(result, 'Credits top-up');
|
|
427
|
+
}
|
|
428
|
+
const result = await sdkPurchaseCredits({ jwt, projectId, qty, couponCode });
|
|
429
|
+
return mcpText(renderPaymentLink(result.paymentLink, 'Credits top-up', 'getAccountStatus'));
|
|
423
430
|
}
|
|
424
431
|
catch (err) {
|
|
425
|
-
return handleToolError(err, 'Error
|
|
432
|
+
return handleToolError(err, 'Error purchasing credits');
|
|
426
433
|
}
|
|
427
434
|
});
|
|
428
|
-
|
|
435
|
+
// ── Renewal Pay ──
|
|
436
|
+
server.tool('payRenewal', 'Pay an existing renewal payment intent. Default `mode: "link"` returns the hosted-checkout URL for the renewal. `mode: "autopay"` pays USDC from the local keypair.', {
|
|
437
|
+
mode: z.enum(['link', 'autopay']).default('link').describe('link (default) or autopay'),
|
|
429
438
|
paymentIntentId: z.string().describe('Payment intent ID from renewal notification'),
|
|
430
|
-
}, async ({ paymentIntentId }) => {
|
|
439
|
+
}, async ({ mode, paymentIntentId }) => {
|
|
431
440
|
try {
|
|
432
|
-
let signerData;
|
|
433
|
-
try {
|
|
434
|
-
signerData = await loadSignerOrFail();
|
|
435
|
-
}
|
|
436
|
-
catch {
|
|
437
|
-
return mcpError('No keypair found. Call `generateKeypair` first.');
|
|
438
|
-
}
|
|
439
441
|
const jwt = getJwt();
|
|
440
442
|
if (!jwt) {
|
|
441
|
-
return mcpError('Not authenticated. Call `
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
443
|
+
return mcpError('Not authenticated. Call `signup`, or run `helius login` in your terminal if you have a dashboard.helius.dev account.', {
|
|
444
|
+
type: 'AUTH',
|
|
445
|
+
code: 'NOT_AUTHENTICATED',
|
|
446
|
+
retryable: false,
|
|
447
|
+
recovery: 'Call `signup`, or run `helius login` in your terminal if you already have a dashboard.helius.dev account.',
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
if (mode === 'autopay') {
|
|
451
|
+
let signerData;
|
|
452
|
+
try {
|
|
453
|
+
signerData = await loadSignerOrFail();
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
return mcpError('No keypair. Autopay needs one — call `generateKeypair` first.', {
|
|
457
|
+
type: 'AUTH',
|
|
458
|
+
code: 'NO_KEYPAIR',
|
|
459
|
+
retryable: false,
|
|
460
|
+
recovery: 'Call `generateKeypair`.',
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
const result = await payRenewalAndPay(signerData.secretKey, jwt, paymentIntentId);
|
|
464
|
+
return renderUpgradeOrCreditsAndPayResult(result, 'Renewal');
|
|
449
465
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
(result.txSignature ? `- **TX:** \`${result.txSignature}\`\n` : '') +
|
|
453
|
-
`\nYour subscription has been renewed successfully.`);
|
|
466
|
+
const result = await payRenewal(jwt, paymentIntentId);
|
|
467
|
+
return mcpText(renderPaymentLink(result.paymentLink, 'Renewal', 'payRenewal again with mode: "autopay" or open the link'));
|
|
454
468
|
}
|
|
455
469
|
catch (err) {
|
|
456
470
|
return handleToolError(err, 'Error processing renewal payment');
|
|
457
471
|
}
|
|
458
472
|
});
|
|
459
473
|
}
|
|
474
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
475
|
+
// Resume / result rendering helpers
|
|
476
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
477
|
+
async function handleResumeSignup() {
|
|
478
|
+
const jwt = getJwt();
|
|
479
|
+
if (!jwt) {
|
|
480
|
+
return mcpError('No pending signup. Call `signup` (link mode) first.', {
|
|
481
|
+
type: 'AUTH',
|
|
482
|
+
code: 'NOT_AUTHENTICATED',
|
|
483
|
+
retryable: false,
|
|
484
|
+
recovery: 'Call `signup`.',
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
const projects = await listProjects(jwt, MCP_USER_AGENT);
|
|
488
|
+
if (projects.length === 0) {
|
|
489
|
+
return mcpText('Payment not yet provisioned on the backend. The webhook may still be processing — try again in a moment.');
|
|
490
|
+
}
|
|
491
|
+
const projectId = projects[0].id;
|
|
492
|
+
const details = await getProject(jwt, projectId, MCP_USER_AGENT);
|
|
493
|
+
const apiKey = details.apiKeys?.[0]?.keyId;
|
|
494
|
+
if (!apiKey) {
|
|
495
|
+
return mcpText('Project is provisioned but no API key has been generated yet. Try again in a moment.');
|
|
496
|
+
}
|
|
497
|
+
setApiKey(apiKey);
|
|
498
|
+
setSharedApiKey(apiKey);
|
|
499
|
+
return mcpText(`**Signup complete!**\n\n` +
|
|
500
|
+
`- **Project ID:** \`${projectId}\`\n` +
|
|
501
|
+
`- **API Key:** \`${apiKey}\`\n\n` +
|
|
502
|
+
`API key configured for this session and saved to \`${SHARED_CONFIG_PATH}\`. All Helius tools are ready to use.`);
|
|
503
|
+
}
|
|
504
|
+
function renderSignupAndPayResult(result) {
|
|
505
|
+
switch (result.kind) {
|
|
506
|
+
case 'completed':
|
|
507
|
+
setApiKey(result.apiKey);
|
|
508
|
+
setSharedApiKey(result.apiKey);
|
|
509
|
+
setJwt(result.jwt);
|
|
510
|
+
return mcpText(`**Helius Account Created**\n\n` +
|
|
511
|
+
`- **Project ID:** \`${result.projectId}\`\n` +
|
|
512
|
+
`- **API Key:** \`${result.apiKey}\`\n` +
|
|
513
|
+
`- **Mainnet RPC:** \`${result.endpoints.mainnet}\`\n` +
|
|
514
|
+
(result.txSignature ? `- **Payment TX:** \`${result.txSignature}\`\n` : '') +
|
|
515
|
+
`\nAPI key configured for this session and saved to \`${SHARED_CONFIG_PATH}\`.`);
|
|
516
|
+
case 'already_subscribed':
|
|
517
|
+
setApiKey(result.apiKey);
|
|
518
|
+
setSharedApiKey(result.apiKey);
|
|
519
|
+
setJwt(result.jwt);
|
|
520
|
+
return mcpText(`**Helius Account Found**\n\n` +
|
|
521
|
+
`You already have a Helius account on this plan. No payment was needed.\n\n` +
|
|
522
|
+
`- **Project ID:** \`${result.projectId}\`\n` +
|
|
523
|
+
`- **API Key:** \`${result.apiKey}\`\n` +
|
|
524
|
+
`\nAPI key configured for this session.`);
|
|
525
|
+
case 'upgrade_required':
|
|
526
|
+
return mcpText(`**Plan change required**\n\n` +
|
|
527
|
+
`Your wallet is already on plan \`${result.currentPlan}\`. ` +
|
|
528
|
+
`Use the \`upgradePlan\` tool to switch to \`${result.requestedPlan}\`.`);
|
|
529
|
+
case 'pending':
|
|
530
|
+
setJwt(result.jwt);
|
|
531
|
+
return mcpText(`**Payment sent — activation pending**\n\n` +
|
|
532
|
+
`USDC was sent (tx: \`${result.txSignature ?? 'unknown'}\`) but the backend hasn't finished provisioning yet.\n\n` +
|
|
533
|
+
`Call \`signup\` with \`mode: "resume"\` to try again.`);
|
|
534
|
+
case 'expired':
|
|
535
|
+
return mcpError(`Payment intent expired (\`${result.paymentIntentId}\`). Run \`signup\` again to start a fresh one.`, { type: 'API', code: 'EXPIRED', retryable: false, recovery: 'Run `signup` again.' });
|
|
536
|
+
case 'failed':
|
|
537
|
+
return mcpError(`Payment failed: ${result.reason ?? 'unknown reason'} (intent \`${result.paymentIntentId}\`).`, { type: 'API', code: 'OPERATION_FAILED', retryable: false, recovery: 'Contact support if this persists.' });
|
|
538
|
+
default: {
|
|
539
|
+
const _exhaustive = result;
|
|
540
|
+
throw new Error(`Unhandled result kind: ${JSON.stringify(_exhaustive)}`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
function renderUpgradeOrCreditsAndPayResult(result, flowName) {
|
|
545
|
+
switch (result.kind) {
|
|
546
|
+
case 'completed':
|
|
547
|
+
return mcpText(`**${flowName} complete!**\n\n` +
|
|
548
|
+
(result.txSignature ? `- **TX:** \`${result.txSignature}\`\n` : '') +
|
|
549
|
+
`- **Payment Intent:** \`${result.paymentIntentId}\``);
|
|
550
|
+
case 'pending':
|
|
551
|
+
return mcpText(`**Payment sent — activation pending**\n\n` +
|
|
552
|
+
`USDC was sent (tx: \`${result.txSignature ?? 'unknown'}\`). The backend is still processing.\n\n` +
|
|
553
|
+
`Call this tool again or check \`getAccountStatus\` shortly.`);
|
|
554
|
+
case 'expired':
|
|
555
|
+
return mcpError(`Payment intent expired (\`${result.paymentIntentId}\`). Run the tool again to start fresh.`, { type: 'API', code: 'EXPIRED', retryable: false, recovery: 'Retry from scratch.' });
|
|
556
|
+
case 'failed':
|
|
557
|
+
return mcpError(`Payment failed: ${result.reason ?? 'unknown reason'} (intent \`${result.paymentIntentId}\`).`, { type: 'API', code: 'OPERATION_FAILED', retryable: false, recovery: 'Contact support if this persists.' });
|
|
558
|
+
default: {
|
|
559
|
+
const _exhaustive = result;
|
|
560
|
+
throw new Error(`Unhandled result kind: ${JSON.stringify(_exhaustive)}`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|