@candledottv/mcp 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +119 -42
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -85,7 +85,7 @@ tools, getting a key, funding the embedded wallet, and idempotent retries, see
85
85
  | `candle_launch_token` | Launch a token on Candle | `POST /api/v1/launch/headless` (or `/dry-run` when `dryRun: true`) | `CANDLE_AGENT_API_KEY` |
86
86
  | `candle_get_market` | Get market state | `GET /api/v1/markets/:chain/:mint` | none |
87
87
  | `candle_get_feed` | Get a token feed | `GET /api/v1/markets/feed?bucket=...` | none |
88
- | `candle_token_forensics` | Deployer history, deploy-window buyers, holder concentration, risk tier | `GET /api/v1/markets/:chain/:mint/forensics` | none |
88
+ | `candle_token_forensics` | Deployer history, deploy-window buyers, sourced token safety flags | `GET /api/v1/markets/:chain/:mint/forensics` | none |
89
89
  | `candle_report_activity` | Report on-chain activity | `POST /api/v1/activity/report` | `CANDLE_AGENT_API_KEY` |
90
90
  | `candle_get_agent_profile` | Get an agent profile | `GET /api/v1/users/:idOrWallet/agent` | none |
91
91
  | `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`) |
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.9.0";
109
+ var SERVER_VERSION = "0.10.0";
110
110
 
111
111
  // src/update-notice.ts
112
112
  var PLAIN_VERSION = /^\d+\.\d+\.\d+$/;
@@ -220,10 +220,36 @@ async function readMarket(mint, cfg, doFetch, extra) {
220
220
  headers: headers()
221
221
  });
222
222
  const text = await res.text();
223
- if (!res.ok)
224
- return { status: res.status, err: relayRead(text, extra) };
223
+ if (!res.ok) {
224
+ let reason;
225
+ try {
226
+ reason = JSON.parse(text)?.error?.routing?.reason;
227
+ } catch {}
228
+ return { status: res.status, err: relayRead(text, extra), reason };
229
+ }
225
230
  return { market: JSON.parse(text).market ?? {} };
226
231
  }
232
+ function isPaperFlag(value) {
233
+ return value === true || value === "true" || value === 1 || value === "1";
234
+ }
235
+ function heldPaperPosition(positions, mint) {
236
+ const key = (value) => {
237
+ const trimmed = value.trim();
238
+ return /^0x[0-9a-fA-F]{40}$/.test(trimmed) ? trimmed.toLowerCase() : trimmed;
239
+ };
240
+ return positions.find((p) => key(p.mint) === key(mint) && p.amountRaw !== "0");
241
+ }
242
+ async function readPaperInventory(cfg, doFetch, extra) {
243
+ const res = await doFetch(`${base(cfg)}/api/v1/trade/agent/paper/inventory`, {
244
+ method: "GET",
245
+ headers: headers(cfg.apiKey)
246
+ });
247
+ const text = await res.text();
248
+ if (!res.ok)
249
+ return { err: relayRead(text, extra) };
250
+ const body = JSON.parse(text);
251
+ return { positions: body.positions ?? [] };
252
+ }
227
253
  async function executeTrade(args, cfg, doFetch) {
228
254
  const apiKey = requireApiKey(cfg);
229
255
  const clientTradeId = args.clientTradeId ?? randomUUID();
@@ -237,19 +263,25 @@ async function executeTrade(args, cfg, doFetch) {
237
263
  return errText("percent is only valid on sells; buys take a quote-asset amount", { clientTradeId });
238
264
  }
239
265
  let amountRaw;
240
- let resolved;
266
+ let resolved = {};
267
+ let paper = isPaperFlag(args.paper);
241
268
  try {
242
269
  if (args.side === "buy") {
243
270
  const amount = args.amount;
244
271
  const read = await readMarket(args.mint, cfg, doFetch, { clientTradeId });
245
272
  let decimals;
246
- if ("market" in read) {
273
+ if ("market" in read && (read.market.external === true || read.market.candleLaunched === false)) {
274
+ const q = quoteIdDecimals(args.quoteAsset, chainForMint(args.mint), { clientTradeId });
275
+ if ("err" in q)
276
+ return q.err;
277
+ decimals = q.decimals;
278
+ } else if ("market" in read) {
247
279
  const quoteDecimals = read.market.quoteDecimals;
248
280
  if (typeof quoteDecimals !== "number") {
249
281
  return errText(`could not resolve the quote decimals for mint ${args.mint}; a buy is denominated in that token's own quote asset and this market does not report its scale. Read the market with candle_get_market, then trade a raw amount via the SDK instead`, { clientTradeId });
250
282
  }
251
283
  decimals = quoteDecimals;
252
- } else if (read.status === 404) {
284
+ } else if (read.status === 404 && read.reason !== "chain_mismatch") {
253
285
  const q = quoteIdDecimals(args.quoteAsset, chainForMint(args.mint), { clientTradeId });
254
286
  if ("err" in q)
255
287
  return q.err;
@@ -264,17 +296,36 @@ async function executeTrade(args, cfg, doFetch) {
264
296
  resolved = { amountDecimal: amount, decimals, amountRaw };
265
297
  } else if (args.amount !== undefined) {
266
298
  const read = await readMarket(args.mint, cfg, doFetch, { clientTradeId });
267
- if (!("market" in read))
299
+ let decimals;
300
+ if ("market" in read) {
301
+ if (typeof read.market.decimals !== "number") {
302
+ return errText(`could not resolve decimals for mint ${args.mint}; pass a raw-ready amount via the SDK instead`, { clientTradeId });
303
+ }
304
+ decimals = read.market.decimals;
305
+ } else if (read.status === 404 && read.reason !== "chain_mismatch") {
306
+ const inventory = await readPaperInventory(cfg, doFetch, { clientTradeId });
307
+ if ("err" in inventory) {
308
+ return paper ? inventory.err : read.err;
309
+ }
310
+ const held = heldPaperPosition(inventory.positions, args.mint);
311
+ if (typeof held?.tokenDecimals !== "number") {
312
+ if (paper) {
313
+ return errText(`could not resolve decimals for mint ${args.mint}; no Candle market and no paper position with a known scale. Buy it in paper first, or pass a raw amount via the SDK`, { clientTradeId });
314
+ }
315
+ return read.err;
316
+ }
317
+ decimals = held.tokenDecimals;
318
+ paper = true;
319
+ } else {
268
320
  return read.err;
269
- const decimals = read.market.decimals;
270
- if (typeof decimals !== "number") {
271
- return errText(`could not resolve decimals for mint ${args.mint}; pass a raw-ready amount via the SDK instead`, { clientTradeId });
272
321
  }
273
322
  const converted = rawOrError(args.amount, decimals, { clientTradeId });
274
323
  if ("err" in converted)
275
324
  return converted.err;
276
325
  amountRaw = converted.raw;
277
326
  resolved = { amountDecimal: args.amount, decimals, amountRaw };
327
+ } else if (paper) {
328
+ resolved = { percent: args.percent };
278
329
  } else {
279
330
  const percent = args.percent;
280
331
  const walletsRes = await doFetch(`${base(cfg)}/api/v1/agent/wallets/embedded`, {
@@ -287,23 +338,37 @@ async function executeTrade(args, cfg, doFetch) {
287
338
  const walletsBody = JSON.parse(walletsText);
288
339
  const chain = chainForMint(args.mint);
289
340
  const address = chain === "hood" ? walletsBody.wallets?.evm?.address : walletsBody.wallets?.solana?.address;
341
+ let walletEmpty;
290
342
  if (!address) {
291
- return errText(`percent sells need an embedded ${chain === "hood" ? "EVM" : "Solana"} wallet, and this account has none`, { clientTradeId });
343
+ walletEmpty = errText(`percent sells need an embedded ${chain === "hood" ? "EVM" : "Solana"} wallet, and this account has none`, { clientTradeId });
344
+ } else {
345
+ const balRes = await doFetch(`${base(cfg)}/api/v1/tokens/${encodeURIComponent(args.mint)}/balance/${encodeURIComponent(address)}`, {
346
+ method: "GET",
347
+ headers: headers()
348
+ });
349
+ const balText = await balRes.text();
350
+ if (!balRes.ok)
351
+ return relayRead(balText, { clientTradeId });
352
+ const balBody = JSON.parse(balText);
353
+ const balance = balBody.payload?.balance;
354
+ if (balance) {
355
+ amountRaw = percentOfBalance(balance, percent);
356
+ resolved = { percent, balanceRaw: balance, amountRaw };
357
+ } else {
358
+ walletEmpty = errText(`the embedded wallet ${address} holds no ${args.mint}; nothing to sell`, {
359
+ clientTradeId
360
+ });
361
+ }
292
362
  }
293
- const balRes = await doFetch(`${base(cfg)}/api/v1/tokens/${encodeURIComponent(args.mint)}/balance/${encodeURIComponent(address)}`, {
294
- method: "GET",
295
- headers: headers()
296
- });
297
- const balText = await balRes.text();
298
- if (!balRes.ok)
299
- return relayRead(balText, { clientTradeId });
300
- const balBody = JSON.parse(balText);
301
- const balance = balBody.payload?.balance;
302
- if (!balance) {
303
- return errText(`the embedded wallet ${address} holds no ${args.mint}; nothing to sell`, { clientTradeId });
363
+ if (walletEmpty) {
364
+ const inventory = await readPaperInventory(cfg, doFetch, { clientTradeId });
365
+ if (!("err" in inventory) && heldPaperPosition(inventory.positions, args.mint)) {
366
+ paper = true;
367
+ resolved = { percent };
368
+ } else {
369
+ return walletEmpty;
370
+ }
304
371
  }
305
- amountRaw = percentOfBalance(balance, percent);
306
- resolved = { percent, balanceRaw: balance, amountRaw };
307
372
  }
308
373
  } catch (err) {
309
374
  return errText(err instanceof Error ? err.message : String(err), { clientTradeId });
@@ -312,11 +377,11 @@ async function executeTrade(args, cfg, doFetch) {
312
377
  clientTradeId,
313
378
  mint: args.mint,
314
379
  side: args.side,
315
- amountRaw,
380
+ ...amountRaw !== undefined ? { amountRaw } : { percent: args.percent },
316
381
  payer: { type: "main" },
317
382
  ...args.quoteAsset !== undefined ? { quoteAsset: args.quoteAsset } : {},
318
383
  ...args.maxSlippageBps !== undefined ? { maxSlippageBps: args.maxSlippageBps } : {},
319
- ...args.paper === true ? { paper: true } : {}
384
+ ...paper ? { paper: true } : {}
320
385
  }, doFetch);
321
386
  if ("thrown" in posted)
322
387
  return transportError("clientTradeId", clientTradeId, posted.thrown);
@@ -750,11 +815,11 @@ var tradeShape = {
750
815
  mint: z.string().describe("Token mint (solana) or contract address (hood)"),
751
816
  side: z.enum(["buy", "sell"]),
752
817
  amount: z.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."),
753
- percent: z.number().optional().describe("Sells only: sell this percent (integer 1-100) of the wallet's holding, on either chain."),
818
+ percent: z.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."),
754
819
  quoteAsset: z.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."),
755
820
  maxSlippageBps: z.number().optional().describe("Max slippage in basis points; API default applies when omitted"),
756
821
  clientTradeId: z.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."),
757
- paper: z.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.")
822
+ paper: z.preprocess((value) => value === "true" || value === 1 || value === "1" ? true : value === "false" || value === 0 || value === "0" ? false : value, z.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."))
758
823
  };
759
824
  var { buyAmount: _rawBuyAmount, ...seedableLaunchShape } = launchTokenShape;
760
825
  var launchAndSeedShape = {
@@ -790,23 +855,21 @@ function registerTools(server, env = process.env) {
790
855
  }, async (args) => callAndRelay("candle_launch_token", args, cfg));
791
856
  register("candle_get_market", {
792
857
  title: "Get market state",
793
- description: "Read the current market state for a token: lifecycle, pool address, whether buys are " + `open. Reads only; moves nothing. No key needed.
794
-
795
- ` + "COVERAGE: this answers for tokens that have a CANDLE market. candle_get_feed indexes " + "the wider market too (pump.fun, pons.family), so a mint that feed just returned can " + "still come back MARKET_NOT_FOUND here. That is a coverage boundary, not a fault and " + "not a reason to retry or re-authenticate -- say Candle has no market for it and move on.",
858
+ description: "Read Candle and indexed external markets, including indexed-but-not-routable tokens. No key needed. " + "Read candleLaunched, launchpad, venue and trade.routable; jupiterOk and discovery flags are distinct. " + "Routability is stored eligibility, not a quote or permission. General quotes use POST /api/v1/trade/agent/quote. " + "Curve quotes and lifecycle describe Candle launches. MARKET_NOT_FOUND is a legacy code: read " + "error.routing.reason, error.discovery and sibling error.retryable. A curve-only 404 does not mean untradeable.",
796
859
  inputSchema: getMarketShape
797
860
  }, async (args) => callAndRelay("candle_get_market", args, cfg));
798
861
  register("candle_token_forensics", {
799
862
  title: "Token forensics",
800
- description: `Gate a buy before making it: deployer history, who bought in the deploy window (the creator's own wallets are marked disclosed; strangers in the same slot are the bundle signal), holder concentration, and a risk tier (LOW/MODERATE/HIGH/CRITICAL) with per-factor reasons. Every measurement carries a coverage note -- 'unavailable' is not 'clean'. No key needed.
863
+ description: `Gate a buy before making it: who launched it (resolved on-chain; pump.fun's shared updateAuthority is never the developer), their went-to-zero rate and last coins, who bought in the deploy window (the creator's own wallets are marked disclosed; strangers in the same slot are the bundle signal), same-funder insider share, same-funder deployer cluster, and safety.summary with six sourced flags (mintAuthority, freezeAuthority, tokenExtensions, lpLock, sellability, liquidityDrain). Refuse an unprompted buy when flagged; incomplete or unknown is not clearance. launch.deployerLaunches is an inclusive informational count, never a warning. Every measurement carries a coverage note -- 'unavailable' is not 'clean'. No key needed.
801
864
 
802
- MARKET_NOT_FOUND means Candle has no market for that token and this could not run. That is also not 'clean': report that you could not check it, rather than reporting the token as safe. That refusal now carries error.coverage -- covered:false, a reason ('external_launchpad' when the token launched somewhere else, 'unknown_mint' when nobody has indexed it), the launchpad when known, and every check that consequently did not run. Read it instead of guessing. Most of the feed answers this way.`,
865
+ MARKET_NOT_FOUND means Candle has no market for that token and this could not run. That is also not 'clean': report that you could not check it, rather than reporting the token as safe. That refusal now carries error.coverage -- covered:false, a reason ('external_launchpad' when the token launched somewhere else, 'unknown_mint' when nobody has indexed it), the launchpad when known, and every check that consequently did not run. Read it instead of guessing. Most of the feed now answers with a partial report instead.`,
803
866
  inputSchema: tokenForensicsShape
804
867
  }, async (args) => callAndRelay("candle_token_forensics", args, cfg));
805
868
  register("candle_get_feed", {
806
869
  title: "Get a token feed",
807
870
  description: "Read one of the trade page's public feeds: new, graduated, onfire, or bluechip. Reads " + `only; moves nothing. No key needed. Start here when nobody has named a token.
808
871
 
809
- ` + "This indexes the WIDER market, not just Candle's own launches, so rows carry a " + "`launchpad` (pump.fun, pons.family, ...). A row appearing here does NOT mean Candle " + "has a market for it: candle_get_market and candle_token_forensics can legitimately " + `answer MARKET_NOT_FOUND for a mint this returned.
872
+ ` + "This indexes the WIDER market, not just Candle's own launches. Rows carry launchpad and " + "discovery flags (jupiterOk, externalTradeable, paperDiscoveryOk, organic0LiveOk). " + "candle_get_market resolves Jupiter-indexed rows; do not hard-skip organicScore=0 on live " + "when organic0LiveOk is true. liquidityDrawdownBps is a signal only — nothing here blocks " + `resolve or trade on drain; filter with where if you want to avoid draining pools.
810
873
 
811
874
  ` + "Filter, sort and pick fields SERVER-SIDE rather than reading the whole feed: an " + "unfiltered response is around 135KB and will not fit in a tool result. See `where`, " + "`sort` and `fields`.\n\n" + "One rule to know before screening on safety: a missing field is NOT a false one. " + "mintAuthorityDisabled and freezeAuthorityDisabled are absent on a real share of rows, " + "and absent means nobody checked, not that the authority is disabled. `where` never lets " + 'an absent field satisfy a comparison, so {"mintAuthorityDisabled":{"eq":true}} returns ' + "only tokens that actually say so.",
812
875
  inputSchema: getFeedShape
@@ -909,11 +972,11 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
909
972
  ` + `Before the first trade of a run, once:
910
973
  ` + "1. candle_execution_status -- confirms the wallets, the tier and this key's spend " + "limits. Call it at the start, or after an auth error; do not infer readiness from a " + `failed trade.
911
974
  ` + "2. candle_resolve_token -- if a human handed you a bare address. It returns the chain, " + `so you never have to guess it.
912
- ` + "3. candle_token_forensics -- before you quote or buy anything. It returns a risk tier " + "and per-factor reasons. MARKET_NOT_FOUND there means Candle has no market for the " + `token, NOT that the token is clean.
975
+ ` + "3. candle_token_forensics -- before you quote or buy anything. It returns safety.summary and sourced flags " + "with timestamps and details. Unknown is not clean. MARKET_NOT_FOUND there means Candle has no market for the " + `token, NOT that the token is clean.
913
976
 
914
977
  ` + "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.
915
978
 
916
- ` + "Pass `paper: true` to rehearse: every admission rule runs and the quote is recorded, but " + "nothing broadcasts and no funds move. Do this before the first live trade of a new " + `strategy, and whenever you are unsure a trade would be admitted at all.
979
+ ` + "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.
917
980
 
918
981
  ` + `After the call:
919
982
  ` + "- A timeout is not a failure. Retry with the SAME clientTradeId from the result -- it " + `coalesces the duplicate. A NEW id is a SECOND trade, and that is how you double-spend.
@@ -940,16 +1003,30 @@ START HERE — five tools need NO credential. Call these first to confirm the se
940
1003
  candle_get_market price, market cap, volume, curve state for one token
941
1004
  candle_get_feed the roster: hot streak, new pairs, graduated, blue chip
942
1005
  candle_resolve_token a ticker or partial name -> mint address + chain
943
- candle_token_forensics call this before quoting or buying, whenever the token has a Candle market. Returns deployer history, who bought in the deploy window (strangers in the same slot are the bundle signal), holder concentration, and a risk tier LOW/MODERATE/HIGH/CRITICAL with per-factor reasons
1006
+ candle_token_forensics call this before quoting or buying. Returns the on-chain developer (never a launchpad shared authority), 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
944
1007
  candle_get_agent_profile your own tier, caps and verified activity
945
1008
 
946
1009
  COVERAGE — read this before you treat an error as a broken server.
947
1010
  candle_get_feed indexes the wider market (pump.fun, pons.family and other external launchpads).
948
- candle_get_market and candle_token_forensics answer for tokens that have a CANDLE market. So a
949
- mint that candle_get_feed just returned can still come back MARKET_NOT_FOUND from those two, and
950
- that is a coverage boundary, not a fault and not a reason to retry, re-auth, or tell the human the
951
- integration is down. Report it as "Candle has no market for this token, so I could not run
952
- forensics on it" and let the human decide.
1011
+ Feed rows carry jupiterOk / externalTradeable / paperDiscoveryOk / organic0LiveOk on external
1012
+ Solana mints. Use discovery=paper on candle_get_feed when rehearsing; on live screens do NOT
1013
+ hard-skip organicScore=0 when organic0LiveOk is true (visible m5 momentum). Hard-skip organic0
1014
+ only when organic0LiveOk is false.
1015
+ candle_get_market and candle_resolve_token answer for same-chain indexed external mints even
1016
+ when trade.routable is false. Read candleLaunched, launchpad, venue and trade; jupiterOk and
1017
+ paperDiscoveryOk are separate signals. Identity-only Solana reads have route_unverified.
1018
+ General quotes use POST /api/v1/trade/agent/quote; curve quotes/lifecycle describe Candle launches.
1019
+ Stored eligibility is not a successful quote or permission. MARKET_NOT_FOUND remains a legacy
1020
+ code: read error.routing.reason, error.discovery and sibling error.retryable. A curve-only
1021
+ 404 is endpoint guidance, not a verdict that Candle cannot trade external tokens.
1022
+ candle_token_forensics also answers for Solana tokens the feed already knows, with a partial
1023
+ report: on-chain developer (never a launchpad shared authority), went-to-zero record, token
1024
+ safety flags, same-funder insiders and cluster. Deploy-window stays unavailable without a
1025
+ Candle launch record. Indexed external Hood tokens also answer, with unknown hacc flags.
1026
+ Unknown mints can still come back MARKET_NOT_FOUND. That remaining gap is a coverage boundary,
1027
+ not a fault and not a reason to retry, re-auth, or tell the human the integration is down.
1028
+ Report MARKET_NOT_FOUND as "Candle has no market for this token, so I could not run forensics
1029
+ on it" and let the human decide.
953
1030
 
954
1031
  Never let a MARKET_NOT_FOUND stand in for a clean bill of health. The same rule governs the
955
1032
  coverage note on every forensics measurement: "unavailable" is NOT "clean" — say so rather than
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@candledottv/mcp",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "MCP server for the Candle agent rail: launch tokens, read markets, report activity",
5
5
  "type": "module",
6
6
  "license": "MIT",