@candledottv/mcp 0.10.5 → 0.10.6
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 +53 -19
- package/dist/index.js +304 -6
- 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-
|
|
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
|
-
|
|
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 `
|
|
14
|
-
server with no `env` block at all and those
|
|
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-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
77
|
-
|
|
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
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
169
|
-
|
|
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.
|
|
109
|
+
var SERVER_VERSION = "0.10.6";
|
|
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.";
|
|
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: "Self-launch from a linked wallet",
|
|
1363
|
+
atomicLaunch: "Atomic launch with first buys",
|
|
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.
|
|
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."))
|
|
@@ -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: "
|
|
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", {
|
|
@@ -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:
|
|
@@ -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.
|
|
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 —
|
|
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).
|