@candledottv/mcp 0.10.16 → 0.10.18
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/README.md +4 -3
- package/dist/index.js +137 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -126,7 +126,7 @@ tools, getting a key, funding the embedded wallet, and idempotent retries, see
|
|
|
126
126
|
| `candle_execution_status` | One call before trading: wallets to spend from, tier, and whether trading is possible | Composes the wallet and tier reads | `CANDLE_AGENT_API_KEY` |
|
|
127
127
|
| `candle_get_operation` | Look up a trade or launch by the id its write used, and whether it landed | `GET /api/v1/trade/agent/jobs/:clientId` or `/api/v1/launch/headless/jobs/:clientId` | `CANDLE_AGENT_API_KEY` |
|
|
128
128
|
| `candle_get_fees` | The creator fees the account's launched tokens can pay: raw amounts from chain reads, and the reason a token cannot be claimed | `GET /api/v1/agent/fees` | `CANDLE_AGENT_API_KEY` (`account:read`) |
|
|
129
|
-
| `candle_claim_fees` | Collect one token's creator fees to the wallet fixed at launch (no destination argument); `dryRun` sends nothing
|
|
129
|
+
| `candle_claim_fees` | Collect one token's creator fees to the wallet fixed at launch (no destination argument); `dryRun` sends nothing | `POST /api/v1/agent/fees/:mint/claim` | `CANDLE_AGENT_API_KEY` (`launch:write`) |
|
|
130
130
|
| `candle_get_staking` | Read CNDL/sCNDL, withdrawals, rewards, pool wait and wallet eligibility notes | `GET /api/v1/agent/staking` |
|
|
131
131
|
| `candle_stake` | Stake raw CNDL; stable clientStakeId, dryRun first | `POST /api/v1/agent/staking/stake/build` |
|
|
132
132
|
| `candle_unstake` | Start a withdrawal of raw sCNDL; no instant unstake | `POST /api/v1/agent/staking/unstake/build` |
|
|
@@ -313,8 +313,9 @@ the section and its coverage fields. Agent boards are outside this slice.
|
|
|
313
313
|
|
|
314
314
|
Solana staking is available through the agent rail while the deployment enables it (off by
|
|
315
315
|
default). There is no instant unstake. Read the pool wait and wallet eligibility notes first:
|
|
316
|
-
pending withdrawal CNDL counts toward nothing for Pro;
|
|
317
|
-
|
|
316
|
+
pending withdrawal CNDL counts toward nothing for Pro; sCNDL in any eligible account wallet
|
|
317
|
+
counts toward Pro and earns the account's Hood reward, except a wallet another account also
|
|
318
|
+
claims, wallets past the 200-wallet cap, and an account with no EVM wallet.
|
|
318
319
|
|
|
319
320
|
Tools: `candle_get_staking`, `candle_stake`, `candle_unstake`, `candle_finish_unstake`,
|
|
320
321
|
`candle_claim_staking_reward`. Build tools accept stable `clientStakeId`, raw `amountRaw`
|
package/dist/index.js
CHANGED
|
@@ -57,6 +57,45 @@ function assertTransportSecurity(apiUrl, env) {
|
|
|
57
57
|
return;
|
|
58
58
|
throw new Error(`Refusing to send credentials in the clear to ${parsed.origin}. Set CANDLE_API_URL to an https:// ` + (isPrivateHost(parsed.hostname) ? "URL, or set CANDLE_ALLOW_INSECURE_HTTP=1 if this really is a trusted local endpoint." : "URL. CANDLE_ALLOW_INSECURE_HTTP does not apply here: it covers private networks only, " + "and this is a public address."));
|
|
59
59
|
}
|
|
60
|
+
var REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
61
|
+
var MAX_REDIRECTS = 5;
|
|
62
|
+
|
|
63
|
+
class CrossOriginRedirectError extends Error {
|
|
64
|
+
from;
|
|
65
|
+
to;
|
|
66
|
+
constructor(from, to) {
|
|
67
|
+
super(`The API at ${from} redirected to ${to}, another origin. Not followed, so the API key was not sent there.`);
|
|
68
|
+
this.from = from;
|
|
69
|
+
this.to = to;
|
|
70
|
+
this.name = "CrossOriginRedirectError";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function keyedRedirectGuard(doFetch) {
|
|
74
|
+
return async (url, init) => {
|
|
75
|
+
if (!init?.headers || !new Headers(init.headers).has("x-api-key"))
|
|
76
|
+
return doFetch(url, init);
|
|
77
|
+
let current = url;
|
|
78
|
+
let next = { ...init, redirect: "manual" };
|
|
79
|
+
for (let hops = 0;; hops++) {
|
|
80
|
+
const response = await doFetch(current, next);
|
|
81
|
+
const headers = response.headers;
|
|
82
|
+
const location = REDIRECT_STATUSES.has(response.status) ? headers?.get?.("location") : null;
|
|
83
|
+
if (!location)
|
|
84
|
+
return response;
|
|
85
|
+
const from = new URL(current);
|
|
86
|
+
const target = new URL(location, from);
|
|
87
|
+
if (target.origin !== from.origin)
|
|
88
|
+
throw new CrossOriginRedirectError(from.origin, target.origin);
|
|
89
|
+
if (hops >= MAX_REDIRECTS)
|
|
90
|
+
throw new Error(`More than ${MAX_REDIRECTS} redirects from ${url}`);
|
|
91
|
+
const method = (next.method ?? "GET").toUpperCase();
|
|
92
|
+
if (response.status === 303 && method !== "GET" && method !== "HEAD" || (response.status === 301 || response.status === 302) && method === "POST") {
|
|
93
|
+
next = { ...next, method: "GET", body: undefined };
|
|
94
|
+
}
|
|
95
|
+
current = target.href;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
60
99
|
function resolveConfig(env = process.env) {
|
|
61
100
|
const apiUrl = env.CANDLE_API_URL?.trim() || DEFAULT_API_URL;
|
|
62
101
|
assertTransportSecurity(apiUrl, env);
|
|
@@ -112,7 +151,7 @@ import { lstat, open, realpath } from "node:fs/promises";
|
|
|
112
151
|
import { dirname, isAbsolute, relative, sep } from "node:path";
|
|
113
152
|
|
|
114
153
|
// src/version.ts
|
|
115
|
-
var SERVER_VERSION = "0.10.
|
|
154
|
+
var SERVER_VERSION = "0.10.18";
|
|
116
155
|
|
|
117
156
|
// src/update-notice.ts
|
|
118
157
|
var PLAIN_VERSION = /^\d+\.\d+\.\d+$/;
|
|
@@ -796,6 +835,10 @@ var HYPERLIQUID_EXCHANGE_URLS = {
|
|
|
796
835
|
mainnet: "https://api.hyperliquid.xyz/exchange",
|
|
797
836
|
testnet: "https://api.hyperliquid-testnet.xyz/exchange"
|
|
798
837
|
};
|
|
838
|
+
var HYPERLIQUID_INFO_URLS = {
|
|
839
|
+
mainnet: "https://api.hyperliquid.xyz/info",
|
|
840
|
+
testnet: "https://api.hyperliquid-testnet.xyz/info"
|
|
841
|
+
};
|
|
799
842
|
var CANDLE_HYPERLIQUID_BUILDER_ADDRESS = null;
|
|
800
843
|
var HYPERLIQUID_MAX_BUILDER_FEE_TENTHS_BPS = 100;
|
|
801
844
|
var HYPERLIQUID_MAX_BUILDER_FEE_RATE = "0.1%";
|
|
@@ -1062,6 +1105,12 @@ function checkActionIntent(action, intent) {
|
|
|
1062
1105
|
const trigger = leg.t?.trigger;
|
|
1063
1106
|
if (leg.a !== first.a || leg.b !== !buy || leg.r !== true || decimal(leg.s) !== decimal(p.size) || trigger?.tpsl !== kind || trigger.isMarket !== true || decimal(trigger.triggerPx) !== decimal(p[field]))
|
|
1064
1107
|
return "trigger side, size, reduce-only flag or price does not match the request";
|
|
1108
|
+
const px = decimal(leg.p) ?? 0n;
|
|
1109
|
+
const triggerPx = decimal(trigger.triggerPx) ?? 0n;
|
|
1110
|
+
const bound = BigInt(HYPERLIQUID_TPSL_SLIPPAGE_BPS + HYPERLIQUID_MID_DRIFT_BPS);
|
|
1111
|
+
const within = leg.b === true ? px * 10000n <= triggerPx * (10000n + bound) : px * 10000n >= triggerPx * (10000n - bound);
|
|
1112
|
+
if (triggerPx <= 0n || !within)
|
|
1113
|
+
return "trigger order price is outside its slippage of the trigger price";
|
|
1065
1114
|
}
|
|
1066
1115
|
} else {
|
|
1067
1116
|
if (list.length !== 1)
|
|
@@ -1101,6 +1150,68 @@ async function hyperliquidCloseOrder(fetcher, network, address, params) {
|
|
|
1101
1150
|
const size = asked < held ? String(params.size) : String(szi).replace(/^-/, "");
|
|
1102
1151
|
return { asset, isBuy: signed < 0n, size };
|
|
1103
1152
|
}
|
|
1153
|
+
var HYPERLIQUID_DEFAULT_SLIPPAGE_BPS = 100;
|
|
1154
|
+
var HYPERLIQUID_MID_DRIFT_BPS = 100;
|
|
1155
|
+
var HYPERLIQUID_TPSL_SLIPPAGE_BPS = 500;
|
|
1156
|
+
async function hyperliquidOrderMarket(fetcher, network, address, method, params) {
|
|
1157
|
+
const read = async (body) => {
|
|
1158
|
+
const res = await fetcher(HYPERLIQUID_INFO_URLS[network], {
|
|
1159
|
+
method: "POST",
|
|
1160
|
+
headers: { "content-type": "application/json" },
|
|
1161
|
+
body: JSON.stringify(body)
|
|
1162
|
+
});
|
|
1163
|
+
if (!res.ok)
|
|
1164
|
+
throw new Error("Cannot read the market from Hyperliquid");
|
|
1165
|
+
return JSON.parse(await res.text());
|
|
1166
|
+
};
|
|
1167
|
+
let coin = String(params.coin ?? "").toUpperCase();
|
|
1168
|
+
let order;
|
|
1169
|
+
if (method === "modify") {
|
|
1170
|
+
const status = await read({ type: "orderStatus", user: address, oid: params.cloid });
|
|
1171
|
+
const resting = status?.order?.order ?? null;
|
|
1172
|
+
if (!resting || typeof resting.coin !== "string" || decimal(resting.limitPx) === null)
|
|
1173
|
+
throw new Error("No resting order with this cloid on Hyperliquid");
|
|
1174
|
+
coin = resting.coin.toUpperCase();
|
|
1175
|
+
order = { isBuy: resting.side === "B", limitPx: String(resting.limitPx) };
|
|
1176
|
+
}
|
|
1177
|
+
const [meta, contexts] = await read({ type: "metaAndAssetCtxs" });
|
|
1178
|
+
const asset = meta.universe.findIndex((m) => m.name.toUpperCase() === coin);
|
|
1179
|
+
if (asset < 0 || asset >= 1e4)
|
|
1180
|
+
throw new Error("No main-universe market for this coin on Hyperliquid");
|
|
1181
|
+
const context = contexts[asset];
|
|
1182
|
+
const midPx = context?.midPx ?? context?.markPx;
|
|
1183
|
+
if ((decimal(midPx) ?? 0n) <= 0n)
|
|
1184
|
+
throw new Error("No Hyperliquid price for this coin");
|
|
1185
|
+
return { asset, midPx: String(midPx), ...order ? { order } : {} };
|
|
1186
|
+
}
|
|
1187
|
+
function checkOrderMarket(action, intent, market) {
|
|
1188
|
+
const list = action.type === "modify" ? [action.order] : action.orders;
|
|
1189
|
+
if (!Array.isArray(list) || list.length === 0)
|
|
1190
|
+
return "missing requested order";
|
|
1191
|
+
if (list.some((o) => !o || o.a !== market.asset))
|
|
1192
|
+
return "order asset is not the requested coin's on Hyperliquid";
|
|
1193
|
+
const first = list[0];
|
|
1194
|
+
const p = intent.params;
|
|
1195
|
+
if (intent.method === "open" && p.price == null) {
|
|
1196
|
+
const requested = p.slippageBps;
|
|
1197
|
+
const slippage = typeof requested === "number" && Number.isInteger(requested) && requested > 0 ? requested : HYPERLIQUID_DEFAULT_SLIPPAGE_BPS;
|
|
1198
|
+
const bound = BigInt(slippage + HYPERLIQUID_MID_DRIFT_BPS);
|
|
1199
|
+
const px = decimal(first.p) ?? 0n;
|
|
1200
|
+
const mid = decimal(market.midPx) ?? 0n;
|
|
1201
|
+
const within = first.b === true ? px * 10000n <= mid * (10000n + bound) : px * 10000n >= mid * (10000n - bound);
|
|
1202
|
+
if (mid <= 0n || !within)
|
|
1203
|
+
return "market order price is outside its slippage of Hyperliquid's mid";
|
|
1204
|
+
}
|
|
1205
|
+
if (intent.method === "modify") {
|
|
1206
|
+
if (!market.order)
|
|
1207
|
+
return "no resting order on Hyperliquid to check the modify against";
|
|
1208
|
+
if (first.b !== market.order.isBuy)
|
|
1209
|
+
return "modify changes the resting order's side";
|
|
1210
|
+
if (p.price == null && decimal(first.p) !== decimal(market.order.limitPx))
|
|
1211
|
+
return "modify changes the resting order's price";
|
|
1212
|
+
}
|
|
1213
|
+
return null;
|
|
1214
|
+
}
|
|
1104
1215
|
function verifyPerpsBuild(build, opts) {
|
|
1105
1216
|
try {
|
|
1106
1217
|
const network = opts.network ?? "mainnet";
|
|
@@ -1128,6 +1239,13 @@ function verifyPerpsBuild(build, opts) {
|
|
|
1128
1239
|
if (!order || order.a !== opts.closeOrder.asset || order.b !== opts.closeOrder.isBuy || decimal(order.s) !== decimal(opts.closeOrder.size) || order.r !== true)
|
|
1129
1240
|
return { ok: false, reason: "close side, asset or size does not match the requested position" };
|
|
1130
1241
|
}
|
|
1242
|
+
if (opts.market) {
|
|
1243
|
+
if (opts.intent?.method !== "open" && opts.intent?.method !== "modify")
|
|
1244
|
+
return { ok: false, reason: "a market check needs an open or modify intent" };
|
|
1245
|
+
const marketRefusal = checkOrderMarket(action, opts.intent, opts.market);
|
|
1246
|
+
if (marketRefusal)
|
|
1247
|
+
return { ok: false, reason: marketRefusal };
|
|
1248
|
+
}
|
|
1131
1249
|
if (typed.primary_type === "Agent") {
|
|
1132
1250
|
const type = action.type;
|
|
1133
1251
|
if (typeof type !== "string" || !HYPERLIQUID_ALLOWED_ACTION_TYPES.includes(type)) {
|
|
@@ -1345,6 +1463,18 @@ async function executePerps(tool, args, cfg, env, fetch2) {
|
|
|
1345
1463
|
if (!closeCheck.ok)
|
|
1346
1464
|
return fail("PERPS_BUILD_REFUSED", `Refused to sign this build: ${closeCheck.reason}. Nothing was signed.`);
|
|
1347
1465
|
}
|
|
1466
|
+
if (method === "open") {
|
|
1467
|
+
const market = await hyperliquidOrderMarket(fetch2, network, row.address, method, fields);
|
|
1468
|
+
const marketCheck = verifyPerpsBuild(build, {
|
|
1469
|
+
builder,
|
|
1470
|
+
network,
|
|
1471
|
+
address: row.address,
|
|
1472
|
+
intent: { method, params: fields },
|
|
1473
|
+
market
|
|
1474
|
+
});
|
|
1475
|
+
if (!marketCheck.ok)
|
|
1476
|
+
return fail("PERPS_BUILD_REFUSED", `Refused to sign this build: ${marketCheck.reason}. Nothing was signed.`);
|
|
1477
|
+
}
|
|
1348
1478
|
if (!willSubmit || signerPem === null)
|
|
1349
1479
|
return { text: JSON.stringify({ ...build, signed: false, submitted: false }) };
|
|
1350
1480
|
if (!appId || !row.privyWalletId)
|
|
@@ -1591,7 +1721,7 @@ var PLAN_CAPABILITY_LABELS = {
|
|
|
1591
1721
|
createLinkedWallets: "Create linked wallets",
|
|
1592
1722
|
importLinkedWallets: "Import linked wallets",
|
|
1593
1723
|
limitOrders: "Limit orders",
|
|
1594
|
-
quant: "Quant
|
|
1724
|
+
quant: "Quant deep analyses (Max)"
|
|
1595
1725
|
};
|
|
1596
1726
|
var PLAN_LABELS = { free: "Free", believer: "Believer", pro: "Pro", max: "Max" };
|
|
1597
1727
|
function planLabel(plan) {
|
|
@@ -1653,9 +1783,9 @@ function planTableMarkdown(table) {
|
|
|
1653
1783
|
|
|
1654
1784
|
// src/tools.ts
|
|
1655
1785
|
var VAMP_CANDIDATES_DESCRIPTION = "Lists Solana tokens that are close to graduating or graduated in the last 6 hours whose ticker other tokens " + "are copying, most-copied in the last 24 hours first. Each entry carries a launch draft (name, ticker, image URL) " + "for `candle_launch_token` or `candle_launch_and_seed`; draft notes identify fields you must supply or adjust. " + "Upload your own image through the agent image upload when the image is unavailable. Use `dryRun: true` first. " + "Counts exclude the candidate itself and cover both Solana and Hood launches at the venues returned in " + "`coverage`, not Candle's own. `coverage.since` is the returned Solana coverage date; Hood history may differ.";
|
|
1656
|
-
var CUSTOM_CURVE_RULE = 'mode "exp-custom" with curve { virtualQuote, threshold } launches a Solana curve with your own V (the virtual ' + "quote it starts with) and G (the quote it raises before graduating), in whole SOL or CNDL as decimal strings, " + "accepted when V is 1 to 100 SOL and G 1 to 250 SOL (66,000 to 6,600,000 and 66,000 to 16,500,000 CNDL), when " + "every price up to 20 x G stays under the ceiling (V + 20G)^2 < V x X at this stakerAllocationBps, when neither " + "is finer than the quote's decimals, and when the dev buy is at most 10% of G; a curve past a limit is " + "CURVE_OUT_OF_RANGE, and at the ceiling it carries maxThresholdRaw, the largest G that fits. " + "Dry-run it first (dryRun: true): the dry run spends nothing and returns curveTerms, the market cap at start and " + "at graduation and the supply sold by graduation. Only where the API has
|
|
1786
|
+
var CUSTOM_CURVE_RULE = 'mode "exp-custom" with curve { virtualQuote, threshold } launches a Solana curve with your own V (the virtual ' + "quote it starts with) and G (the quote it raises before graduating), in whole SOL or CNDL as decimal strings, " + "accepted when V is 1 to 100 SOL and G 1 to 250 SOL (66,000 to 6,600,000 and 66,000 to 16,500,000 CNDL), when " + "every price up to 20 x G stays under the ceiling (V + 20G)^2 < V x X at this stakerAllocationBps, when neither " + "is finer than the quote's decimals, and when the dev buy is at most 10% of G; a curve past a limit is " + "CURVE_OUT_OF_RANGE, and at the ceiling it carries maxThresholdRaw, the largest G that fits. " + "Dry-run it first (dryRun: true): the dry run spends nothing and returns curveTerms, the market cap at start and " + "at graduation and the supply sold by graduation. Only where the API has EXP_CUSTOM_CURVES on; " + "elsewhere exp-custom is refused as an unknown mode.";
|
|
1657
1787
|
var localProfileTools = new WeakMap;
|
|
1658
|
-
var STAKING_DISCLOSURE = "No instant unstake. Read pool.withdrawalTimelockSeconds for the wait. Solana rewards accrue to the holding wallet.
|
|
1788
|
+
var STAKING_DISCLOSURE = "No instant unstake. Read pool.withdrawalTimelockSeconds for the wait. Solana rewards accrue to the holding wallet. sCNDL in any eligible account wallet counts toward Pro and earns the account's Hood staking reward, except a wallet another account also claims, wallets past the 200-wallet cap, and an account with no EVM wallet. CNDL in a pending withdrawal counts toward nothing for Pro.";
|
|
1659
1789
|
var stakingWriteShape = {
|
|
1660
1790
|
payer: z2.enum(["main", "linked"]).optional().describe("main is the embedded wallet; linked names a linked or TEE wallet"),
|
|
1661
1791
|
linkedWalletId: z2.string().min(1).optional(),
|
|
@@ -2234,7 +2364,7 @@ function registerToolSubset(server, options) {
|
|
|
2234
2364
|
const allowed = new Set(options.tools);
|
|
2235
2365
|
const getConfig = options.getConfig;
|
|
2236
2366
|
const env = options.env ?? {};
|
|
2237
|
-
const doFetch = options.fetch ?? ((url, init) => fetch(url, init));
|
|
2367
|
+
const doFetch = keyedRedirectGuard(options.fetch ?? ((url, init) => fetch(url, init)));
|
|
2238
2368
|
const register = (name, definition, handler) => {
|
|
2239
2369
|
if (!allowed.has(name))
|
|
2240
2370
|
return;
|
|
@@ -2395,7 +2525,7 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
|
|
|
2395
2525
|
}, async (args) => callAndRelay("candle_get_fees", args, getConfig(), doFetch));
|
|
2396
2526
|
register("candle_claim_fees", {
|
|
2397
2527
|
title: "Claim a token's creator fees",
|
|
2398
|
-
description: "Collect one token's creator fees and pay them to the wallet fixed at launch. There is no destination " + "argument and the tool cannot send funds anywhere else. Candle signs with the embedded wallet, which " + "pays the network fee. Needs launch:write
|
|
2528
|
+
description: "Collect one token's creator fees and pay them to the wallet fixed at launch. There is no destination " + "argument and the tool cannot send funds anywhere else. Candle signs with the embedded wallet, which " + "pays the network fee. Needs launch:write. " + "Call it with dryRun: true first; a real claim moves real funds into the recipient wallet, which may be " + "the wallet this agent trades from. Not idempotent by id: if the result is lost, read candle_get_fees " + "before claiming again.",
|
|
2399
2529
|
inputSchema: claimFeesShape
|
|
2400
2530
|
}, async (args) => callAndRelay("candle_claim_fees", args, getConfig(), doFetch));
|
|
2401
2531
|
register("candle_get_wallets", {
|
|
@@ -2561,7 +2691,7 @@ async function registerEligibleProfileTool(server, env = process.env, doFetch =
|
|
|
2561
2691
|
let eligible = false;
|
|
2562
2692
|
if (cfg.apiKey && wanted.length > 0) {
|
|
2563
2693
|
try {
|
|
2564
|
-
const res = await doFetch(`${cfg.apiUrl}/api/v1/agent/keys/self/public-profile`, {
|
|
2694
|
+
const res = await keyedRedirectGuard(doFetch)(`${cfg.apiUrl}/api/v1/agent/keys/self/public-profile`, {
|
|
2565
2695
|
method: "GET",
|
|
2566
2696
|
headers: { "x-api-key": cfg.apiKey },
|
|
2567
2697
|
signal: AbortSignal.timeout(15000)
|