@candledottv/mcp 0.9.1 → 0.10.1

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 +2 -2
  2. package/dist/index.js +50 -26
  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`) |
@@ -97,7 +97,7 @@ tools, getting a key, funding the embedded wallet, and idempotent retries, see
97
97
  | `candle_get_wallets` | The account's embedded wallets, one per chain, with delegation state | `GET /api/v1/agent/wallets/embedded` | `CANDLE_AGENT_API_KEY` |
98
98
  | `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` |
99
99
  | `candle_set_profile_wallets` | Replace the wallets an agent profile may spend from | `PUT /api/v1/agent/keys/:prefix/wallets` | `CANDLE_AGENT_API_KEY` |
100
- | `candle_get_profile_pnl` | An agent profile's realized P&L, fees, and open positions at cost basis | `GET /api/v1/agent/keys/:prefix/pnl` | `CANDLE_AGENT_API_KEY` |
100
+ | `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) | `GET /api/v1/agent/keys/:prefix/pnl` | `CANDLE_AGENT_API_KEY` |
101
101
  | `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` |
102
102
  | `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` |
103
103
  | `candle_get_operation` | Look up a trade or launch by the id its write used, and whether it landed | `GET /api/v1/trade/agent/jobs/:clientId` or `/api/v1/launch/headless/jobs/:clientId` | `CANDLE_AGENT_API_KEY` |
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.1";
109
+ var SERVER_VERSION = "0.10.1";
110
110
 
111
111
  // src/update-notice.ts
112
112
  var PLAIN_VERSION = /^\d+\.\d+\.\d+$/;
@@ -220,15 +220,24 @@ 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
  }
227
232
  function isPaperFlag(value) {
228
233
  return value === true || value === "true" || value === 1 || value === "1";
229
234
  }
230
235
  function heldPaperPosition(positions, mint) {
231
- return positions.find((p) => p.mint === mint && p.amountRaw !== "0");
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");
232
241
  }
233
242
  async function readPaperInventory(cfg, doFetch, extra) {
234
243
  const res = await doFetch(`${base(cfg)}/api/v1/trade/agent/paper/inventory`, {
@@ -261,13 +270,18 @@ async function executeTrade(args, cfg, doFetch) {
261
270
  const amount = args.amount;
262
271
  const read = await readMarket(args.mint, cfg, doFetch, { clientTradeId });
263
272
  let decimals;
264
- 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) {
265
279
  const quoteDecimals = read.market.quoteDecimals;
266
280
  if (typeof quoteDecimals !== "number") {
267
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 });
268
282
  }
269
283
  decimals = quoteDecimals;
270
- } else if (read.status === 404) {
284
+ } else if (read.status === 404 && read.reason !== "chain_mismatch") {
271
285
  const q = quoteIdDecimals(args.quoteAsset, chainForMint(args.mint), { clientTradeId });
272
286
  if ("err" in q)
273
287
  return q.err;
@@ -288,7 +302,7 @@ async function executeTrade(args, cfg, doFetch) {
288
302
  return errText(`could not resolve decimals for mint ${args.mint}; pass a raw-ready amount via the SDK instead`, { clientTradeId });
289
303
  }
290
304
  decimals = read.market.decimals;
291
- } else if (read.status === 404) {
305
+ } else if (read.status === 404 && read.reason !== "chain_mismatch") {
292
306
  const inventory = await readPaperInventory(cfg, doFetch, { clientTradeId });
293
307
  if ("err" in inventory) {
294
308
  return paper ? inventory.err : read.err;
@@ -795,7 +809,7 @@ var swapShape = {
795
809
  amount: z.string().optional().describe('Decimal amount of `from` to spend, e.g. "0.5". Preferred. Pass exactly one of amount or amountRaw.'),
796
810
  amountRaw: z.string().optional().describe("Raw base units of `from`, as a positive integer string. Kept for callers that already " + "compute raw units; new callers should use `amount`."),
797
811
  maxSlippageBps: z.number().optional().describe("Slippage bound in bps, 0-10000. Server defaults to 100 (1%)"),
798
- clientSwapId: z.string().optional().describe("Optional dedup key. Only coalesces a duplicate that arrives while the first call is still " + "in flight; one arriving after it settled will swap again")
812
+ clientSwapId: z.string().optional().describe("Durable idempotency key. Same id + same from/to/amountRaw/effective slippage (omitted means " + "100 bps) replays the stored result, including an indeterminate SWAP_FAILED with the " + "signature in the message. A different body is rejected. Omit it and nothing is coalesced. " + "Pass one so a timeout retry returns that stored result.")
799
813
  };
800
814
  var tradeShape = {
801
815
  mint: z.string().describe("Token mint (solana) or contract address (hood)"),
@@ -841,14 +855,12 @@ function registerTools(server, env = process.env) {
841
855
  }, async (args) => callAndRelay("candle_launch_token", args, cfg));
842
856
  register("candle_get_market", {
843
857
  title: "Get market state",
844
- description: "Read the current market state for a token: lifecycle, pool address, whether buys are " + `open. Reads only; moves nothing. No key needed.
845
-
846
- ` + "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.",
847
859
  inputSchema: getMarketShape
848
860
  }, async (args) => callAndRelay("candle_get_market", args, cfg));
849
861
  register("candle_token_forensics", {
850
862
  title: "Token forensics",
851
- 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), holder concentration, same-funder insider share, same-funder deployer cluster, 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.
852
864
 
853
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.`,
854
866
  inputSchema: tokenForensicsShape
@@ -857,7 +869,7 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
857
869
  title: "Get a token feed",
858
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.
859
871
 
860
- ` + "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.
861
873
 
862
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.",
863
875
  inputSchema: getFeedShape
@@ -901,8 +913,8 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
901
913
  inputSchema: setProfileWalletsShape
902
914
  }, async (args) => callAndRelay("candle_set_profile_wallets", args, cfg));
903
915
  register("candle_get_profile_pnl", {
904
- title: "Read an agent profile's realized P&L",
905
- 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 -- not their current value, which is not marked " + "here. 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. Reads only.",
916
+ title: "Read an agent profile's P&L",
917
+ 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. Reads only.",
906
918
  inputSchema: profilePnlShape
907
919
  }, async (args) => callAndRelay("candle_get_profile_pnl", args, cfg));
908
920
  register("candle_get_profile_trades", {
@@ -935,7 +947,8 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
935
947
  ` + `A bridge behaves differently and the difference matters:
936
948
  ` + `- It is several transactions, not one, and it takes time rather than settling on the call.
937
949
  ` + "- A confirmed source transaction is NOT proof the destination was credited. Read the " + "returned status before treating the funds as arrived; the response carries the venue's " + `own status URLs for the cross-chain fill.
938
- ` + "- Do NOT re-send after a timeout. `clientSwapId` only coalesces a duplicate that arrives " + "while the first call is still in flight; once the first has settled, a second call with " + `the same id bridges AGAIN. If a bridge times out, check its status rather than retrying.
950
+ ` + "- A timeout is unknown, not failed. Pass a `clientSwapId` and retry the SAME request, " + "including the same slippage. The replay returns the stored result: the original success, " + "or a stored error. An indeterminate first leg comes back as SWAP_FAILED, retryable false, " + "with the signature in the message -- verify that on-chain before a new id. A swap that is " + "still running, with no stored outcome, is a retryable conflict. A different body under the " + "same id is rejected. A confirmed first leg is replayed with its hash, retryable false, and " + "is not run again. retryable true on the first LEG2_FAILED means send leg 2 as a new request. " + `Omitting the id never coalesces -- do not retry a timed-out call that had no id.
951
+ ` + "- If a bridge times out, check the returned status URLs rather than treating the funds " + `as arrived or lost.
939
952
 
940
953
  ` + 'Amounts are decimal (`amount`, e.g. "0.5"); `amountRaw` still accepts raw base units for ' + "callers that already compute them. Test-environment keys are refused: every leg settles " + "on a live venue.",
941
954
  inputSchema: swapShape
@@ -960,7 +973,7 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
960
973
  ` + `Before the first trade of a run, once:
961
974
  ` + "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.
962
975
  ` + "2. candle_resolve_token -- if a human handed you a bare address. It returns the chain, " + `so you never have to guess it.
963
- ` + "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.
976
+ ` + "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.
964
977
 
965
978
  ` + "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.
966
979
 
@@ -991,19 +1004,30 @@ START HERE — five tools need NO credential. Call these first to confirm the se
991
1004
  candle_get_market price, market cap, volume, curve state for one token
992
1005
  candle_get_feed the roster: hot streak, new pairs, graduated, blue chip
993
1006
  candle_resolve_token a ticker or partial name -> mint address + chain
994
- 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), holder concentration, same-funder insider share, same-funder cluster, and a risk tier LOW/MODERATE/HIGH/CRITICAL with per-factor reasons
1007
+ 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
995
1008
  candle_get_agent_profile your own tier, caps and verified activity
996
1009
 
997
1010
  COVERAGE — read this before you treat an error as a broken server.
998
1011
  candle_get_feed indexes the wider market (pump.fun, pons.family and other external launchpads).
999
- candle_get_market answers for tokens that have a CANDLE market. candle_token_forensics also
1000
- answers for Solana tokens the feed already knows, with a partial report: on-chain developer
1001
- (never a launchpad shared authority), went-to-zero record, holder concentration, same-funder
1002
- insiders and cluster. Deploy-window stays unavailable without a Candle launch record. Hood
1003
- tokens Candle did not launch, and unknown mints, still come back MARKET_NOT_FOUND. That is a
1004
- coverage boundary, not a fault and not a reason to retry, re-auth, or tell the human the
1005
- integration is down. Report MARKET_NOT_FOUND as "Candle has no market for this token, so I
1006
- could not run forensics on it" and let the human decide.
1012
+ Feed rows carry jupiterOk / externalTradeable / paperDiscoveryOk / organic0LiveOk on external
1013
+ Solana mints. Use discovery=paper on candle_get_feed when rehearsing; on live screens do NOT
1014
+ hard-skip organicScore=0 when organic0LiveOk is true (visible m5 momentum). Hard-skip organic0
1015
+ only when organic0LiveOk is false.
1016
+ candle_get_market and candle_resolve_token answer for same-chain indexed external mints even
1017
+ when trade.routable is false. Read candleLaunched, launchpad, venue and trade; jupiterOk and
1018
+ paperDiscoveryOk are separate signals. Identity-only Solana reads have route_unverified.
1019
+ General quotes use POST /api/v1/trade/agent/quote; curve quotes/lifecycle describe Candle launches.
1020
+ Stored eligibility is not a successful quote or permission. MARKET_NOT_FOUND remains a legacy
1021
+ code: read error.routing.reason, error.discovery and sibling error.retryable. A curve-only
1022
+ 404 is endpoint guidance, not a verdict that Candle cannot trade external tokens.
1023
+ candle_token_forensics also answers for Solana tokens the feed already knows, with a partial
1024
+ report: on-chain developer (never a launchpad shared authority), went-to-zero record, token
1025
+ safety flags, same-funder insiders and cluster. Deploy-window stays unavailable without a
1026
+ Candle launch record. Indexed external Hood tokens also answer, with unknown hacc flags.
1027
+ Unknown mints can still come back MARKET_NOT_FOUND. That remaining gap is a coverage boundary,
1028
+ not a fault and not a reason to retry, re-auth, or tell the human the integration is down.
1029
+ Report MARKET_NOT_FOUND as "Candle has no market for this token, so I could not run forensics
1030
+ on it" and let the human decide.
1007
1031
 
1008
1032
  Never let a MARKET_NOT_FOUND stand in for a clean bill of health. The same rule governs the
1009
1033
  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.1",
3
+ "version": "0.10.1",
4
4
  "description": "MCP server for the Candle agent rail: launch tokens, read markets, report activity",
5
5
  "type": "module",
6
6
  "license": "MIT",