@candledottv/mcp 0.10.5 → 0.10.7

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 (3) hide show
  1. package/README.md +53 -19
  2. package/dist/index.js +308 -10
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,17 +1,17 @@
1
1
  # @candledottv/mcp
2
2
 
3
3
  An MCP (Model Context Protocol) server for the Candle agent rail. It exposes Candle's REST API as
4
- twenty-seven tools over stdio, so an MCP-capable agent can launch tokens (optionally seeded with a dev
4
+ twenty-nine tools over stdio, so an MCP-capable agent can launch tokens (optionally seeded with a dev
5
5
  buy in the same call), trade, convert between base assets (including across chains), read market
6
6
  and feed data, report on-chain activity, and check an agent profile without hand-rolling HTTP
7
7
  calls.
8
8
 
9
9
  ## Try it without a key
10
10
 
11
- Five tools are read-only and need nothing but `CANDLE_API_URL` (which already defaults to
11
+ Six tools are read-only and need nothing but `CANDLE_API_URL` (which already defaults to
12
12
  production): `candle_get_market`, `candle_get_feed`, `candle_token_forensics`,
13
- `candle_get_agent_profile`, and `candle_resolve_token`. Install the
14
- server with no `env` block at all and those five work immediately:
13
+ `candle_get_agent_profile`, `candle_resolve_token` and `candle_get_plans`. Install the
14
+ server with no `env` block at all and those six work immediately:
15
15
 
16
16
  ```json
17
17
  {
@@ -31,11 +31,11 @@ Environment below -- or skip env editing entirely: install the Candle CLI
31
31
  environment.
32
32
 
33
33
  `CANDLE_MCP_TOOLS` (optional) is a comma-separated allowlist of tool names; only those register.
34
- Unset means all twenty-seven. An unknown name fails startup with the valid names in the message, rather
34
+ Unset means all twenty-nine. An unknown name fails startup with the valid names in the message, rather
35
35
  than silently registering the wrong surface. `candle mcp --read-only` / `--tools` set this for
36
36
  you.
37
37
 
38
- Most tools are a thin wrapper: a one-request mapping onto `apps/api`
38
+ Most tools are a thin wrapper: a one-request mapping onto Candle's REST API
39
39
  (`POST /api/v1/launch/headless`, `GET /api/v1/markets/:chain/:mint`, `GET /api/v1/markets/feed`,
40
40
  `POST /api/v1/activity/report`, `GET /api/v1/users/:idOrWallet/agent`) that hands the response body
41
41
  straight back to the caller, unchanged, error responses included. `candle_trade` and
@@ -48,7 +48,8 @@ Both take decimal amounts, never raw base units, and resolve the scale themselve
48
48
  - A `candle_trade` **buy** is denominated in the token's OWN quote asset, whatever it was launched
49
49
  against (SOL for a SOL-launched token, USDC or CNDL for those quote pairs). The tool reads the
50
50
  market first and converts against its `quoteDecimals`; the `quoteAsset` field applies only to a
51
- mint Candle never launched, the arbitrary-token path a Pro or Max key trades through Jupiter. A
51
+ mint Candle never launched, the path traded through Jupiter (buying needs Pro or Max; selling
52
+ works on any plan). A
52
53
  **sell** is denominated in tokens and converts against the market's own `decimals`.
53
54
  - A `candle_launch_and_seed` `devBuy` is denominated in the quote asset that launch selects, since
54
55
  there the caller genuinely picks the new token's quote pair: `quoteAsset` if given, otherwise
@@ -56,11 +57,11 @@ Both take decimal amounts, never raw base units, and resolve the scale themselve
56
57
 
57
58
  For a full walkthrough of `candle_launch_and_seed` and `candle_trade`, including the keyless read
58
59
  tools, getting a key, funding the embedded wallet, and idempotent retries, see
59
- `docs/mcp-launch-and-seed.md` in the `candle-monorepo` repo.
60
+ [Candle MCP server](https://docs.candle.tv/developers/mcp-server).
60
61
 
61
62
  ## Environment
62
63
 
63
- - `CANDLE_API_URL` -- base URL of the Candle API. Defaults to `https://api.alpha.candle.tv` (the alpha deployment; production does not serve the agent API yet). Set it to
64
+ - `CANDLE_API_URL` -- base URL of the Candle API. Defaults to `https://api.alpha.candle.tv`, production. Set it to
64
65
  `http://localhost:3001` when developing against a local API. A cleartext `http://` URL pointing
65
66
  at a NON-loopback host is refused at startup, since every write tool sends `x-api-key`; loopback
66
67
  (`localhost`, `127.0.0.0/8`, `::1`) needs no opt-in.
@@ -68,13 +69,13 @@ tools, getting a key, funding the embedded wallet, and idempotent retries, see
68
69
  non-loopback host. For a trusted local endpoint that is not loopback, such as a devcontainer
69
70
  reaching its host; not for anything that leaves the machine.
70
71
  - `CANDLE_AGENT_API_KEY` -- an agent API key (`cndl_live_...` / `cndl_test_...`), issued from a
71
- Candle account's agent settings page. Required by every tool except the five read-only ones
72
+ Candle account's agent settings page. Required by every tool except the six read-only ones
72
73
  listed above, which includes the account-scoped reads (`candle_get_wallets`,
73
- `candle_execution_status`, `candle_get_operation`) as well as the writes; those five
74
+ `candle_execution_status`, `candle_get_operation`) as well as the writes; those six
74
75
  work without it, so the server is useful the moment it is installed and only asks for a key when
75
76
  you try to write. `candle_trade` additionally needs the key's `swap:write` scope server-side,
76
- which is opt-in only and never granted by omission, see `docs/mcp-launch-and-seed.md` in the
77
- `candle-monorepo` repo.
77
+ which is opt-in only and never granted by omission, see
78
+ [Agent access](https://docs.candle.tv/developers/agent-access).
78
79
  - `CANDLE_KEY_SIGNER_PEM_FILE` -- the perps write tools only: the path to the key signer's PEM
79
80
  (`candle tee signer new --out <pem>` writes one). The relay's authorization is signed with it;
80
81
  without it those tools refuse before anything is built.
@@ -93,6 +94,7 @@ tools, getting a key, funding the embedded wallet, and idempotent retries, see
93
94
  | `candle_token_forensics` | Deployer history, deploy-window buyers, sourced token safety flags | `GET /api/v1/markets/:chain/:mint/forensics` | none |
94
95
  | `candle_report_activity` | Report on-chain activity | `POST /api/v1/activity/report` | `CANDLE_AGENT_API_KEY` |
95
96
  | `candle_get_agent_profile` | Get an agent profile | `GET /api/v1/users/:idOrWallet/agent` | none |
97
+ | `candle_get_plans` | Plans: prices, fees, limits and what each can do, as JSON and a Markdown table | `GET /api/v1/agent/plans` | none |
96
98
  | `candle_trade` | Buy or sell a token | Reads the market for its decimals (or wallet balance, for a percent sell) then `POST /api/v1/trade/agent/build` | `CANDLE_AGENT_API_KEY` (`swap:write`) |
97
99
  | `candle_launch_and_seed` | Launch a token and seed it | `POST /api/v1/launch/headless` (or `/dry-run`), then a follow-up `GET /api/v1/markets/:chain/:mint` | `CANDLE_AGENT_API_KEY` |
98
100
  | `candle_swap` | Swap between base assets | `POST /api/v1/agent/swap` | `CANDLE_AGENT_API_KEY` (`swap:write`) |
@@ -102,7 +104,7 @@ tools, getting a key, funding the embedded wallet, and idempotent retries, see
102
104
  | `candle_get_wallets` | The account's embedded wallets, one per chain, with delegation state | `GET /api/v1/agent/wallets/embedded` | `CANDLE_AGENT_API_KEY` |
103
105
  | `candle_get_profile_wallets` | Which wallets an agent profile may spend from, and whether it is scoped | `GET /api/v1/agent/keys/:prefix/wallets` | `CANDLE_AGENT_API_KEY` |
104
106
  | `candle_set_profile_wallets` | Replace the wallets an agent profile may spend from | `PUT /api/v1/agent/keys/:prefix/wallets` | `CANDLE_AGENT_API_KEY` |
105
- | `candle_get_profile_pnl` | An agent profile's realized P&L, fees, and open positions at cost basis, marked at current prices (unpriced positions counted, not zeroed), split by the wallet that paid (`pnl.byWallet`) and by chain (`pnl.byChain`), with `chain` on every position and wallet row | `GET /api/v1/agent/keys/:prefix/pnl` | `CANDLE_AGENT_API_KEY` |
107
+ | `candle_get_profile_pnl` | An agent profile's share of the account's P&L, the console's figures to the cent: realized on its sales, fees, and the open positions that belong to it at cost basis, marked at current prices (unpriced positions counted, not zeroed), with `totalUsd`, the closed positions as made, lost and partial sells (`closed`), dust flagged and counted apart (`openPositionsExDust`), split by wallet (`pnl.byWallet`, summing to the total) and by chain (`pnl.byChain`), with `chain` and `wallet` on every position | `GET /api/v1/agent/keys/:prefix/pnl` | `CANDLE_AGENT_API_KEY` |
106
108
  | `candle_get_profile_trades` | An agent profile's orders, fills, fees and transaction hashes | `GET /api/v1/agent/keys/:prefix/trades` | `CANDLE_AGENT_API_KEY` |
107
109
  | `candle_get_portfolio` | What the account's embedded and TEE wallets hold on Solana and Hood (Hood in `hood`), with prices; unpriced holdings say why, never zero | `GET /api/v1/agent/portfolio` | `CANDLE_AGENT_API_KEY` (`account:read`) |
108
110
  | `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` |
@@ -112,6 +114,7 @@ tools, getting a key, funding the embedded wallet, and idempotent retries, see
112
114
  | `candle_perps_close` | Close all or part of a perps position, reduce-only | `POST /api/v1/agent/perps/close`, the same | `CANDLE_AGENT_API_KEY` (`perps:write`), `CANDLE_KEY_SIGNER_PEM_FILE` |
113
115
  | `candle_perps_cancel` | Cancel a perps order Candle built, by cloid | `POST /api/v1/agent/perps/cancel`, the same | `CANDLE_AGENT_API_KEY` (`perps:write`), `CANDLE_KEY_SIGNER_PEM_FILE` |
114
116
  | `candle_perps_leverage` | Set a market's leverage and margin mode | `POST /api/v1/agent/perps/leverage`, the same | `CANDLE_AGENT_API_KEY` (`perps:write`), `CANDLE_KEY_SIGNER_PEM_FILE` |
117
+ | `candle_perps_deposit` | Deposit onto the key's Hyperliquid account through Relay, from its Solana or Hood TEE wallet | `POST /api/v1/agent/perps/deposit`, checked here, relay-signed, `POST /api/v1/agent/perps/deposit/submit` | `CANDLE_AGENT_API_KEY` (`swap:write`), `CANDLE_KEY_SIGNER_PEM_FILE` |
115
118
  | `candle_perps_orders` | Open Hyperliquid orders and every action Candle built | `GET /api/v1/agent/perps/orders` | `CANDLE_AGENT_API_KEY` |
116
119
  | `candle_perps_positions` | Perps positions and account value, read live | `GET /api/v1/agent/perps/positions` | `CANDLE_AGENT_API_KEY` |
117
120
 
@@ -130,13 +133,13 @@ explicitly. Assets with nothing spendable report `empty`; a failed asset never s
130
133
  ## Errors
131
134
 
132
135
  This package never reinterprets an error body, and that body is not one uniform shape across all
133
- twenty-seven tools:
136
+ twenty-nine tools:
134
137
 
135
138
  - `candle_launch_token`, `candle_get_market`, and `candle_get_feed` hit endpoints that use the
136
139
  structured envelope `{ success: false, error: { code, message, ... } }`. Branch on `error.code`.
137
- - `candle_report_activity` relays `apps/api/src/routes/activity.ts`'s own plain error shape
140
+ - `candle_report_activity` relays the activity endpoint's own plain error shape
138
141
  verbatim: `{ error: true, payload: string }`.
139
- - `candle_get_agent_profile` relays `apps/api/src/routes/users.ts`'s own plain error shape
142
+ - `candle_get_agent_profile` relays the users endpoint's own plain error shape
140
143
  verbatim: `{ error: string }`.
141
144
  - `candle_trade` and `candle_launch_and_seed` wrap the underlying REST body instead of relaying it
142
145
  bare, in one of two shapes depending on how far the call got:
@@ -165,8 +168,9 @@ twenty-seven tools:
165
168
  `candle_get_market` to fetch it separately.
166
169
  - Either way, both tools echo their idempotency id (`clientTradeId` / `clientLaunchId`) at the
167
170
  top level, so a caller can always find it to retry safely: retrying with the SAME id is a safe
168
- replay, a new id is a second trade or launch. See `docs/mcp-launch-and-seed.md` in the
169
- `candle-monorepo` repo for the full idempotent-retry rule.
171
+ replay, a new id is a second trade or launch. See
172
+ [Candle MCP server](https://docs.candle.tv/developers/mcp-server) for the full idempotent-retry
173
+ rule.
170
174
 
171
175
  ## MCP client config
172
176
 
@@ -221,3 +225,33 @@ this on machines that have node and may not have bun.
221
225
  calls with multi-field zod input shapes coexist in one file, which this package's tools
222
226
  always will. `1.22.0` predates that rewrite and type-checks cleanly with the exact same tool
223
227
  code. Re-check this pin when bumping the SDK.
228
+
229
+ ### Hyperliquid per-key PnL
230
+
231
+ `candle pnl --profile <name>` / SDK `getProfilePnl(keyPrefix)` / MCP `candle_get_profile_pnl`
232
+ read the existing `GET /api/v1/agent/keys/{prefix}/pnl`. The answer adds an optional
233
+ `hyperliquid` section, and the CLI prints it under **Hyperliquid**. The existing read
234
+ authentication applies; on an older API the section is simply absent. No additional signing, deposit or setup is needed for this read.
235
+
236
+ The section reports main-exchange perp `realizedGrossUsd` (venue `closedPnl`), signed
237
+ `fundingUsd` (received positive, paid negative), `feesUsd` (venue fees including builder fees;
238
+ rebates are negative), and `realizedNetUsd = realizedGrossUsd + fundingUsd - feesUsd`.
239
+ Builder fees are already included in the venue fee and are not subtracted again. These figures
240
+ stay separate from spot and LP totals and exclude unrealized perps, deposits and withdrawals.
241
+
242
+ Coverage is **currently bound EVM TEE wallets since the current TEE binding**,
243
+ shown in `byWallet` with `startTime` in epoch milliseconds. That start is the latest rebind
244
+ onto this key, or the wallet's creation time when it was bound then and never rebound. A
245
+ rebound wallet does not bring its previous key's history. A wallet-set row is not required,
246
+ and an older one cannot move the start earlier than the binding. Revoked and previous
247
+ bindings, spot and HIP-3 fills are excluded. This is a binding-period read, not lifetime or
248
+ order-level attribution.
249
+
250
+ Reads use the venue client's two-second cache. `userFills` is capped at 2,000 recent rows;
251
+ `userFunding` at 500 rows from `startTime`, and this response covers at most 20 bound
252
+ wallets. `truncated` flags any reached history cap or remaining wallet page; `byWallet` names
253
+ `fillsTruncated` and `fundingTruncated`. Funding and fills can cover different time spans when
254
+ capped: a partial net must not be quoted as a lifetime figure. On upstream, wallet-discovery or
255
+ malformed-data failure the section is `{ read: false, reason }`, without amounts; spot PnL still
256
+ answers. An empty successful read is `{ read: true, ... }` with zero amounts. `--json` preserves
257
+ the section and its coverage fields. Agent boards are outside this slice.
package/dist/index.js CHANGED
@@ -106,7 +106,7 @@ function percentOfBalance(balanceRaw, percent) {
106
106
  import { randomUUID } from "node:crypto";
107
107
 
108
108
  // src/version.ts
109
- var SERVER_VERSION = "0.10.5";
109
+ var SERVER_VERSION = "0.10.7";
110
110
 
111
111
  // src/update-notice.ts
112
112
  var PLAIN_VERSION = /^\d+\.\d+\.\d+$/;
@@ -1175,6 +1175,254 @@ async function executePerps(tool, args, cfg, env, fetch2) {
1175
1175
  return fail("MCP_TRANSPORT", error instanceof Error ? error.message : "The perps call failed.");
1176
1176
  }
1177
1177
  }
1178
+ var HYPERLIQUID_RELAY_CHAIN_ID = 1337;
1179
+ var HYPERLIQUID_RELAY_USDC = "0x00000000000000000000000000000000";
1180
+ var DEPOSIT_ASSETS = {
1181
+ SOL: { chain: "solana", decimals: 9 },
1182
+ USDC: { chain: "solana", decimals: 6 },
1183
+ ETH: { chain: "evm", decimals: 18 },
1184
+ USDG: { chain: "evm", decimals: 6 }
1185
+ };
1186
+ var perpsDepositShape = {
1187
+ asset: z.enum(["SOL", "USDC", "ETH", "USDG"]).describe("What to deposit. SOL or USDC pay from the key's Solana TEE wallet; ETH or USDG from its Hood one."),
1188
+ amount: decimal2.describe('Amount of the asset, decimal (e.g. "25")'),
1189
+ wallet: z.string().optional().describe("The paying TEE wallet by id, address or label. Omit when the key has exactly one on that chain."),
1190
+ perpsWallet: z.string().optional().describe("From Solana only: the EVM TEE wallet whose Hyperliquid account receives it. Omit when the key has one."),
1191
+ clientDepositId: z.string().regex(/^[A-Za-z0-9._:-]{1,128}$/).optional().describe("Idempotency id; the same id answers the deposit it already built. Default: a new one."),
1192
+ maxSlippageBps: z.number().int().min(1).max(1000).optional().describe("Relay slippage (default 100)"),
1193
+ submit
1194
+ };
1195
+ function rawUnits(amount, decimals) {
1196
+ const [whole = "", fraction = ""] = amount.split(".");
1197
+ if (fraction.length > decimals)
1198
+ return null;
1199
+ const raw = BigInt(whole + fraction.padEnd(decimals, "0"));
1200
+ return raw > 0n ? raw.toString() : null;
1201
+ }
1202
+ function pickWallet(rows, chain, name, what) {
1203
+ const onChain = rows.filter((row) => row.chain === chain);
1204
+ const matches = name === undefined ? onChain : onChain.filter((row) => row.id === name || row.label === name || row.address.toLowerCase() === name.toLowerCase());
1205
+ if (matches.length !== 1)
1206
+ throw new Refusal(fail("TEE_WALLET_REQUIRED", onChain.length === 0 ? `This key has no ${what} bound to it.` : `Name exactly one ${what}: ${onChain.map((row) => row.label ?? row.id).join(", ")}`));
1207
+ return matches[0];
1208
+ }
1209
+ function depositProblem(build, expect) {
1210
+ const destination = build.destination ?? {};
1211
+ const fee = build.fee ?? {};
1212
+ const same = (a, b) => typeof a === "string" && a.toLowerCase() === b.toLowerCase();
1213
+ if (build.walletId !== expect.walletId)
1214
+ return "the build names another paying wallet";
1215
+ if (build.asset !== expect.asset || build.amountRaw !== expect.amountRaw)
1216
+ return "the build is for another amount";
1217
+ if (destination.chainId !== HYPERLIQUID_RELAY_CHAIN_ID)
1218
+ return "the build does not land on Hyperliquid";
1219
+ if (destination.currency !== HYPERLIQUID_RELAY_USDC)
1220
+ return "the build does not deliver Hyperliquid USDC";
1221
+ if (!same(destination.address, expect.account))
1222
+ return "the build lands on another account";
1223
+ if (fee.bps !== 0 || fee.feeRaw !== "0")
1224
+ return "the build carries a Candle fee";
1225
+ if (expect.hood !== (build.chain === "hood"))
1226
+ return "the build pays from the wrong chain";
1227
+ if (!expect.hood && !(Array.isArray(build.transactionsBase64) && build.transactionsBase64.length === 1))
1228
+ return "the build is not one Solana deposit";
1229
+ return null;
1230
+ }
1231
+ async function executePerpsDeposit(args, cfg, env, fetch2) {
1232
+ if (!cfg.apiKey)
1233
+ return fail("MCP_VALIDATION", "CANDLE_AGENT_API_KEY is required for this tool.");
1234
+ const asset = args.asset;
1235
+ const spec = DEPOSIT_ASSETS[asset];
1236
+ if (!spec)
1237
+ return fail("MCP_VALIDATION", "asset must be SOL, USDC, ETH or USDG.");
1238
+ const amountRaw = typeof args.amount === "string" ? rawUnits(args.amount, spec.decimals) : null;
1239
+ if (!amountRaw)
1240
+ return fail("MCP_VALIDATION", `amount must be a positive ${asset} amount with at most ${spec.decimals} decimals.`);
1241
+ const hood = spec.chain === "evm";
1242
+ const clientDepositId = typeof args.clientDepositId === "string" ? args.clientDepositId : `deposit-${crypto.randomUUID()}`;
1243
+ const willSubmit = args.submit !== false;
1244
+ let signerPem = null;
1245
+ if (willSubmit) {
1246
+ const pemFile = env.CANDLE_KEY_SIGNER_PEM_FILE?.trim();
1247
+ if (!pemFile)
1248
+ return fail("SIGNER_UNAVAILABLE", "Set CANDLE_KEY_SIGNER_PEM_FILE to the key signer PEM (candle tee signer new --out <pem>) to sign a deposit.");
1249
+ try {
1250
+ signerPem = await readFile(pemFile, "utf8");
1251
+ createPrivateKey(signerPem);
1252
+ } catch {
1253
+ return fail("SIGNER_UNAVAILABLE", "CANDLE_KEY_SIGNER_PEM_FILE does not hold a readable private key PEM.");
1254
+ }
1255
+ }
1256
+ try {
1257
+ const listed = await call(cfg, fetch2, "GET", "/api/v1/agent/wallets/trading");
1258
+ if (!(Array.isArray(listed.scopes) && listed.scopes.includes("swap:write")))
1259
+ throw new Refusal(fail("SCOPE_MISSING", "A deposit needs swap:write on the key."));
1260
+ const rows = Array.isArray(listed.page) ? listed.page : [];
1261
+ const appId = typeof listed.privyAppId === "string" ? listed.privyAppId : "";
1262
+ const name = typeof args.wallet === "string" ? args.wallet : undefined;
1263
+ const source = pickWallet(rows, spec.chain, name, `${hood ? "Hood" : "Solana"} TEE wallet`);
1264
+ if (hood && args.perpsWallet !== undefined)
1265
+ return fail("MCP_VALIDATION", "From a Hood TEE wallet the deposit lands on that wallet's own account; omit perpsWallet.");
1266
+ const account = hood ? source : pickWallet(rows, "evm", typeof args.perpsWallet === "string" ? args.perpsWallet : undefined, "EVM TEE wallet");
1267
+ const build = await call(cfg, fetch2, "POST", "/api/v1/agent/perps/deposit", {
1268
+ clientDepositId,
1269
+ walletId: source.id,
1270
+ asset,
1271
+ amountRaw,
1272
+ ...typeof args.maxSlippageBps === "number" ? { maxSlippageBps: args.maxSlippageBps } : {},
1273
+ ...hood ? {} : { perpsWalletId: account.id }
1274
+ });
1275
+ if (build.job !== undefined)
1276
+ return { text: JSON.stringify(build) };
1277
+ const problem = depositProblem(build, { walletId: source.id, asset, amountRaw, account: account.address, hood });
1278
+ if (problem)
1279
+ return fail("PERPS_BUILD_REFUSED", `Refused to sign this deposit: ${problem}. Nothing was signed.`);
1280
+ if (!willSubmit || signerPem === null)
1281
+ return { text: JSON.stringify({ ...build, signed: false, submitted: false }) };
1282
+ if (!appId || !source.privyWalletId)
1283
+ return fail("SIGNER_UNAVAILABLE", "The server did not return its relay identifiers.");
1284
+ const pem = signerPem;
1285
+ const relaySign = async (body2) => {
1286
+ const payload = hyperliquidCanonicalJson({
1287
+ body: body2,
1288
+ headers: { "privy-app-id": appId },
1289
+ method: "POST",
1290
+ url: `https://api.privy.io/v1/wallets/${source.privyWalletId}/rpc`,
1291
+ version: 1
1292
+ });
1293
+ const authorizationSignature = sign("sha256", Buffer.from(payload), pem).toString("base64");
1294
+ const relay = await call(cfg, fetch2, "POST", `/api/v1/agent/wallets/${encodeURIComponent(source.id)}/sign`, {
1295
+ authorizationSignature,
1296
+ body: body2
1297
+ });
1298
+ if (typeof relay.signedTransaction !== "string")
1299
+ throw new Refusal(fail("INVALID_RESPONSE", "The relay did not return a signed transaction."));
1300
+ return relay.signedTransaction;
1301
+ };
1302
+ const ids = { clientDepositId, depositId: build.depositId };
1303
+ const submitPath = "/api/v1/agent/perps/deposit/submit";
1304
+ if (!hood) {
1305
+ const transaction = build.transactionsBase64[0];
1306
+ const signed = await relaySign({ method: "signTransaction", params: { encoding: "base64", transaction } });
1307
+ return {
1308
+ text: JSON.stringify(await call(cfg, fetch2, "POST", submitPath, { ...ids, signedTransactionsBase64: [signed] }))
1309
+ };
1310
+ }
1311
+ const allowed = asset === "USDG" ? ["approval", "bridgeDeposit"] : ["bridgeDeposit"];
1312
+ const planned = build.plannedLegCount;
1313
+ const hex = (value) => `0x${BigInt(String(value)).toString(16)}`;
1314
+ const signedKinds = new Set;
1315
+ let body = build;
1316
+ while (body.mode === "sequenced") {
1317
+ const leg = body.nextLeg;
1318
+ const kind = String(body.legKind);
1319
+ if (!leg || body.operationId !== build.operationId || body.plannedLegCount !== planned || typeof planned !== "number" || planned > allowed.length || !allowed.includes(kind) || signedKinds.has(kind) || leg.chainId !== 4663)
1320
+ return fail("PERPS_BUILD_REFUSED", `Refused to sign deposit leg ${kind}: it is not the plan this deposit was built with.`, { clientDepositId, operationId: build.operationId });
1321
+ signedKinds.add(kind);
1322
+ const signed = await relaySign({
1323
+ method: "eth_signTransaction",
1324
+ params: {
1325
+ transaction: {
1326
+ chain_id: leg.chainId,
1327
+ data: leg.data,
1328
+ from: source.address,
1329
+ gas_limit: hex(leg.gas),
1330
+ max_fee_per_gas: hex(leg.maxFeePerGas),
1331
+ max_priority_fee_per_gas: hex(leg.maxPriorityFeePerGas),
1332
+ nonce: leg.nonce,
1333
+ to: leg.to,
1334
+ type: 2,
1335
+ value: hex(leg.value)
1336
+ }
1337
+ }
1338
+ });
1339
+ body = await call(cfg, fetch2, "POST", submitPath, {
1340
+ ...ids,
1341
+ operationId: build.operationId,
1342
+ signedTransaction: signed
1343
+ });
1344
+ }
1345
+ return { text: JSON.stringify({ ...body, clientDepositId }) };
1346
+ } catch (error) {
1347
+ if (error instanceof Refusal)
1348
+ return error.tool;
1349
+ return fail("MCP_TRANSPORT", error instanceof Error ? error.message : "The deposit call failed.");
1350
+ }
1351
+ }
1352
+
1353
+ // src/plans.ts
1354
+ var PLAN_CAPABILITY_NOTE = "A capability marked yes is what the plan allows. It is subject to the deployment's own switches (perps and own-wallet bridges each have one) and to the wallet, scopes and setup the feature needs. Every plan can launch a token from its embedded wallet, with an optional dev buy (same transaction on Solana; best-effort follow-up on Hood), through the headless launch; the two launch rows are additional routes, not the only ones.";
1355
+ var PLAN_CAPABILITY_LABELS = {
1356
+ tradeCandleTokens: "Trade Candle-launched tokens",
1357
+ tradeBaseAssets: "Trade base assets",
1358
+ freeBaseTransfers: "Base-pair swaps and own-wallet bridges (when enabled), no Candle fee",
1359
+ sellExternalTokens: "Sell tokens not launched on Candle",
1360
+ buyExternalTokens: "Buy tokens not launched on Candle",
1361
+ hyperliquidPerps: "Hyperliquid perps (when enabled)",
1362
+ selfLaunch: "Launch from a linked or TEE wallet (self-signed)",
1363
+ atomicLaunch: "Atomic launch: launch + 1–4 first buys in one bundle",
1364
+ createLinkedWallets: "Create linked wallets",
1365
+ importLinkedWallets: "Import linked wallets",
1366
+ limitOrders: "Limit orders",
1367
+ quant: "Quant on Telegram"
1368
+ };
1369
+ var PLAN_LABELS = { free: "Free", believer: "Believer", pro: "Pro", max: "Max" };
1370
+ function planLabel(plan) {
1371
+ return Object.hasOwn(PLAN_LABELS, plan) ? PLAN_LABELS[plan] : plan;
1372
+ }
1373
+ function formatPlanBps(bps) {
1374
+ return bps === 0 ? "none" : `${Number((bps / 100).toFixed(4))}%`;
1375
+ }
1376
+ function count(n) {
1377
+ return n.toLocaleString("en-US");
1378
+ }
1379
+ function priceCell(entry) {
1380
+ if (entry.price)
1381
+ return `$${count(entry.price.pricePerMonthUsd)} a month`;
1382
+ return entry.plan === "free" ? "free" : "not sold";
1383
+ }
1384
+ function planTableRows(table) {
1385
+ const plans = table.plans;
1386
+ const row = (label, cell) => [label, ...plans.map(cell)];
1387
+ const rows = [
1388
+ row("Price", priceCell),
1389
+ row("Agent trade fee", (e) => formatPlanBps(e.feeBps)),
1390
+ row("Perps builder fee", (e) => formatPlanBps(e.perpFeeBps)),
1391
+ row("Requests per minute", (e) => count(e.limits.rateLimitPerMin)),
1392
+ row("Launches per day", (e) => count(e.limits.dailyLaunchCap)),
1393
+ row("Uploads per minute", (e) => count(e.limits.uploadsPerMin)),
1394
+ row("Linked wallets", (e) => count(e.limits.linkedWallets))
1395
+ ];
1396
+ const known = Object.keys(PLAN_CAPABILITY_LABELS);
1397
+ const served = new Set;
1398
+ for (const entry of plans)
1399
+ for (const key of Object.keys(entry.capabilities ?? {}))
1400
+ served.add(key);
1401
+ const keys = [
1402
+ ...known.filter((k) => served.has(k)),
1403
+ ...[...served].filter((k) => !Object.hasOwn(PLAN_CAPABILITY_LABELS, k))
1404
+ ];
1405
+ for (const key of keys) {
1406
+ const label = Object.hasOwn(PLAN_CAPABILITY_LABELS, key) ? PLAN_CAPABILITY_LABELS[key] : key;
1407
+ rows.push(row(label, (e) => {
1408
+ const value = e.capabilities?.[key];
1409
+ return value === true ? "yes" : value === false ? "no" : "-";
1410
+ }));
1411
+ }
1412
+ return { headers: ["", ...plans.map((e) => planLabel(e.plan))], rows };
1413
+ }
1414
+ function planPromotionLine(table) {
1415
+ if (!(table.promoMaxDays > 0))
1416
+ return null;
1417
+ const days = table.promoMaxDays === 1 ? "1 day" : `${table.promoMaxDays} days`;
1418
+ return `A first Pro purchase includes Max for its first ${days}, then continues on Pro.`;
1419
+ }
1420
+ function planTableMarkdown(table) {
1421
+ const { headers: headers2, rows } = planTableRows(table);
1422
+ const line = (cells) => `| ${cells.map((c) => c.replace(/\|/g, "\\|")).join(" | ")} |`;
1423
+ return [line(headers2), line(headers2.map(() => "---")), ...rows.map(line)].join(`
1424
+ `);
1425
+ }
1178
1426
 
1179
1427
  // src/tools.ts
1180
1428
  var TOOL_NAMES = [
@@ -1184,6 +1432,7 @@ var TOOL_NAMES = [
1184
1432
  "candle_token_forensics",
1185
1433
  "candle_report_activity",
1186
1434
  "candle_get_agent_profile",
1435
+ "candle_get_plans",
1187
1436
  "candle_trade",
1188
1437
  "candle_launch_and_seed",
1189
1438
  "candle_swap",
@@ -1204,7 +1453,8 @@ var TOOL_NAMES = [
1204
1453
  "candle_perps_cancel",
1205
1454
  "candle_perps_orders",
1206
1455
  "candle_perps_positions",
1207
- "candle_perps_leverage"
1456
+ "candle_perps_leverage",
1457
+ "candle_perps_deposit"
1208
1458
  ];
1209
1459
  function resolveToolAllowlist(env) {
1210
1460
  const raw = env.CANDLE_MCP_TOOLS?.trim();
@@ -1303,6 +1553,9 @@ function buildRequest(name, args, cfg) {
1303
1553
  init: { method: "GET", headers: jsonHeaders() }
1304
1554
  };
1305
1555
  }
1556
+ case "candle_get_plans": {
1557
+ return { url: `${base2}/api/v1/agent/plans`, init: { method: "GET", headers: jsonHeaders() } };
1558
+ }
1306
1559
  case "candle_swap": {
1307
1560
  const apiKey = requireApiKey2(cfg);
1308
1561
  return {
@@ -1372,6 +1625,22 @@ function buildRequest(name, args, cfg) {
1372
1625
  }
1373
1626
  }
1374
1627
  }
1628
+ function plansMarkdown(body) {
1629
+ try {
1630
+ const table = JSON.parse(body);
1631
+ if (!Array.isArray(table.plans))
1632
+ return "";
1633
+ const promotion = planPromotionLine(table);
1634
+ const markdown = `${planTableMarkdown(table)}
1635
+
1636
+ ${PLAN_CAPABILITY_NOTE}`;
1637
+ return promotion ? `${markdown}
1638
+
1639
+ ${promotion}` : markdown;
1640
+ } catch {
1641
+ return "";
1642
+ }
1643
+ }
1375
1644
  async function callAndRelay(name, args, cfg) {
1376
1645
  const { url, init } = buildRequest(name, args, cfg);
1377
1646
  const res = await fetch(url, init);
@@ -1456,7 +1725,7 @@ var tradeShape = {
1456
1725
  side: z2.enum(["buy", "sell"]),
1457
1726
  amount: z2.string().optional().describe("Decimal amount. Buys: how much of THIS TOKEN'S OWN quote asset to spend (SOL for a " + 'SOL-launched token, USDC for a USDC-quoted one, and so on: e.g. "0.5"). Sells: how many ' + "TOKENS to sell. Pass exactly one of amount or percent."),
1458
1727
  percent: z2.number().optional().describe("Sells only: sell this percent (integer 1-100) of the holding. Live trades size against the " + "embedded wallet. Paper trades (`paper: true`) size against this key's paper inventory -- " + "the position a previous paper buy credited -- because paper never moves the live wallet."),
1459
- quoteAsset: z2.string().optional().describe('What the wallet spends on a buy or receives on a sell: "sol", "usdc" or "cndl" on Solana, ' + '"eth" or "usdg" on Hood. Safe to pass through from candle_quote. On Solana it applies only ' + "to an arbitrary mint Candle never launched (Pro/Max) and is ignored for a Candle token, " + "whose quote comes from the token itself. On Hood it is the settlement asset of a DEX " + "trade; a USDG buy adds an approval transaction an ETH buy does not. It is not the route: " + "the cheapest path to the asset is chosen separately. Defaults to sol / ETH settlement."),
1728
+ quoteAsset: z2.string().optional().describe('What the wallet spends on a buy or receives on a sell: "sol", "usdc" or "cndl" on Solana, ' + '"eth" or "usdg" on Hood. Echoing the `quoteAsset` a quote (POST /api/v1/trade/agent/quote) returned is safe. On Solana it applies only ' + "to an arbitrary mint Candle never launched (a buy needs Pro or Max; a sell works on any plan) and is ignored for a Candle token, " + "whose quote comes from the token itself. On Hood it is the settlement asset of a DEX " + "trade; a USDG buy adds an approval transaction an ETH buy does not. It is not the route: " + "the cheapest path to the asset is chosen separately. Defaults to sol / ETH settlement."),
1460
1729
  maxSlippageBps: z2.number().optional().describe("Max slippage in basis points; API default applies when omitted"),
1461
1730
  clientTradeId: z2.string().optional().describe("Idempotency key. Auto-generated when omitted and echoed in the result. Retrying with the " + "SAME id is safe (idempotent replay); a new id is a SECOND trade."),
1462
1731
  paper: z2.preprocess((value) => value === "true" || value === 1 || value === "1" ? true : value === "false" || value === 0 || value === "0" ? false : value, z2.boolean().optional().describe("Rehearse instead of trading. The request passes every admission rule a live trade passes " + "-- the same planner, spend gate, key cap and loss limits -- and records the quote, but " + "nothing is ever broadcast and no funds move. Use it to check that a strategy is admitted " + "before risking anything on it. A paper fill is optimistic by construction: it books the " + "quoted price, so the gap between a paper arm and a live one IS the execution cost. " + "A sell of a mint this key already paper-bought also closes that paper book when the " + "live wallet is empty, even if this flag is omitted."))
@@ -1465,7 +1734,7 @@ var { buyAmount: _rawBuyAmount, ...seedableLaunchShape } = launchTokenShape;
1465
1734
  var launchAndSeedShape = {
1466
1735
  ...seedableLaunchShape,
1467
1736
  clientLaunchId: z2.string().optional().describe("Idempotency key. Auto-generated when omitted and echoed in the result."),
1468
- devBuy: z2.string().optional().describe('Seed buy in DECIMAL units of the quote asset this launch selects (e.g. "0.25" SOL, or ' + "ETH on hood), bundled into the launch transaction itself. Follows quoteAsset, which " + "defaults to sol on solana and eth on hood. Capped by the platform dev-buy ceiling; for " + "a larger seed, launch then follow with candle_trade.")
1737
+ devBuy: z2.string().optional().describe('Seed buy in DECIMAL units of the quote asset this launch selects (e.g. "0.25" SOL, or ' + "ETH on hood), bundled into the launch transaction itself on solana and sent as a follow-up " + "transaction on hood. Paid from the account's embedded wallet. Follows quoteAsset, which " + "defaults to sol on solana and eth on hood. Capped by the platform dev-buy ceiling; for " + "a larger seed, launch then follow with candle_trade.")
1469
1738
  };
1470
1739
  var transferShape = {
1471
1740
  chain: z2.enum(["solana", "hood"]).describe("Which chain the transfer executes on"),
@@ -1490,7 +1759,7 @@ function registerTools(server, env = process.env) {
1490
1759
  };
1491
1760
  register("candle_launch_token", {
1492
1761
  title: "Launch a token on Candle",
1493
- description: "Launch a new token via the Candle headless launch API. Set dryRun: true to validate without spending anything.",
1762
+ description: "Launch a new token via the Candle headless launch API, from the account's embedded wallet. Works on every plan, Free included. Set dryRun: true to validate without spending anything.",
1494
1763
  inputSchema: launchTokenShape
1495
1764
  }, async (args) => callAndRelay("candle_launch_token", args, cfg));
1496
1765
  register("candle_get_market", {
@@ -1524,6 +1793,24 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
1524
1793
  description: "Read a Candle user's public agent profile: whether agent features are enabled and launch counts.",
1525
1794
  inputSchema: getAgentProfileShape
1526
1795
  }, async (args) => callAndRelay("candle_get_agent_profile", args, cfg));
1796
+ register("candle_get_plans", {
1797
+ title: "Plans: prices, fees, limits and what each can do",
1798
+ description: "The plan table this Candle deployment serves (GET /api/v1/agent/plans). No key needed. Reads only. For " + "each plan: price (null when not sold), feeBps (the agent fee charged on top of every trade or dev buy the " + "API builds), perpFeeBps (the Hyperliquid builder fee), limits for a new key (requests per minute, launches " + "per day, uploads per minute, linked wallets) and capabilities: buyExternalTokens, sellExternalTokens, " + "tradeBaseAssets, tradeCandleTokens, selfLaunch, atomicLaunch, createLinkedWallets, importLinkedWallets, " + "hyperliquidPerps, limitOrders, quant, freeBaseTransfers. promoMaxDays is the days of Max a first Pro " + "purchase includes (0: no promotion). Quote prices and fees from here, never from memory: they differ by " + "deployment and change at the three-plan launch (Free, Pro, Max). The account's own plan and fee are in " + "candle_execution_status's tier. A TIER_REQUIRED refusal names a capability this table shows the account's " + "plan lacks. A capability true here is plan eligibility, not deployment availability: perps and own-wallet bridges " + "are each behind a deployment switch and need their wallet, scopes and setup. Returns the server's JSON, then the same table as Markdown.",
1799
+ inputSchema: {}
1800
+ }, async () => {
1801
+ const { url, init } = buildRequest("candle_get_plans", {}, cfg);
1802
+ const res = await fetch(url, init);
1803
+ noteVersionHeaders(res);
1804
+ const text = await res.text();
1805
+ if (!res.ok)
1806
+ return { content: [{ type: "text", text }], isError: true };
1807
+ return {
1808
+ content: [
1809
+ { type: "text", text },
1810
+ { type: "text", text: plansMarkdown(text) }
1811
+ ]
1812
+ };
1813
+ });
1527
1814
  register("candle_get_operation", {
1528
1815
  title: "What happened to a write",
1529
1816
  description: "Look up a trade or launch by the id its write used, and find out whether it landed. " + `Reads only; moves nothing.
@@ -1554,7 +1841,7 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
1554
1841
  }, async (args) => callAndRelay("candle_set_profile_wallets", args, cfg));
1555
1842
  register("candle_get_profile_pnl", {
1556
1843
  title: "Read an agent profile's P&L",
1557
- description: "Realized profit for this profile's own fills, the Candle fees charged against it, and the " + "positions it still holds with their cost basis, each MARKED at Candle's current price where one " + "exists: `markPriceUsd`, `marketValueUsd` and `unrealizedUsd` per position, and `unrealizedUsd` " + "overall. A position with no price is counted in `unmarkedPositions` and left out of unrealized, " + "never valued at zero; `oldestMarkAt` says how old the marks are. Deposits, withdrawals and " + "transfers are excluded: funding a wallet is not profit. Check `unvalued` and `truncated` before " + "quoting the number; they mean the total is partial. `pnl.byWallet` splits the same fills by the " + "wallet that paid for each trade (main or linked, with the linked wallet's label), one row per " + "wallet with the same figures, for tracking one strategy per wallet. Each row is an independent " + "average-cost pool over its own fills, so the rows need not sum to the total (a token moved " + "between wallets, or two wallets holding one token at different costs); the total stays correct. " + "Every open position and every `byWallet` row (and its positions) carries `chain` ('solana' or " + "'hood'), and `pnl.byChain` has the total's figures over each chain's fills alone, both chains " + "always present, with `openPositions` there a count (the positions are in `pnl.openPositions`, by " + "`chain`). Each chain is its own average-cost pool, so the two need not sum exactly to the total. A " + "server that predates per-chain P&L omits `chain` and `byChain`. Reads only.",
1844
+ description: "This profile's share of the account's P&L: realized profit on the sales it made, the Candle fees " + "charged against it, and the positions that belong to it (the wallet's bound key, else the key " + "whose buy opened them) with their cost basis, each MARKED at Candle's current price where one " + "exists: `markPriceUsd`, `marketValueUsd` and `unrealizedUsd` per position, and `unrealizedUsd` " + "overall. A position with no price is counted in `unmarkedPositions` and left out of unrealized, " + "never valued at zero; `oldestMarkAt` says how old the marks are. Deposits and withdrawals are " + "excluded: funding a wallet is not profit. Check `unvalued` and `truncated` before quoting the " + "number; they mean the total is partial. The figures come from the account's one P&L read, the " + "same as the console's, to the cent: `pnl.totalUsd` (realized net plus unrealized) is the " + "console's P&L. Positions live in wallets: each wallet is its own average-cost pool, the total " + "is the sum of `pnl.byWallet` (one row per wallet, main or linked, with the linked wallet's " + "label; `unknown:solana` or `unknown:hood` is the row for fills no record places in a wallet), " + "and one token held in two wallets is two positions, each with its `wallet`. A recorded move " + "between the account's wallets carries its cost and realizes nothing. Each position carries " + "`agent` (the key it belongs to) and `dust` when it is worth under one cent; dust stays listed " + "and counted, and `openPositionsExDust` leaves it out. `closed` gives the closed positions as " + "`madeUsd` + `lostUsd` + `partialSellsUsd` = `realizedNetUsd`, with `wins` and `losses`. " + "`lookback` and `truncated` are the account's activity bound, and `tradesConsidered` counts this " + "key's ledger fills. Since 2026-10-02 the total is the sum of wallets rather than one pool across " + "them, so realized figures can differ from earlier reads; a server that predates it omits " + "`totalUsd`, `closed`, `openPositionsExDust`, `wallet`, `agent` and `dust`. " + "Every open position and every `byWallet` row (and its positions) carries `chain` ('solana' or " + "'hood'), and `pnl.byChain` has the total's figures over each chain's fills alone, both chains " + "always present, with `openPositions` there a count (the positions are in `pnl.openPositions`, by " + "`chain`). The two chains sum to the total. A " + "server that predates per-chain P&L omits `chain` and `byChain`. While Hyperliquid is enabled, " + "`hyperliquid` separately reports main-perp realized gross, signed funding, fees (inclusive of builder fees), " + "and net = gross + funding - fees, for currently bound EVM TEE wallets since the current TEE binding. " + "Check `read` and `truncated`; historical bindings and unrealized perps are excluded. Reads only.",
1558
1845
  inputSchema: profilePnlShape
1559
1846
  }, async (args) => callAndRelay("candle_get_profile_pnl", args, cfg));
1560
1847
  register("candle_get_profile_trades", {
@@ -1564,7 +1851,7 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
1564
1851
  }, async (args) => callAndRelay("candle_get_profile_trades", args, cfg));
1565
1852
  register("candle_get_portfolio", {
1566
1853
  title: "Read what the account holds, on Solana and Hood",
1567
- description: "Balances and prices for the wallets Candle already knows on this account, on both chains: the " + "embedded wallet and every TEE wallet. Solana wallets are in `embedded` and `tee` (SOL as raw " + "`lamports`, tokens as raw `amountRaw` with `decimals`). Hood wallets are in `hood.embedded` and " + "`hood.tee` (ETH as raw `wei`, ERC-20s including USDG), never in the top-level arrays. Every " + "wallet and holding carries `chain`. Prices are in `prices`: a Solana mint under its own " + "address, Hood ETH under `hood:native`, and a Hood token under `hood:<contract lowercased>`, so " + "lowercase the address before looking it up. An unpriced entry is `priceUsd: null`, never zero; " + "a Hood one says why in `unpricedReason` (no-market-row, unusable-price, stale-mark, " + "source-unavailable), and `hood.unpriced` / `hood.unpricedByReason` count them. A Hood token " + "mark older than six hours is not used, so many Hood tokens read unpriced. A wallet whose read " + "failed has null balances, never zero, and is listed in `unavailable` (Solana) or " + "`hood.unavailable`; check `complete` before quoting a total. Vault and external wallets are " + "not included: Candle does not know their addresses (the Candle CLI's `candle portfolio` reads " + "them over your own RPC). Needs a key with the account:read scope; without it the answer is " + "SCOPE_MISSING. A server that predates Hood in the portfolio omits `hood` and `chain`. Reads " + "only; moves nothing.",
1854
+ description: "Balances and prices for the wallets Candle already knows on this account, on both chains: the " + "embedded wallet and every TEE wallet. Solana wallets are in `embedded` and `tee` (SOL as raw " + "`lamports`, tokens as raw `amountRaw` with `decimals`). Hood wallets are in `hood.embedded` and " + "`hood.tee` (ETH as raw `wei`, ERC-20s including USDG), never in the top-level arrays. Every " + "wallet and holding carries `chain`. Prices are in `prices`: a Solana mint under its own " + "address, Hood ETH under `hood:native`, and a Hood token under `hood:<contract lowercased>`, so " + "lowercase the address before looking it up. An unpriced entry is `priceUsd: null`, never zero; " + "a Hood one says why in `unpricedReason` (no-market-row, unusable-price, stale-mark, " + "source-unavailable), and `hood.unpriced` / `hood.unpricedByReason` count them. A Hood token " + "mark older than six hours is not used, so many Hood tokens read unpriced. A wallet whose read " + "failed has null balances, never zero, and is listed in `unavailable` (Solana) or " + "`hood.unavailable`; check `complete` before quoting a total. `walletsComplete` is false only " + "when the wallet list itself was cut off, and is absent on a server that predates it: then " + "`complete: false` may be a cut-off list or a failed read. Vault and external wallets are " + "not included: Candle does not know their addresses (the Candle CLI's `candle portfolio` reads " + "them over your own RPC). Needs a key with the account:read scope; without it the answer is " + "SCOPE_MISSING. A server that predates Hood in the portfolio omits `hood` and `chain`. Reads " + "only; moves nothing.",
1568
1855
  inputSchema: {}
1569
1856
  }, async () => callAndRelay("candle_get_portfolio", {}, cfg));
1570
1857
  register("candle_resolve_token", {
@@ -1624,6 +1911,8 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
1624
1911
 
1625
1912
  ` + "Arguments: `mint` and `side` are required. Amounts are DECIMAL, never raw base units " + '(amount: "0.5", not lamports). Omitting the amount on a sell sells the whole ' + `position.
1626
1913
 
1914
+ ` + "Plans: every plan can buy and sell Candle-launched tokens and base assets, and SELL a token it holds that " + "Candle did not launch. BUYING such a token needs Pro or Max (TIER_REQUIRED otherwise). Each trade pays the " + `plan's agent fee on top (feeBps in candle_get_plans).
1915
+
1627
1916
  ` + "Pass `paper: true` to rehearse: every admission rule runs and the quote is recorded, but " + "nothing broadcasts and no funds move. A paper buy credits this key's paper inventory, " + "including for external Solana mints routed through Jupiter. A later sell by amount or " + "percent closes that book without reading the live wallet and without MARKET_NOT_FOUND " + "-- including when `paper` is omitted on the exit, as long as the paper position exists. " + "Do this before the first live trade of a new strategy, and whenever you are unsure a " + `trade would be admitted at all.
1628
1917
 
1629
1918
  ` + `After the call:
@@ -1636,7 +1925,7 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
1636
1925
  });
1637
1926
  register("candle_launch_and_seed", {
1638
1927
  title: "Launch a token and seed it",
1639
- description: "Launch a new token with an optional dev-buy seed bundled into the launch itself, then " + "return the fresh market state and token links in one result. MOVES REAL FUNDS unless " + "dryRun. Seeds above the platform dev-buy ceiling are rejected (DEV_BUY_TOO_HIGH); " + "launch, then top up with candle_trade.",
1928
+ description: "Launch a new token from the account's embedded wallet, with an optional dev-buy seed (in the " + "launch transaction on solana, a best-effort follow-up on hood). Works on every plan, Free included (this is not the atomic launch). Then " + "return the fresh market state and token links in one result. MOVES REAL FUNDS unless " + "dryRun. Seeds above the platform dev-buy ceiling are rejected (DEV_BUY_TOO_HIGH); " + "launch, then top up with candle_trade.",
1640
1929
  inputSchema: launchAndSeedShape
1641
1930
  }, async (args) => {
1642
1931
  const result = await executeLaunchAndSeed(args, cfg, fetch);
@@ -1654,7 +1943,7 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
1654
1943
  }, perpsTool("candle_perps_setup"));
1655
1944
  register("candle_perps_open", {
1656
1945
  title: "Open a perps position",
1657
- description: "Open or add to a perpetual position on Hyperliquid's main perp exchange: a market order (IOC within " + "slippageBps of the mid) without `price`, a limit order with it, and optional reduce-only takeProfit and " + "stopLoss triggers. Free, Believer and Pro pay a 0.1% builder fee; Max pays none." + perpsWrite,
1946
+ description: "Open or add to a perpetual position on Hyperliquid's main perp exchange: a market order (IOC within " + "slippageBps of the mid) without `price`, a limit order with it, and optional reduce-only takeProfit and " + "stopLoss triggers. Every plan except Max pays Candle a builder fee on each order (0.1% today; perpFeeBps in " + "candle_get_plans); Max pays none." + perpsWrite,
1658
1947
  inputSchema: perpsShapes.candle_perps_open
1659
1948
  }, perpsTool("candle_perps_open"));
1660
1949
  register("candle_perps_close", {
@@ -1682,17 +1971,26 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
1682
1971
  description: "Set a market's leverage and margin mode (cross or isolated) on the account, within the key's maximum leverage." + perpsWrite,
1683
1972
  inputSchema: perpsShapes.candle_perps_leverage
1684
1973
  }, perpsTool("candle_perps_leverage"));
1974
+ register("candle_perps_deposit", {
1975
+ title: "Deposit to Hyperliquid",
1976
+ description: "MOVES REAL FUNDS unless submit is false. Deposit onto the key's Hyperliquid account through Relay: SOL or " + "USDC from its Solana TEE wallet, or ETH or USDG from its Hood TEE wallet, credited as Hyperliquid perps " + "USDC at the key's EVM TEE wallet's own address (Candle resolves it; no recipient is taken). Refused below a " + "floor that covers Hyperliquid's 1 USDC first-deposit charge. No Candle fee. Needs swap:write and a raw cap " + "on the asset, and CANDLE_KEY_SIGNER_PEM_FILE to sign. There is no withdrawal from Hyperliquid yet.",
1977
+ inputSchema: perpsDepositShape
1978
+ }, async (args) => {
1979
+ const result = await executePerpsDeposit(args, cfg, env, fetch);
1980
+ return { content: [{ type: "text", text: result.text }], ...result.isError ? { isError: true } : {} };
1981
+ });
1685
1982
  }
1686
1983
 
1687
1984
  // src/server.ts
1688
1985
  var INSTRUCTIONS = `Candle is a trading and token-launch rail for agents. You hold a scoped API key, never a private key; signing and funding stay with the key owner's wallet.
1689
1986
 
1690
- START HERE — five tools need NO credential. Call these first to confirm the server is wired before asking anyone for anything:
1987
+ START HERE — six tools need NO credential. Call these first to confirm the server is wired before asking anyone for anything:
1691
1988
  candle_get_market price, market cap, volume, curve state for one token
1692
1989
  candle_get_feed the roster: hot streak, new pairs, graduated, blue chip
1693
1990
  candle_resolve_token a ticker or partial name -> mint address + chain
1694
1991
  candle_token_forensics call this before quoting or buying. Returns the on-chain developer (never a launchpad shared authority; deployer.attribution names the launchpad or issuer when no developer is on chain), their went-to-zero rate and last coins, who bought in the deploy window (strangers in the same slot are the bundle signal), same-funder insider share, same-funder cluster, and safety.summary with six sourced flags. Refuse an unprompted buy when flagged; incomplete or unknown is not clearance. launch.deployerLaunches is an inclusive informational count, never a warning
1695
1992
  candle_get_agent_profile your own tier, caps and verified activity
1993
+ candle_get_plans every plan's price, fees, limits and what it can do; quote these, never from memory
1696
1994
 
1697
1995
  COVERAGE — read this before you treat an error as a broken server.
1698
1996
  candle_get_feed indexes the wider market (pump.fun, pons.family and other external launchpads).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@candledottv/mcp",
3
- "version": "0.10.5",
3
+ "version": "0.10.7",
4
4
  "description": "MCP server for the Candle agent rail: launch tokens, read markets, report activity",
5
5
  "type": "module",
6
6
  "license": "MIT",